refs
Validates correct usage of refs by checking that 'ref.current' is not read or written during render.
This is an evaluation implementation and may contain false positives or negatives that have not yet been fully audited. Review each report carefully before applying fixes.
Full Name in eslint-plugin-react-x
react-x/refsFull Name in @eslint-react/eslint-plugin
@eslint-react/refsFeatures
🧪
Rule Details
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()orReact.createRef().const scrollRef = useRef(null); -
An identifier named
refor ending inRefthat reads from or writes to.current.buttonRef.current = node; -
Passed through a JSX
refprop (for example<div ref={someRef} />).<input ref={inputRef} />
Once something is marked as a ref, the lint surfaces violations when ref.current is accessed during the render phase of a component or hook. Effects, event handlers, useCallback, timers, promise handlers, and other deferred callbacks are outside render. A nested function only shields the access it contains if that function is not invoked during render: calling a local helper (even through an alias), an IIFE, a synchronous array callback, or a useState/useMemo/useReducer render-time callback is still checked.
// 🔴 Problem: helper mutates a ref, and is called during render
function Component() {
const ref = useRef(null);
const setRef = () => {
ref.current = false;
// ^^^ flagged: this runs during render
};
setRef();
return <button ref={ref} />;
}Examples
Reading ref.current during render
Refs are not guaranteed to be initialized during render. Reading them in the component body may return null or a stale value from a previous render cycle.
// 🔴 Problem: reading ref during render
function Component() {
const ref = useRef(0);
const value = ref.current; // read during render
// ^^^ Cannot access refs during render
return <div>{value}</div>;
}// 🟢 Recommended: use state to display values
function Component() {
const [count, setCount] = useState(0);
return <div>{count}</div>;
}Reading ref.current in custom hooks
Custom hooks follow the same rules as components. Reading ref.current during the execution of a custom hook is still part of the render phase.
// 🔴 Problem: reading ref.current inside a custom hook during render
function useMyHook() {
const ref = useRef(0);
const value = ref.current;
// ^^^ Cannot access refs during render
return value;
}// 🟢 Recommended: pass the ref to a component and read it in an effect
function Component() {
const ref = useRef(null);
useEffect(() => {
if (ref.current) {
console.log(ref.current.offsetWidth);
}
}, []);
return <div ref={ref} />;
}Modifying ref.current during render
Writing to a ref during render is a side effect. React may invoke your component multiple times before committing, so the ref write may happen more often than you expect.
// 🔴 Problem: modifying ref during render
function Component({ value }) {
const ref = useRef(null);
ref.current = value; // modified during render
// ^^^ Cannot update ref during render
return <div />;
}// 🟢 Recommended: modify refs in effects or event handlers
function Component() {
const ref = useRef(null);
useEffect(() => {
if (ref.current) {
console.log(ref.current.offsetWidth);
}
});
return <div ref={ref} />;
}// 🟢 Recommended: use useLayoutEffect when you need to measure DOM nodes before paint
function Component() {
const ref = useRef(null);
useLayoutEffect(() => {
console.log(ref.current);
}, []);
return <div ref={ref} />;
}Lazy initialization of ref value
There is one exception: lazy initialization of a ref value behind an exact null or undefined check is a common and safe pattern, as long as it only runs once. Truthiness checks such as if (!ref.current) are not sufficient because an initialized ref may legitimately contain 0, false, or an empty string.
// 🔵 OK: lazy initialization of ref value
function Component() {
const ref = useRef(null);
if (ref.current === null) {
ref.current = expensiveComputation(); // lazy initialization
}
const handleClick = () => {
console.log(ref.current);
};
return <button onClick={handleClick}>Click</button>;
}Only a single guarded initialization per ref is allowed. Guarding the same ref a second time (whether in the same or a different if block) is flagged, since it means the ref is being initialized more than once during render.
// 🔴 Problem: the same ref is guard-initialized twice
function Component() {
const ref = useRef(null);
if (ref.current == null) {
ref.current = 1;
}
if (ref.current == null) {
ref.current = 2;
// ^^^ Ref is initialized more than once during render.
}
return <div />;
}Using refs for rendered values
If a value affects what is shown on screen, it should be state, not a ref. Refs are meant for values that don't affect rendering output.
// 🔴 Problem: using a ref to store a value that needs to be displayed in JSX
function Component() {
const ref = useRef(0);
return <div>{ref.current}</div>;
// ^^^ Cannot access refs during render
}// 🟢 Recommended: use state to store rendered values
function Component() {
const [count, setCount] = useState(0);
return <div>{count}</div>;
}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.
Versions
Resources
Further Reading
See Also
react-naming-convention/ref-name
Enforces identifier names assigned fromuseRefcalls to be eitherrefor end withRef.react-x/no-create-ref
DisallowscreateRefin function components and Hooks.react-x/no-forward-ref
Replaces usage offorwardRefwith passingrefas a prop.