useRef vs useState in React
Short answer
Both hold a value between renders. Changing state with useState schedules a re-render and the new value appears on the next one. Changing a ref with useRef.current mutates it immediately and re-renders nothing. Use state for anything the UI displays; use a ref for anything the UI does not.
| useState | useRef | |
|---|---|---|
| Triggers a re-render | Yes | No |
| Update is visible | On the next render | Immediately |
| How you update it | setState(next) | ref.current = next |
| Safe to mutate during render | No | No (but for a different reason) |
| Survives re-renders | Yes | Yes |
| Survives unmount | No | No |
| Can hold a DOM node | Awkwardly | Yes — its main use |
| Use for | Anything rendered | Timers, DOM nodes, previous values, flags |
The difference in one sentence
Both hooks give you a value that persists across renders. The difference is whether React cares when it changes. State is part of the render output contract: change it and React re-renders the component so the screen matches. A ref is a private mutable box that React deliberately ignores — writing to it changes nothing on screen until something else triggers a render.
1function StateCounter() {
2 const [count, setCount] = useState(0);
3 return <button onClick={() => setCount(count + 1)}>{count}</button>;
4 // → click: 1, 2, 3 — the label updates every time
5}
6
7function RefCounter() {
8 const count = useRef(0);
9 return <button onClick={() => { count.current++; }}>{count.current}</button>;
10 // → click: still 0 on screen forever.
11 // count.current really is 3 — React just never re-rendered to show it.
12}That second component is the shape of a real bug people hit. Nothing errors, nothing warns, the value genuinely updates — it simply never reaches the screen. The rule that avoids it is blunt and worth memorising: if a value appears in your JSX, it belongs in state.
useRef's first job: reaching a DOM node
The most common use of useRef has nothing to do with avoiding re-renders. Pass a ref to a JSX element's ref attribute and React puts the DOM node into `.current` after committing to the screen. That is how you focus an input, measure an element, scroll a container or hand a node to a non-React library.
1function SearchBox() {
2 const input = useRef(null);
3 const list = useRef(null);
4
5 useEffect(() => {
6 input.current.focus();
7 // → runs after the DOM exists; focusing during render would throw
8
9 const { height } = list.current.getBoundingClientRect();
10 console.log(height); // → 320
11
12 list.current.scrollTo({ top: 0, behavior: "smooth" });
13 }, []);
14
15 return (
16 <>
17 <input ref={input} />
18 <ul ref={list} />
19 </>
20 );
21}The timing is the part that catches people. `input.current` is null during the first render and only gets its value after React commits — which is precisely why the code above sits inside an effect. Reading a DOM ref in the component body will give you null every time, and the crash message points at your code rather than the timing.
The value that must not reset: timers and subscriptions
A local variable inside a component is recreated on every render, so it cannot hold anything that must survive. State survives, but storing an interval id in state would re-render the component every time you started a timer, for no visible benefit. A ref is exactly the right shape: it persists, and nothing re-renders when it changes.
1function Stopwatch() {
2 const [seconds, setSeconds] = useState(0); // rendered → state
3 const timerId = useRef(null); // not rendered → ref
4
5 const start = () => {
6 if (timerId.current) return; // already running
7 timerId.current = setInterval(() => {
8 setSeconds(s => s + 1); // → functional update, see below
9 }, 1000);
10 };
11
12 const stop = () => {
13 clearInterval(timerId.current);
14 timerId.current = null;
15 };
16
17 useEffect(() => stop, []); // → clear on unmount, or the timer leaks
18
19 return <><span>{seconds}</span><button onClick={start}>Go</button></>;
20}The same pattern applies to anything with a handle you need to release later: an AbortController for a fetch, a websocket, an IntersectionObserver, a third-party chart instance. All of them are values React should not re-render for, and all of them must outlive a render. That is a ref, every time.
The stale closure problem, and why a ref fixes it
This is the highest-value section on the page, because it is the follow-up that separates people who have used hooks from people who have read about them. Every render creates new closures over that render's variables. A callback registered once — inside an empty-dependency effect — keeps seeing the first render's values forever, no matter how much the state changes afterwards.
1function Broken() {
2 const [count, setCount] = useState(0);
3
4 useEffect(() => {
5 const id = setInterval(() => {
6 setCount(count + 1); // `count` is 0 in this closure, forever
7 }, 1000);
8 return () => clearInterval(id);
9 }, []); // ← empty deps: the effect never re-runs
10
11 return <p>{count}</p>;
12 // → 1, then 1, then 1, then 1… it never gets past 1
13}There are two correct fixes and knowing both is what makes this a strong answer. The functional update form sidesteps the closure entirely, because React hands you the current value instead of you reading it from scope. When the stale value is not state — a prop, a callback from above — a ref that you keep up to date is the general solution.
1// Fix 1 — functional update. Best when the stale value IS the state.
2useEffect(() => {
3 const id = setInterval(() => setCount(c => c + 1), 1000);
4 return () => clearInterval(id);
5}, []);
6// → 1, 2, 3, 4…
7
8// Fix 2 — a ref that always holds the latest value.
9// Works for props and callbacks too, which fix 1 cannot help with.
10function Poller({ onTick }) {
11 const latest = useRef(onTick);
12 useEffect(() => { latest.current = onTick; }); // every render, no deps
13
14 useEffect(() => {
15 const id = setInterval(() => latest.current(), 1000);
16 return () => clearInterval(id);
17 }, []);
18 // → always calls the newest onTick, and the interval is never recreated
19}This is 1 of 75+ questions in the React Interview Kit
Get every question with detailed answers, follow-ups and real code — plus coding challenges and a last-minute revision sheet. One-time payment, instant access.
⚡ Get the React Interview Kit → ₹399Keeping the previous value
A ref updated inside an effect lags one render behind, because effects run after render. That accident is genuinely useful: it gives you the previous value of any prop or state, which is how you animate a change, log a transition or fire something only when a value actually moves.
1function usePrevious(value) {
2 const ref = useRef(undefined);
3 useEffect(() => { ref.current = value; }); // after every render
4 return ref.current; // → last render's value
5}
6
7function Price({ amount }) {
8 const previous = usePrevious(amount);
9 const direction = previous === undefined ? null : amount > previous ? "up" : "down";
10
11 return <span className={direction}>₹{amount}</span>;
12 // amount 499 → 599 gives previous 499 and direction "up"
13}Note that `previous` is undefined on the first render, and that is correct — there was no previous value. Handling that case explicitly rather than defaulting it is usually what you want, because "no previous value" and "the previous value was zero" are different situations.
When a ref would be wrong
The temptation, once you understand that refs avoid re-renders, is to use them to avoid re-renders. That is backwards. A re-render is React doing its job, and skipping one by hiding data in a ref means the screen no longer reflects the data. The bug it produces is the hardest kind to find, because everything looks correct in the code and only the display is wrong.
1function Form() {
2 const email = useRef("");
3
4 return (
5 <>
6 <input onChange={e => { email.current = e.target.value; }} />
7 <button disabled={!email.current.includes("@")}>Submit</button>
8 {/* → the button never enables: nothing re-renders to re-evaluate it */}
9 </>
10 );
11}
12
13// Correct — the disabled state is rendered, so it is state:
14const [email, setEmail] = useState("");
15<button disabled={!email.includes("@")}>Submit</button>The uncontrolled-input pattern is the one legitimate exception, and it is legitimate precisely because nothing else on screen depends on the value while you type. You read `inputRef.current.value` once on submit. The moment you want live validation, a character counter or a disabled button, the value is rendered and must be state.
Refs on components, and forwardRef
Putting a ref on a DOM element works out of the box. Putting one on your own component does not: function components have no instance for React to hand you, so the ref arrives as nothing useful. Before React 19 the fix was `forwardRef`; from React 19 a component can simply accept `ref` as an ordinary prop.
1// React 19 and later — ref is just a prop
2function Input({ ref, ...props }) {
3 return <input ref={ref} {...props} />;
4}
5
6// React 18 and earlier
7const Input = forwardRef(function Input(props, ref) {
8 return <input ref={ref} {...props} />;
9});
10
11// Either way:
12const box = useRef(null);
13<Input ref={box} />;
14box.current.focus(); // → focuses the real <input>
15
16// Without the forwarding, in React 18:
17// → Warning: Function components cannot be given refs. Did you mean
18// to use React.forwardRef()?`useImperativeHandle` is the companion: it lets you decide what `.current` exposes instead of handing out the raw DOM node. Use it sparingly — exposing a `focus()` and a `scrollIntoView()` is reasonable, exposing a way to set internal state is a sign the data should have lived in the parent.
Which one, in practice
| You are storing… | Use | Because |
|---|---|---|
| Anything shown in the JSX | useState | The screen must update |
| A DOM node | useRef | That is what refs are for |
| setInterval / setTimeout id | useRef | Persist without re-rendering |
| The previous value of a prop | useRef in an effect | Lags one render behind, by design |
| "Has this already run?" flag | useRef | Not rendered, must survive renders |
| An AbortController or socket | useRef | Needs cleanup, never rendered |
| Uncontrolled input value | useRef | Read once on submit |
| Value derived from props/state | Neither | Just compute it during render |
Does useRef work in class components?
No — hooks only run in function components. The class equivalents are createRef, which makes a fresh ref each render and is used with instance fields, and plain instance properties like this.timerId for values that must persist. useRef combines both jobs in one hook.
What is the difference between useRef and useMemo for caching?
useMemo recomputes when its dependencies change and React is allowed to discard its cache to free memory, so it is a performance hint rather than a guarantee. A ref holds exactly what you put in it until you change it. Use useMemo for derived values, a ref for something whose identity must be stable.
Why is my ref null in StrictMode during development?
React 18's StrictMode mounts, unmounts and remounts components in development to surface missing cleanup. Refs are detached on unmount, so any effect that assumed a ref stayed attached across that cycle will see null. It is exposing a real cleanup bug, not creating one.
Can I put an object in a ref and mutate its fields?
Yes, and it is a common pattern for grouping related non-rendered values — ref.current = { startX, startY } during a drag. Nothing re-renders, so you can mutate freely. Just remember that no consumer will be notified, so nothing derived from those fields can be displayed without separate state.
Does a ref survive when the component unmounts?
No. Both state and refs are stored on the component instance in React's internal tree and are discarded when it unmounts. If a value must outlive the component, it belongs in a module-level variable, a context above the component, or a store.
Frequently asked questions
- What is the difference between useRef and useState in React?
- Both persist a value across renders. Updating state with the setter tells React to re-render so the screen reflects the change; updating ref.current mutates the value immediately and re-renders nothing. Use state for anything the UI displays and a ref for anything it does not.
- When should I use useRef instead of useState?
- When the value is not rendered: a DOM node, an interval or timeout id, an AbortController, a websocket, a "has this already run" flag, or the value of an uncontrolled input read only on submit. All of these must survive re-renders without causing one.
- Why doesn't my component update when I change a ref?
- Because that is exactly what refs are for. Assigning to ref.current does not schedule a render, so the screen keeps showing the old value even though the ref really did change. If the value needs to be visible, it belongs in state.
- Why is ref.current null on the first render?
- React attaches the DOM node after the render is committed to the screen, so during the first render there is no node yet. Read the ref in useEffect or an event handler, guard with optional chaining, or use a callback ref if you need to act the instant the node appears.
- How do I get the previous value of a prop or state?
- Store it in a ref inside an effect with no dependency array. Effects run after render, so the ref holds the previous render's value while the component body sees the current one — that is the entire usePrevious hook, in four lines.
- What is a stale closure in React?
- A callback registered once — usually in an effect with an empty dependency array — captures that render's variables and keeps seeing them forever. The interval that counts to 1 and stops is the classic symptom. Fix it with a functional state update, or with a ref that you refresh on every render.
- Can I use useRef to avoid re-renders for performance?
- Only for values that are genuinely not displayed. Hiding rendered data in a ref does not optimise anything — it just stops the screen updating, producing a bug that looks correct in the code. Real render optimisation comes from memoisation and from splitting components.
- Is it safe to mutate ref.current during render?
- No. Concurrent rendering can start, abandon and restart a render, so a mutation during render may run twice or be discarded. Write to refs in effects and event handlers. The one documented exception is lazy initialisation: if (ref.current === null) ref.current = createExpensiveThing().
- How do I pass a ref to my own component?
- From React 19, accept ref as a normal prop and spread it onto the element you want to expose. Before that, wrap the component in forwardRef. Without either, React warns that function components cannot be given refs and the ref stays null.
- What is the difference between useRef and createRef?
- createRef returns a brand-new ref object every time it runs, so calling it in a function component gives you a fresh empty ref on every render. useRef returns the same object for the component's whole lifetime. createRef belongs in class components; useRef is the hooks equivalent.
- Does changing a ref inside useEffect cause an infinite loop?
- No, and that is the point. Writing to a ref does not trigger a render, so an effect with no dependency array that updates a ref every render is safe and is the standard latest-ref pattern. Doing the same with setState would loop forever.
- Should form inputs use useState or useRef?
- Use state for controlled inputs, which you need for live validation, a character counter, a conditionally disabled button, or anything else that changes as the user types. Use a ref for an uncontrolled input whose value you read once on submit — it is less code and avoids a render per keystroke.
This is 1 of 75+ questions in the React Interview Kit
Get every question with detailed answers, follow-ups and real code — plus coding challenges and a last-minute revision sheet. One-time payment, instant access.
⚡ Get the React Interview Kit → ₹399