React useRef is key to component memory. Learn why variables reset on re-render and how to use refs to persist data without triggering unnecessary updates.
I remember sitting with a junior dev a few months ago, watching them pull their hair out because a simple let count = 0 variable inside a component kept resetting to zero every time they clicked a button. It’s a classic rite of passage. If you’ve ever wondered why your local variables seem to "forget" their values the moment your UI updates, you’re not alone.
To understand this, you have to stop thinking of a React component as a persistent object that sits in memory. Instead, think of it as a function that gets called over and over again.
Every time a component re-renders, React executes your component function from top to bottom. If you declare a variable like let count = 0 inside that function, you’re essentially creating a brand-new variable every single time the function runs. It’s not "remembering" the old value; it’s being re-initialized.
Think of it like this:
JAVASCRIPTfunction Counter() { let count = 0; // This line runs every single render! return ( <button onClick={() => count++}> Count: {count} </button> ); }
When you click the button, count becomes 1, but the UI doesn't update because React doesn't know the state changed. Even if you forced a re-render, the next time the function executes, that let count = 0 line will happily reset your count back to zero. Understanding this requires a solid grasp of React reconciliation and component state persistence: A mental model, where React Fiber determines what to keep and what to throw away.
We have two main ways to "save" data across renders: useState and useRef.
If the value you're tracking needs to be reflected in the UI, you use useState. When you call the setter function, React schedules a re-render to update the DOM. But sometimes, you need to store data—like an interval ID, a previous prop value, or a reference to a DOM node—without triggering a re-render. This is where React useRef becomes your best friend.
A ref is a plain JavaScript object with a single property: .current. Because it’s an object created outside your component function's scope, React keeps it alive for the entire lifecycle of the component.
When you use useRef, you aren't just creating a variable; you’re creating a stable container that React promises to keep around. Unlike state, updating ref.current does not trigger a re-render.
Here’s how we’d fix that counter logic if we wanted to track clicks without triggering a UI update:
JAVASCRIPTimport { useRef } from CE9178">'react'; function ClickTracker() { const clickCount = useRef(0); const handleClick = () => { clickCount.current += 1; console.log(CE9178">`Total clicks: ${clickCount.current}`); }; return <button onClick={handleClick}>Log clicks to console</button>; }
In this example, even if the component re-renders for other reasons, clickCount.current stays exactly where you left it. It's essentially a "private field" that survives the function’s execution cycle. It’s an essential tool for React useRef Hook: Mastering DOM Access and Mutable State when you need to handle side effects or store values that don't belong in the render flow.
Choosing between useState and useRef is a fundamental part of React state management and the unidirectional data flow. I usually follow this simple heuristic:
useState.useRef.I’ve seen developers try to force useRef to drive the UI. It’s a trap. If you find yourself needing to manually call a forceUpdate or wondering why your screen isn't showing the latest value, you’ve likely picked the wrong tool. Remember, refs are for "behind the scenes" data, not for the data that drives your user interface.
Be careful about reading or writing to ref.current during the rendering phase. React expects your component function to be pure. If you start mutating ref values while the component is calculating its output, you’ll run into inconsistent behavior, especially as React moves toward concurrent rendering.
Always perform your ref updates inside useEffect or event handlers. If you’re unsure, treat the ref as an escape hatch—something you use when the standard reactive flow of props and state isn't enough. I still catch myself trying to over-engineer with refs when a simple state lift would have been cleaner. We’re all learning, and sometimes the best code is the simplest code that works.
Q: Can I use useRef to store data that needs to be displayed in the JSX?
A: Technically yes, but you shouldn't. Because changing a ref doesn't trigger a re-render, the UI won't update to reflect the new value. Use useState for anything that needs to show up on the screen.
Q: Why not just use a global variable?
A: Global variables are shared across all instances of a component. If you have two Counter components on the page, they’d both fight over the same global variable. useRef is local to a single component instance.
Q: Is useRef only for DOM nodes?
A: Definitely not. While it's the standard way to access DOM nodes, it's just as useful for storing timers, previous props, or any mutable value that doesn't need to trigger a UI update.
React derived state is the key to faster components. Learn how to stop abusing useEffect for data transformations and simplify your React performance optimization.