React 43 ⚛️ Storing Mutable Values with useRef
useRef returns a plain object with a single property, current. React creates that object once, on the first render, and returns the same object on every subsequent render. Nothing about the object is special. It is not a proxy, it is not reactive, and it is not tracked by the reconciler. It is a box. You can put a number in it, a string, an object, a function, a timer ID, a DOM node — anything. You can change what is inside the box at any time, and React will not notice. This is the entire mechanism, and it is the reason useRef is the tool for mutable values that need to survive renders without triggering them.
The comparison with useState is the key. Both hooks persist a value across renders. The difference is what happens when the value changes. useState returns a value and a setter. When the setter is called, React schedules a re-render, and the new value is available in the next render. useRef returns an object. When .current is assigned, nothing happens. No render is scheduled, no effect runs, and the component continues with the same output. The value has changed, but the UI has not. This is exactly what is needed for values that are part of the component’s implementation rather than its output: a timer ID, a previous value, a flag that tracks whether an effect has run, a WebSocket connection, a mutable counter that is read in event handlers.
The timing rules matter as much as the storage rules. A ref can be read and written during the render, but the value it holds is not guaranteed to be consistent with the current render. The React documentation is explicit: do not read or write ref.current during rendering, except for initialization. The reason is that React may render a component more than once for the same commit, or discard a render entirely, and a ref that is mutated during the render will hold whatever the last discarded render wrote. The safe pattern is to read and write refs in effects and event handlers, which run after the commit, when the render is final. There are exceptions — lazy initialization and the “latest value” pattern — but they are deliberate and narrow.
This chapter covers three areas. First, why useRef is the right container for mutable values — the difference between state and a ref, and the problems that arise from putting non-rendering values in state. Second, how to store and read mutable values — the patterns for timer IDs, previous values, flags, and instance variables, and the rules for reading and writing refs. Third, the advanced patterns and their caveats — the latest-value pattern for callbacks, lazy initialization, and the cases where a ref is the wrong choice. The chapter ends with a complete example session, a quick reference, best practices, common pitfalls, real-world examples, and diagrams showing the ref lifecycle and the state-versus-ref decision.
Key point: useRef returns a mutable object with a .current property that persists across renders. Assigning to .current does not trigger a re-render. Use it for values that are part of the component’s implementation and do not affect its output: timer IDs, previous values, flags, and instance variables. Do not read or write .current during the render, except for lazy initialization; do it in effects and event handlers instead.
Why useRef stores mutable values
The state-for-non-rendering-values problem. State is for values that the UI depends on. When the value changes, the UI must update, so React re-renders. But some values are not part of the UI. A timer ID is used to clear a timer; it is never displayed. A previous value is used to compare with the current value; it is not rendered. A flag that tracks whether an effect has run is used to skip logic; it is not rendered. Putting these in state causes a re-render every time they change, even though the render output is identical. The re-render is wasted work, and in some cases it causes bugs: a flag that triggers a re-render can cause an effect to run again, which changes the flag, which triggers another re-render. A ref avoids all of this. It holds the value without notifying React, so the component does not re-render when the value changes .
The render-persistence problem. A local variable declared inside a component is recreated on every render. It does not persist. A value that needs to survive across renders must be stored somewhere that React preserves. State is one option; a ref is another. The difference is the re-render behavior. A ref is the right choice when the value must persist but must not trigger a render. The box is created once and handed back on every render, and the value inside survives because the box itself survives .
The closure-staleness problem. A function defined inside a component captures the values from the render in which it was created. An event handler that captures a state variable sees the value from the render in which the handler was attached. If the state changes, the handler sees the old value. This is the stale-closure problem, and it is the reason the “latest value” pattern exists: a ref holds the current value, and the handler reads the ref instead of the captured state. The ref is not stale because it is the same box across renders, and its .current property holds the latest assignment. This pattern is common in timers, subscriptions, and third-party integrations that capture callbacks .
The initialization-once problem. Some values should be computed only once, on the first render, and reused thereafter. An expensive computation, a connection object, an ID generated once — these should not be recomputed on every render. A ref provides the storage, and the pattern is to check whether .current is null and initialize it if it is. This is lazy initialization: the value is computed on the first access, not on every render. The ref is the container, and the check is the guard .
The instance-variable problem. A class component has instance variables: fields that persist for the lifetime of the component instance and do not trigger a re-render when they change. A function component has no instance, but a ref provides the same capability. A ref is a field on the component’s persistent state, and it behaves like an instance variable: it persists, it is mutable, and it does not affect rendering. The translation from class to function component is often a matter of converting instance variables into refs .
The trade-off. A ref is an escape hatch from React’s rendering model. Because React does not track the value, React cannot react to changes. If the value needs to affect the UI, a ref is the wrong choice, and the UI will not update. If the value needs to be read during the render, a ref is unreliable, because the render may be discarded. The discipline is to use refs only for values that are truly outside the rendering model: timers, subscriptions, connections, previous values, and flags. For everything else, state is the right tool. The trade-off is between the persistence without rendering that a ref provides and the rendering model that state participates in.
a. The ref object and the state comparison
useRef(initialValue) returns an object with a single property, current, initialized to initialValue. The object is created on the first render and returned unchanged on every subsequent render .
import { useRef } from 'react';
function Counter() {
const countRef = useRef(0);
console.log(countRef.current); // 0 on first render, 0 on every render
function increment() {
countRef.current += 1;
console.log(countRef.current); // the new value, but no re-render
}
return <button onClick={increment}>Increment</button>;
}
The countRef.current is 0 on the first render and remains 0 in the rendered output, because the component does not re-render when the ref changes. The button’s click handler increments the ref, and the new value is stored, but the UI is unchanged. This is the key behavior: the ref holds the value, but the value is invisible to the render.
The contrast with useState is exact:
function Counter() {
const [count, setCount] = useState(0);
function increment() {
setCount(count + 1); // schedules a re-render
}
return <button onClick={increment}>{count}</button>;
}
Here, setCount schedules a re-render, and the new value appears in the output. The state is part of the rendering model; the ref is not.
The decision rule is: does the rendered output depend on the value? If yes, it is state. If no, it can be a ref. A timer ID is not rendered, so it can be a ref. A count that appears on the screen is rendered, so it must be state. A previous value that is displayed alongside the current value is rendered, so it must be state — or a ref that is read in the render, which is the exception discussed in the next section.
b. Reading and writing refs
The React documentation gives a clear rule: do not read or write ref.current during rendering, except for initialization. The reason is that the render may be called multiple times, or discarded, and a ref mutated during the render will hold the value from the last call, which may not be the render that is committed .
function MyComponent() {
const ref = useRef(0);
// ❌ WRONG: reading and writing during render
ref.current += 1;
console.log(ref.current);
return <div>{ref.current}</div>;
}
The component above reads and writes the ref during the render. React may call the function twice in development (Strict Mode), which would increment the ref twice. The displayed value would be unpredictable. The fix is to move the mutation into an effect or an event handler:
function MyComponent() {
const ref = useRef(0);
useEffect(() => {
ref.current += 1; // ✅ safe: runs after commit
});
return <div>{/* do not read ref.current here */}</div>;
}
The effect runs after the commit, so the mutation is applied once per committed render. Reading the ref during the render is also discouraged, because the value may not be what the render expects. The render should be a pure function of props and state, and a ref read is not part of that contract.
The exceptions are narrow and deliberate:
Lazy initialization. The first render initializes the ref if it is null. This is a read and a write during the render, but it happens once, and the value is stable thereafter. The pattern is safe because the check is idempotent: if the ref is already initialized, the initialization is skipped .
function ExpensiveComponent() {
const ref = useRef(null);
if (ref.current === null) {
ref.current = computeExpensiveValue();
}
return <div>{/* use ref.current */}</div>;
}
The latest value pattern. The render reads the ref to pass it to a callback, and an effect writes the latest value to the ref. The read is safe because the ref holds the value from the previous render, and the effect updates it after the render. The pattern is used when a stable callback needs access to the latest props or state .
function useLatest(value) {
const ref = useRef(value);
useEffect(() => {
ref.current = value;
}, [value]);
return ref;
}
The useLatest hook returns a ref that always holds the latest value. The ref is updated in an effect, so the render sees the previous value, and the callback reads the latest value when it is invoked. This is the standard pattern for timers, subscriptions, and event handlers that need the current value without being recreated.
c. The common patterns
The mutable-value use of useRef follows a small number of recurring patterns. Each pattern has a characteristic use case and a characteristic timing rule.
Timer IDs. A timer ID is created by setInterval or setTimeout and used to clear the timer. It is not rendered, so it belongs in a ref. The timer is started in an effect or an event handler, and the ID is stored in the ref. The cleanup effect reads the ref and clears the timer .
function Stopwatch() {
const [seconds, setSeconds] = useState(0);
const timerRef = useRef(null);
function start() {
if (timerRef.current) return;
timerRef.current = setInterval(() => {
setSeconds((s) => s + 1);
}, 1000);
}
function stop() {
clearInterval(timerRef.current);
timerRef.current = null;
}
useEffect(() => {
return () => clearInterval(timerRef.current);
}, []);
return (
<>
<p>{seconds}</p>
<button onClick={start}>Start</button>
<button onClick={stop}>Stop</button>
</>
);
}
The timerRef holds the interval ID. The start function checks whether a timer is already running before starting a new one. The cleanup effect clears the timer when the component unmounts. The ref is the right container because the ID is not displayed and changing it should not re-render.
Previous values. A previous value is a value from the last render, used to compare with the current value. The pattern is to store the value in a ref after each render, so the render sees the previous value and the effect updates it .
function Counter() {
const [count, setCount] = useState(0);
const prevCountRef = useRef();
useEffect(() => {
prevCountRef.current = count;
}, [count]);
return (
<p>
Now: {count}, Before: {prevCountRef.current}
</p>
);
}
The prevCountRef holds the previous value of count. The effect updates it after each render where count changes. The render reads the ref, which holds the value from the previous render. This is a read during the render, but it is safe because the ref is updated by an effect, and the value is consistent with the previous committed render.
Flags and instance variables. A flag that tracks whether an effect has run, whether a component is mounted, or whether a value has been initialized is a classic use for a ref. The flag is not rendered, so it belongs in a ref. The pattern is to initialize it with a value and mutate it in effects and event handlers .
function Form() {
const [text, setText] = useState('');
const isFirstRender = useRef(true);
useEffect(() => {
if (isFirstRender.current) {
isFirstRender.current = false;
return;
}
console.log('Form updated');
}, [text]);
return <input value={text} onChange={(e) => setText(e.target.value)} />;
}
The isFirstRender ref starts as true and is set to false after the first effect. The effect skips its logic on the first render. The ref is the right container because the flag does not affect the output; it only affects the effect’s behavior.
Connections and subscriptions. A WebSocket connection, an event listener, or a subscription is an object that persists across renders and needs to be closed when the component unmounts. The object is not rendered, so it belongs in a ref. The pattern is to create it in an effect and close it in the cleanup .
function ChatRoom({ roomId }) {
const [messages, setMessages] = useState([]);
const socketRef = useRef(null);
useEffect(() => {
const socket = new WebSocket(`wss://example.com/${roomId}`);
socketRef.current = socket;
socket.onmessage = (event) => {
setMessages((prev) => [...prev, JSON.parse(event.data)]);
};
return () => {
socket.close();
socketRef.current = null;
};
}, [roomId]);
return (
<ul>
{messages.map((m) => (
<li key={m.id}>{m.text}</li>
))}
</ul>
);
}
The socketRef holds the connection. The effect creates it and the cleanup closes it. The ref is not read during the render; it is only written in the effect and read in the cleanup. The connection is an instance variable of the component, and the ref is its container.
Complete Example Session
// ============================================
// PART 1: A REF HOLDS A VALUE WITHOUT RENDERING
// ============================================
import { useRef } from 'react';
function Counter() {
const countRef = useRef(0);
function increment() {
countRef.current += 1;
console.log(countRef.current);
}
return <button onClick={increment}>Increment</button>;
}
// The ref changes, but the component does not re-render.
// The value is invisible to the UI.
// ============================================
// PART 2: STATE VS REF
// ============================================
function CounterWithState() {
const [count, setCount] = useState(0);
return (
<button onClick={() => setCount(count + 1)}>
{count} {/* re-renders on every click */}
</button>
);
}
function CounterWithRef() {
const countRef = useRef(0);
return (
<button onClick={() => countRef.current++}>
Increment {/* never re-renders */}
</button>
);
}
// ============================================
// PART 3: TIMER ID IN A REF
// ============================================
function Stopwatch() {
const [seconds, setSeconds] = useState(0);
const timerRef = useRef(null);
function start() {
if (timerRef.current) return;
timerRef.current = setInterval(() => {
setSeconds((s) => s + 1);
}, 1000);
}
function stop() {
clearInterval(timerRef.current);
timerRef.current = null;
}
useEffect(() => {
return () => clearInterval(timerRef.current);
}, []);
return (
<>
<p>{seconds}</p>
<button onClick={start}>Start</button>
<button onClick={stop}>Stop</button>
</>
);
}
// ============================================
// PART 4: PREVIOUS VALUE IN A REF
// ============================================
function Counter() {
const [count, setCount] = useState(0);
const prevCountRef = useRef();
useEffect(() => {
prevCountRef.current = count;
}, [count]);
return (
<p>
Now: {count}, Before: {prevCountRef.current}
</p>
);
}
// ============================================
// PART 5: SKIP FIRST RENDER
// ============================================
function Form() {
const [text, setText] = useState('');
const isFirstRender = useRef(true);
useEffect(() => {
if (isFirstRender.current) {
isFirstRender.current = false;
return;
}
console.log('Form updated');
}, [text]);
return <input value={text} onChange={(e) => setText(e.target.value)} />;
}
// ============================================
// PART 6: THE LATEST VALUE PATTERN
// ============================================
function useLatest(value) {
const ref = useRef(value);
useEffect(() => {
ref.current = value;
}, [value]);
return ref;
}
function useInterval(callback, delay) {
const savedCallback = useLatest(callback);
useEffect(() => {
if (delay === null) return;
const id = setInterval(() => savedCallback.current(), delay);
return () => clearInterval(id);
}, [delay, savedCallback]);
}
// The interval always calls the latest callback,
// without resetting when the callback changes.
// ============================================
// PART 7: LAZY INITIALIZATION
// ============================================
function ExpensiveComponent() {
const ref = useRef(null);
if (ref.current === null) {
ref.current = computeExpensiveValue();
}
return <div>{ref.current}</div>;
}
// The expensive value is computed once, on the first render.
// Subsequent renders reuse the stored value.
// ============================================
// PART 8: IS MOUNTED FLAG
// ============================================
function useIsMounted() {
const isMountedRef = useRef(false);
useEffect(() => {
isMountedRef.current = true;
return () => {
isMountedRef.current = false;
};
}, []);
return isMountedRef;
}
// The ref tracks whether the component is mounted.
// It is used to guard against state updates after unmount.
// ============================================
// PART 9: WEBSOCKET CONNECTION
// ============================================
function ChatRoom({ roomId }) {
const [messages, setMessages] = useState([]);
const socketRef = useRef(null);
useEffect(() => {
const socket = new WebSocket(`wss://example.com/${roomId}`);
socketRef.current = socket;
socket.onmessage = (event) => {
setMessages((prev) => [...prev, JSON.parse(event.data)]);
};
return () => {
socket.close();
socketRef.current = null;
};
}, [roomId]);
return (
<ul>
{messages.map((m) => (
<li key={m.id}>{m.text}</li>
))}
</ul>
);
}
// ============================================
// PART 10: THE COMPLETE PICTURE
// ============================================
// useRef for mutable values:
// - timer IDs
// - previous values
// - flags (isFirstRender, isMounted)
// - connections (WebSocket, EventSource)
// - instance variables
// - lazy initialization
// - the latest value pattern
//
// useState for rendering values:
// - values displayed in the UI
// - values that affect the rendered output
// - values that need to trigger effects
//
// The question: does the rendered output depend on the value?
// Yes → useState
// No → useRef
The ten parts show a ref holding a value without rendering, the state-versus-ref comparison, a timer ID in a ref, a previous value in a ref, skipping the first render, the latest value pattern, lazy initialization, the is-mounted flag, a WebSocket connection, and the complete picture.
Quick Reference
Ref vs State
| Aspect | Ref | State |
|---|---|---|
| Triggers re-render | No | Yes |
| Persists across renders | Yes | Yes |
| Read during render | Discouraged | Yes |
| Written during render | Discouraged | No |
| Use case | Non-rendering values | Rendering values |
The Ref Object
| Property | Meaning |
|---|---|
.current | The stored value |
| Initialization | useRef(initialValue) |
| Persistence | Same object across renders |
| Mutation | ref.current = value |
Common Patterns
| Pattern | Use case |
|---|---|
| Timer ID | setInterval / setTimeout |
| Previous value | Compare with the last render |
| Flag | isFirstRender, isMounted |
| Connection | WebSocket, EventSource |
| Instance variable | Class-component equivalent |
| Lazy initialization | Expensive value computed once |
| Latest value | Stable callback reads current value |
Timing Rules
| Phase | Read/Write |
|---|---|
| During render | Discouraged (except lazy init) |
| In effect | Safe |
| In event handler | Safe |
| In cleanup | Safe |
Best Practices
✅ Do This:
// Use a ref for a timer ID
const timerRef = useRef(null); // ✅
// Use a ref for a previous value
const prevRef = useRef(); // ✅
// Use a ref for a flag
const isFirstRender = useRef(true); // ✅
// Use a ref for a connection
const socketRef = useRef(null); // ✅
// Update refs in effects and event handlers
useEffect(() => { ref.current = value; }, [value]); // ✅
// Clean up in the effect's cleanup function
return () => clearInterval(timerRef.current); // ✅
// Use lazy initialization for expensive values
if (ref.current === null) ref.current = compute(); // ✅
❌ Don’t Do This:
// Don't use a ref for a value that affects rendering
const countRef = useRef(0); // use state instead // ❌
// Don't read ref.current during render
console.log(ref.current); // unreliable // ❌
// Don't write ref.current during render
ref.current += 1; // may run twice in Strict Mode // ❌
// Don't forget cleanup for timers and connections
setInterval(() => {}, 1000); // no cleanup // ❌
// Don't use a ref when a local variable works
const x = useRef(compute()); // runs on every render // ❌
// Don't expect a re-render when the ref changes
ref.current = 1; // no re-render // ❌
Common Pitfalls
| Pitfall | Why It Happens | Fix |
|---|---|---|
| UI does not update | Ref change does not trigger render | Use state for rendering values |
| Value is stale | Read in a closure from an old render | Use the latest value pattern |
| Ref mutated during render | Read/write during render | Move to effect or event handler |
| Strict Mode double-invocation | Ref mutation during render | Move mutation to effect |
| Timer not cleared | No cleanup effect | clearInterval in cleanup |
| Expensive value recomputed | No lazy initialization | Check ref.current === null |
| Ref read before commit | Read during render | Read in effect or event handler |
Real-World Examples
1. Timer ID
const timerRef = useRef(null);
timerRef.current = setInterval(...);
clearInterval(timerRef.current);
2. Previous Value
const prevRef = useRef();
useEffect(() => { prevRef.current = value; }, [value]);
3. Skip First Render
const isFirst = useRef(true);
if (isFirst.current) { isFirst.current = false; return; }
4. Is Mounted
const isMounted = useRef(false);
useEffect(() => { isMounted.current = true; return () => { isMounted.current = false; }; }, []);
5. WebSocket
const socketRef = useRef(null);
useEffect(() => { socketRef.current = new WebSocket(url); return () => socketRef.current.close(); }, [url]);
6. Latest Value
function useLatest(value) {
const ref = useRef(value);
useEffect(() => { ref.current = value; }, [value]);
return ref;
}
7. Lazy Initialization
if (ref.current === null) ref.current = compute();
8. Instance Variable
const renderCount = useRef(0);
renderCount.current++;
9. Interval with Latest Callback
const savedCallback = useLatest(callback);
useEffect(() => { const id = setInterval(() => savedCallback.current(), delay); return () => clearInterval(id); }, [delay]);
10. Mutable Counter
const countRef = useRef(0);
countRef.current++;
Visual
The Ref Object Lifecycle
┌──────────────────────────────────────────────────────────────┐
│ THE REF OBJECT LIFECYCLE │
│ │
│ Render 1 │
│ ┌──────────────────────────────────────────────────────┐ │
│ │ useRef(0) → creates { current: 0 } │ │
│ └──────────────────────────────────────────────────────┘ │
│ │ │
│ ▼ │
│ Commit │
│ ┌──────────────────────────────────────────────────────┐ │
│ │ Effects run │ │
│ │ ref.current can be read and written │ │
│ └──────────────────────────────────────────────────────┘ │
│ │ │
│ ▼ │
│ Render 2 │
│ ┌──────────────────────────────────────────────────────┐ │
│ │ useRef(0) → returns the SAME object │ │
│ │ { current: <whatever was written> } │ │
│ └──────────────────────────────────────────────────────┘ │
│ │ │
│ ▼ │
│ Commit │
│ ┌──────────────────────────────────────────────────────┐ │
│ │ Effects run again │ │
│ └──────────────────────────────────────────────────────┘ │
│ │ │
│ ▼ │
│ ... repeats on every render │
│ │ │
│ ▼ │
│ Unmount │
│ ┌──────────────────────────────────────────────────────┐ │
│ │ Cleanup effects run │ │
│ │ The ref object is garbage collected │ │
│ └──────────────────────────────────────────────────────┘ │
│ │
│ The same object is returned on every render. │
│ The value inside persists. │
│ Changing the value does not trigger a render. │
│ │
└──────────────────────────────────────────────────────────────┘
State vs Ref Decision
┌──────────────────────────────────────────────────────────────┐
│ STATE vs REF DECISION │
│ │
│ Does the rendered output depend on the value? │
│ │ │
│ ├── YES ──► useState │
│ │ ┌──────────────────────────────────────┐ │
│ │ │ const [value, setValue] = useState()│ │
│ │ │ setValue(x) → re-render │ │
│ │ │ value is read in the render │ │
│ │ └──────────────────────────────────────┘ │
│ │ │
│ └── NO ──► useRef │
│ ┌──────────────────────────────────────┐ │
│ │ const ref = useRef(initial) │ │
│ │ ref.current = x → no re-render │ │
│ │ ref.current is read in effects │ │
│ │ and event handlers │ │
│ └──────────────────────────────────────┘ │
│ │
│ Examples: │
│ ┌──────────────────────────────────────────────────────┐ │
│ │ Rendered: count, name, items, error, loading │ │
│ │ Not rendered: timerId, prevValue, isMounted, │ │
│ │ socket, isFirstRender │ │
│ └──────────────────────────────────────────────────────┘ │
│ │
└──────────────────────────────────────────────────────────────┘
The Latest Value Pattern
┌──────────────────────────────────────────────────────────────┐
│ THE LATEST VALUE PATTERN │
│ │
│ function useLatest(value) { │
│ const ref = useRef(value); │
│ │
│ useEffect(() => { │
│ ref.current = value; │
│ }, [value]); │
│ │
│ return ref; │
│ } │
│ │
│ ┌──────────────────────────────────────────────────────┐ │
│ │ Render 1: ref.current = value 1 │ │
│ │ Effect: ref.current = value 1 │ │
│ │ │ │
│ │ Render 2: ref.current = value 1 (previous) │ │
│ │ Effect: ref.current = value 2 (latest) │ │
│ │ │ │
│ │ A stable callback reads ref.current │ │
│ │ → it always sees the latest value │ │
│ │ → the callback does not need to be recreated │ │
│ └──────────────────────────────────────────────────────┘ │
│ │
│ This is the standard pattern for intervals, subscriptions, │
│ and third-party integrations that capture callbacks. │
│ │
└──────────────────────────────────────────────────────────────┘
Common Ref Patterns
┌──────────────────────────────────────────────────────────────┐
│ COMMON REF PATTERNS │
│ │
│ TIMER: │
│ ┌──────────────────────────────────────────────────────┐ │
│ │ const timerRef = useRef(null); │ │
│ │ timerRef.current = setInterval(...) │ │
│ │ clearInterval(timerRef.current) │ │
│ └──────────────────────────────────────────────────────┘ │
│ │
│ PREVIOUS VALUE: │
│ ┌──────────────────────────────────────────────────────┐ │
│ │ const prevRef = useRef(); │ │
│ │ useEffect(() => { prevRef.current = value; }, [value])│ │
│ └──────────────────────────────────────────────────────┘ │
│ │
│ FLAG: │
│ ┌──────────────────────────────────────────────────────┐ │
│ │ const isFirst = useRef(true); │ │
│ │ if (isFirst.current) { isFirst.current = false; } │ │
│ └──────────────────────────────────────────────────────┘ │
│ │
│ CONNECTION: │
│ ┌──────────────────────────────────────────────────────┐ │
│ │ const socketRef = useRef(null); │ │
│ │ useEffect(() => { │ │
│ │ socketRef.current = new WebSocket(url); │ │
│ │ return () => socketRef.current.close(); │ │
│ │ }, [url]); │ │
│ └──────────────────────────────────────────────────────┘ │
│ │
└──────────────────────────────────────────────────────────────┘
Summary
| Item | Value |
|---|---|
useRef | Returns { current: initialValue } |
| Persistence | Same object across renders |
| Re-render | Changing .current does not trigger one |
| Read during render | Discouraged (except lazy init) |
| Write during render | Discouraged (except lazy init) |
| Safe timing | Effects, event handlers, cleanup |
| Use cases | Timers, previous values, flags, connections |
| Latest value | Ref updated in effect; callback reads ref |
| Lazy init | if (ref.current === null) ref.current = ... |
| Decision rule | Rendered? → state. Not rendered? → ref |
Key takeaways:
useRefreturns a mutable object that persists across renders. The object has a.currentproperty, and React creates it once and returns it on every render. Assigning to.currentdoes not trigger a re-render, so the value is invisible to the UI .- The distinction between a ref and state is rendering. If the rendered output depends on the value, it is state. If it does not, it can be a ref. Putting non-rendering values in state causes unnecessary re-renders and, in some cases, infinite loops .
- Do not read or write
.currentduring the render. The render may be called multiple times or discarded, and a ref mutated during the render holds the value from the last call. Read and write refs in effects and event handlers, which run after the commit . - Lazy initialization is the exception. The first render can initialize the ref if it is
null, because the check is idempotent and the value is stable thereafter. This is the pattern for expensive values that should be computed once . - The latest value pattern solves stale closures. A ref updated in an effect holds the latest value, and a stable callback reads the ref. The callback does not need to be recreated, and it always sees the current value. This is the standard pattern for intervals, subscriptions, and third-party integrations .
- The common patterns are timers, previous values, flags, and connections. Each is a value that persists across renders and does not affect the UI. Each is stored in a ref and read or written in effects and event handlers .
- Refs are an escape hatch. React does not track the value, so it cannot react to changes. If the value needs to affect the UI, a ref is the wrong choice, and the UI will not update. Use refs for implementation details, not for rendering data .
- The decision rule is simple. Ask whether the rendered output depends on the value. If yes, use state. If no, use a ref. This question resolves almost every case.
Remember: useRef is the hook for mutable values that need to survive renders without causing them. It is a box that React creates once and hands back on every render, and the value inside persists. It does not trigger re-renders, so the value is invisible to the UI. Use it for timer IDs, previous values, flags, connections, and instance variables — values that are part of the component’s implementation, not its output. Do not read or write it during the render, except for lazy initialization; do it in effects and event handlers. The latest value pattern and lazy initialization are the two deliberate exceptions. For everything else, the question is whether the rendered output depends on the value. If it does, use state. If it does not, use a ref.
Stop using slow, ad-bloated tool sites! 🤮
🔎 Search “KandZ Tools” on Google to use many professional utilities for free.
KandZ.me is the ultimate minimalist hub for:
✅ Finance (Mortgage, Interest, Inflation)
✅ Tech (Base64, JSON, Dev Suite, IP)
✅ Health (BMI, BMR, TDEE)
✅ Productivity (Timer, Workspace, QR)
⚡️ Fast & Private
🔒 No data leaves your device
💎 100% Free
🔗 Use it now: https://tools.kandz.me
🔖 Bookmark it—you’ll need it later!