ForgeFrontend — Prepare, Practice, Crack
Secure checkout
Lifetime access
Instant PDF download
Free updates forever

Prepare · Practice · Crack

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.

useStateuseRef
Triggers a re-renderYesNo
Update is visibleOn the next renderImmediately
How you update itsetState(next)ref.current = next
Safe to mutate during renderNoNo (but for a different reason)
Survives re-rendersYesYes
Survives unmountNoNo
Can hold a DOM nodeAwkwardlyYes — its main use
Use forAnything renderedTimers, DOM nodes, previous values, flags
Same job — remembering a value — with opposite consequences.

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.

Same counter, two behavioursjsx
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.

The four things people actually use a DOM ref forjsx
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.

A stopwatch that stores its id in a refjsx
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.

The bug: an interval frozen at the first renderjsx
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.

Both fixesjsx
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 → ₹399

Keeping 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.

usePrevious, the whole implementationjsx
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.

A ref used as state, and what it costsjsx
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.

Passing a ref through your own componentjsx
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…UseBecause
Anything shown in the JSXuseStateThe screen must update
A DOM nodeuseRefThat is what refs are for
setInterval / setTimeout iduseRefPersist without re-rendering
The previous value of a propuseRef in an effectLags one render behind, by design
"Has this already run?" flaguseRefNot rendered, must survive renders
An AbortController or socketuseRefNeeds cleanup, never rendered
Uncontrolled input valueuseRefRead once on submit
Value derived from props/stateNeitherJust compute it during render
A decision table you can answer from in an interview.

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
Written by Arun Karthikeyan · Last updated

Full kit

React Interview Kit · ₹399

Get it →