logoESLint React
Rules

refs

Full Name in @eslint-react/eslint-plugin@beta

@eslint-react/refs

Full Name in eslint-plugin-react-x@beta

react-x/refs

Presets

Description

Validates correct usage of refs by checking that ref.current is not read or written during render.

Refs hold values that aren't used for rendering. Unlike state, changing a ref doesn't trigger a re-render. Reading or writing ref.current during render breaks React's expectations. Refs might not be initialized when you try to read them, and their values can be stale or inconsistent.

How It Detects Refs

The lint only applies these rules to values it knows are refs. A value is inferred as a ref when:

  • Returned from useRef() or React.createRef().

    const scrollRef = useRef(null);
  • An identifier named ref or ending in Ref that reads from or writes to .current.

    buttonRef.current = node;
  • Passed through a JSX ref prop (for example <div ref={someRef} />).

    <input ref={inputRef} />

Once something is marked as a ref, the lint surfaces violations when ref.current is accessed directly in the render phase of a component or hook body (i.e. not inside effects, event handlers, callbacks, or other nested functions).

Common Violations

  • Reading ref.current during render
  • Updating refs during render
  • Using refs for values that should be state

Invalid

// ❌ Reading ref during render
function Component() {
  const ref = useRef(0);
  const value = ref.current; // Don't read during render
  return <div>{value}</div>;
}
// ❌ Modifying ref during render
function Component({ value }) {
  const ref = useRef(null);
  ref.current = value; // Don't modify during render
  return <div />;
}

Valid

// ✅ Read ref in effects/handlers
function Component() {
  const ref = useRef(null);
  useEffect(() => {
    if (ref.current) {
      console.log(ref.current.offsetWidth); // OK in effect
    }
  });
  return <div ref={ref} />;
}
// ✅ Use state for UI values
function Component() {
  const [count, setCount] = useState(0);
  return (
    <button onClick={() => setCount(count + 1)}>
      {count}
    </button>
  );
}
// ✅ Lazy initialization of ref value
function Component() {
  const ref = useRef(null);
  // Initialize only once on first use
  if (ref.current === null) {
    ref.current = expensiveComputation(); // OK - lazy initialization
  }
  const handleClick = () => {
    console.log(ref.current); // Use the initialized value
  };
  return <button onClick={handleClick}>Click</button>;
}

Troubleshooting

The lint flagged my plain object with .current

The name heuristic intentionally treats ref.current and fooRef.current as real refs. If you're modeling a custom container object, pick a different name (for example, box) or move the mutable value into state. Renaming avoids the lint because the compiler stops inferring it as a ref.

I need to read a ref value in JSX

Refs are meant for values that don't affect rendering output. If you need to display a value in JSX, use state instead:

// ❌ Wrong: Reading ref in JSX
function Component() {
  const ref = useRef(0);
  return <div>{ref.current}</div>;
}
// ✅ Correct: Use state for rendered values
function Component() {
  const [count, setCount] = useState(0);
  return <div>{count}</div>;
}

Examples

Failing

function Component() {
  const ref = useRef(0);
  const value = ref.current;
  //            ^^^^^^^^^^^
  //            - Do not read 'ref.current' during render. Refs are not available during rendering
  //              and their values may be stale or inconsistent. Move this read into an effect or
  //              event handler.
  return <div>{value}</div>;
}
function Component({ value }) {
  const ref = useRef(null);
  ref.current = value;
  // ^^^^^^^^^^^
  // - Do not write to 'ref.current' during render. Refs should only be mutated in effects
  //   or event handlers. Move this write into an effect or event handler.
  return <div />;
}
function Component() {
  const ref = useRef(0);
  return <div>{ref.current}</div>;
  //          ^^^^^^^^^^^
  //          - Do not read 'ref.current' during render.
}
function useMyHook() {
  const ref = useRef(0);
  const value = ref.current;
  //            ^^^^^^^^^^^
  //            - Do not read 'ref.current' during render.
  return value;
}

Passing

function Component() {
  const ref = useRef(null);
  useEffect(() => {
    if (ref.current) {
      console.log(ref.current.offsetWidth);
    }
  });
  return <div ref={ref} />;
}
function Component() {
  const ref = useRef(null);
  const handleClick = () => {
    console.log(ref.current);
  };
  return <button onClick={handleClick}>Click</button>;
}
function Component() {
  const ref = useRef(null);
  if (ref.current === null) {
    ref.current = expensiveComputation();
  }
  return <div />;
}
function Component() {
  const ref = useRef(null);
  useLayoutEffect(() => {
    console.log(ref.current);
  }, []);
  return <div ref={ref} />;
}

Implementation

Further Reading

On this page