React is declarative ā you describe what the UI should look like and it handles the DOM for you ā but sometimes you need an escape hatch to imperatively command the browser directly, like focusing an input or integrating a third-party library. That's what useRef provides.
1Direct DOM Access
React is a declarative framework: you describe what the UI should look like based on state, and React figures out the DOM updates. Occasionally, though, you need an 'escape hatch' to imperatively command the browser directly ā focusing an input, playing a video, or measuring an element's size.
useRef provides exactly that escape hatch, giving you direct access to the actual DOM node underneath a React element when declarative state alone isn't enough.
// useRef: The Reference PortalThe Escape Hatch
Bridging React State & Raw DOM
2Creating a Ref
You create a ref by calling useRef(initialValue), usually with null as the initial value if you intend to attach it to a DOM node later. Unlike useState, useRef doesn't return an array with a setter function ā it returns a single plain JavaScript object with exactly one property, .current.
That object reference itself stays identical across every re-render of the component, which is what makes it a stable place to stash a value that needs to persist without triggering updates.
import { useRef } from 'react';
const inputRef = useRef(null);current: null
};
3The .current Property
The .current property is the actual 'box' holding your value. Whatever you pass into useRef(initialValue) becomes the starting value of .current, and you can freely read or write myRef.current = newValue at any time, much like an instance variable would work in a class.
Unlike state, writing to .current is a plain, synchronous mutation ā there's no setter function, no batching, and critically, no re-render triggered by the assignment itself.
return <input ref={inputRef} />;4Attaching to the DOM
To link a ref to a real HTML element, pass the ref object to that JSX element's ref attribute: <input ref={inputRef} />. After React renders the component and the browser creates the actual DOM node, React automatically mutates inputRef.current, replacing null with a reference to that real node.
From that point on, inputRef.current is the genuine DOM element ā anything you could do with document.querySelector(...).focus() or similar, you can now do directly through inputRef.current.
const handleClick = () => {
inputRef.current.focus();
};React connects the wire
5Why not getElementById?
It's tempting to reach for document.getElementById('my-input') instead of a ref, but React components are meant to be reusable ā if the same component renders five times on one page, IDs must be globally unique, and getElementById breaks the moment more than one instance exists.
useRef scopes the DOM reference specifically to that one component instance, so every rendered copy safely gets its own independent reference with zero risk of colliding with another instance's node.
const timerRef = useRef();
useEffect(() => {
timerRef.current = setInterval(() => {}, 1000);
}, []);Avoid Global Selectors
React handles the mapping.
6Programmatic Focus
The most common use case for a DOM ref is managing focus ā if a user clicks a 'Reply' button, you likely want their cursor to jump straight into the text box. You achieve this by calling the native .focus() method directly on ref.current inside an event handler.
Because this bypasses React's declarative rendering entirely and calls a native browser API directly, it's a genuinely imperative action ā exactly the kind of thing useRef exists to enable.
// Var: Reset every render
// Ref: Persists forever7Refs vs State
Here's the critical difference between useState and useRef: updating a state variable causes the component to re-render, while updating a ref's .current value does not. Writing myRef.current = 5 is a silent background mutation that React is completely unaware of.
This makes the two hooks suited for entirely different jobs ā state for anything the user should visibly see reflected on screen, refs for anything that needs to persist across renders without ever causing one.
useEffect(() => {
new MyLibrary(myRef.current);
}, []);State
Visible Updates
Ref
Silent Storage
8Storing Mutable Data
Because changing a ref never triggers a render, it's the ideal place to store background data the user doesn't need to see reflected on screen ā the classic example is holding an interval or timeout ID. You need that ID later to clear the timer, but re-rendering the whole component just to remember it would be wasteful.
const timerRef = useRef(); timerRef.current = setInterval(...) stores the ID silently, available whenever you need to call clearInterval(timerRef.current).
/* Ref Lab: DOM Interaction & Persistent Timers Rendered */9The Previous Value Pattern
A ref combined with an effect gives a clean way to remember the *previous* render's value of a prop or state, so it can be compared against the current one. The effect updates the ref to the latest value after each render commits ā so during render itself, the ref still holds whatever value was current on the render before.
const prevPrice = useRef(price);
useEffect(() => { prevPrice.current = price; });
const isUp = price > prevPrice.current;10 ā 15
ā Compared against the OLD ref value
10Why not a normal variable?
It's fair to ask why not just declare a plain let myTimer = null inside the component instead of reaching for useRef. The answer is that components are functions ā every time state changes, that function runs again from top to bottom, and a plain let variable would be destroyed and recreated as null on every single render.
useRef guarantees its .current value survives across renders untouched, which is exactly the persistence a normal local variable can never provide inside a function component.
/* Next: Memoization (useMemo) */Refs Survive Re-Renders
11Step-by-Step Breakdown
Direct DOM Access. Welcome to useRef. React is a declarative framework: you describe what the UI should look like based on state, and React handles the DOM updates automatically. However, sometimes you need an 'Escape Hatch'. You need to imperatively command the browser to take a specific action, like focusing an input, playing an HTML5 video, or measuring an element's size. useRef gives you direct access to the actual DOM nodes.
Creating a Ref. You create a ref using the useRef() hook. It takes an initial value (often null if you intend to attach it to a DOM node later). Unlike useState, useRef doesn't return an array with a setter function. Instead, it returns a plain JavaScript object with exactly one property: .current.
The .current Property. The .current property is the 'box' where your value is stored. Whatever you pass into useRef(initialValue) becomes the starting value of .current. You can freely read from or write to myRef.current = newValue at any time. It acts like an instance variable in a class.
Which property on the object returned by useRef() holds the actual mutable value or DOM element?
- āvalue
- ācurrent
Attaching to the DOM. To link your ref to a real HTML element on the page, you pass the ref object to the ref attribute of that JSX element. After React renders the component and creates the actual HTML nodes in the browser, it automatically mutates inputRef.current, replacing null with the actual DOM node.
Why not getElementById?. You might wonder: why not just use document.getElementById('my-input')? In React, components are designed to be reusable. If you render the same component 5 times, getElementById will fail because IDs must be globally unique. useRef scopes the DOM reference specifically to THAT instance of the component. It is safe and encapsulated.
Programmatic Focus. The most common use case for a DOM ref is managing focus. If a user clicks a 'Reply' button, you want their cursor to immediately jump into the text box. You achieve this by calling the native HTML .focus() method directly on the current element in an event handler.
Which native DOM method do you call on ref.current to programmatically move the user's cursor into an input field?
- āfocus
- āselect
Refs vs State. Here is the critical difference between useState and useRef: Updating a state variable causes the component to re-render. Updating a ref variable does NOT cause a re-render. Changing myRef.current = 5 is a silent background mutation. React doesn't care, and the UI won't update automatically.
Storing Mutable Data. Because changing a ref doesn't trigger a render, it is the perfect place to store background data that the user doesn't need to see on the screen. The most common example is storing an Interval ID or Timer ID. You need the ID to clear the timer later, but you certainly don't want to re-render your app just to save a timer ID!
True or False: Updating a ref's .current property automatically triggers the component to re-render and update the UI.
- āTrue
- āFalse
The Previous Value Pattern. A ref combined with an effect gives you a clean way to remember the PREVIOUS render's value of a prop or state, so you can compare it against the current one. The effect updates the ref to the latest value AFTER each render commits ā so during render itself, the ref still holds whatever value was current on the render before.
Why not a normal variable?. If a ref is just a box that doesn't trigger renders, why not just declare a normal variable like let myTimer = null inside your component? Because components are functions! Every time state changes, the function runs again from top to bottom. A let variable would be destroyed and recreated every single render. useRef guarantees the value survives across renders.
Third-Party Integration. Refs are the crucial bridge for integrating third-party vanilla JavaScript libraries like D3.js, Chart.js, or Google Maps. These libraries don't understand React components; they need a raw <div> element to inject their canvas into. You pass a ref down to a div, and then pass myRef.current to the library initialization function.
Forwarding Refs. By default, you can only pass a ref prop to standard HTML elements (div, input). If you try to pass a ref to a custom component like <MyInput ref={myRef} />, React will throw an error. To make custom components accept a ref, you must wrap them in the forwardRef function. This is an advanced pattern we'll cover in depth later.
Mastery Achieved. Ref mastery achieved! You've learned how to bridge the gap between declarative React and the imperative browser DOM. You can programmatically manage focus, integrate with legacy libraries, and store mutable background data that survives re-renders perfectly. You are now a master of React's Escape Hatch.
Level Up š
Advanced cheat sheets, SEO tricks, and interview prep for this topic.
Browser Support
Fully supported.
Fully supported.
Fully supported.
Fully supported.
Accessibility (A11y)
1Moving Focus Programmatically Should Match User Expectations
Calling `ref.current.focus()` is powerful, but stealing focus unexpectedly (e.g., on page load with no user action) disorients screen reader and keyboard users ā reserve programmatic focus for direct responses to a user's own action, like opening a modal.
2Refs Bypassing React Don't Bypass Accessibility Requirements
A DOM node accessed imperatively via a ref still needs the same labels, roles, and ARIA attributes any other element would ā `useRef` changes how you interact with the node, not what markup that node needs to be accessible.
SEO Implications
- 1
Ref-Driven DOM Mutations Happen After the Initial Render, Invisible to Non-JS Crawlers
Content or attributes set imperatively via a ref (e.g., in a `useEffect`) only exist after JavaScript runs ā a crawler evaluating pre-hydration HTML won't see them, so critical content should never depend on ref-based imperative mutation to appear.
- 2
Refs Have No Direct SEO Weight of Their Own
A ref is purely a JavaScript-side handle to a DOM node or a persisted value ā it carries no semantic or content meaning by itself; only whatever actual HTML the node represents (set declaratively) matters to a crawler.
Best Practices
Never Use a Ref to Read or Set Values React Should Own Declaratively
If a value should be reflected in the rendered UI, it belongs in state, not a ref ā refs exist specifically for values or DOM access that fall outside React's normal render cycle, like a timer ID or a third-party library instance.
Clean Up Anything Stored in a Ref That Needs Teardown
A timer ID or subscription object stored in `.current` still needs to be cleared or unsubscribed in a `useEffect` cleanup function ā storing it in a ref avoids unnecessary re-renders, but doesn't automatically manage its lifecycle for you.
Frequent Bugs
A component reads `ref.current` immediately after render and gets `null` instead of the expected DOM node.
Refs are only attached to the real DOM node after React commits the render to the browser ā reading `ref.current` synchronously during the render itself (rather than in `useEffect` or an event handler, which run after commit) will see the stale or initial value.
Changing a ref's `.current` value doesn't update anything visible on screen.
This is expected behavior, not a bug ā mutating a ref never triggers a re-render. If the value needs to be reflected in the UI, it needs to be state (`useState`/`useReducer`), not a ref.
Real-World Examples
Auto-Focusing a Modal's First Input
When a modal dialog opens, a ref attached to its first input is used to programmatically focus it, directly responding to the user's own action of opening the modal rather than stealing focus unexpectedly.
const firstInputRef = useRef(null);
useEffect(() => {
if (isOpen) firstInputRef.current?.focus();
}, [isOpen]);