React gives you two ways to keep data alive inside a component: useState and useRef. At a glance they look similar. Both return something you can read, both survive re-renders, and both let you remember a value from one click to the next. Pick the wrong one, though, and you end up with either a screen that refuses to update or an endless parade of unnecessary renders. The choice is not about syntax. It is about whether React needs to know.

What Each Hook Actually Does

useState is React’s official line of communication with a component. When you call it, you get back a value and a setter function. React tracks that value as part of the component’s identity. When the setter fires, React says, “Something changed,” and schedules a fresh render so the screen can catch up.

useRef, on the other hand, is nothing more than a plain JavaScript object with a current property. React promises to hand you the exact same object reference on every single render. It does not watch what is inside. Mutating someRef.current happens silently. React will not react.

That silence is the whole point. Refs are an escape hatch, not a state replacement.

The Rendering Divide

If you change state, the component re-renders. This is the behavior most beginners expect, and it is exactly what you want when the new data must appear on screen. A counter, a form field, a fetched list of users—if the user sees it, it probably belongs in state. React’s entire data flow is built around the idea that state changes notify the renderer to sync the DOM.

If you change a ref, nothing happens visually. The variable updates immediately and synchronously, but the component does not re-render. This makes refs ideal for values that support the component’s inner workings without being part of the visual output. Think timer IDs, previous prop snapshots, or direct DOM handles. The UI does not care about the interval ID that drives a carousel autoplay; it only cares about which slide is visible. The interval ID belongs in a ref.

When State Is the Right Tool

Reach for useState anytime a value is part of the surface of your UI.

Input fields are the obvious example. If a user types an email address and you need to validate it and show an error message below the box, that email string needs state. The validation logic and the error banner both depend on the latest value, and React only knows to update the banner because state triggered a re-render.

Counts and toggles are another staple. A button that increments a score, a modal open/close flag, a tab index—these all flow through state because the render output changes along with the value. Even derived values, such as a filtered list that depends on a search string, usually start with state because the source value is visible to the user.

There is also a timing nuance worth understanding. State updates are asynchronous and batched. If you call setCount(count + 1) three times inside one event handler, React does not render three times. It batches them into a single update. The variable count inside your running function also stays stale until the next render. This batching is a feature. It keeps applications fast. But it means you cannot expect the state variable to reflect the new value on the very next line.

When Refs Save the Day

Use useRef for plumbing, not