React useRef and DOM Manipulation
useRef for DOM access, storing mutable values, forwardRef, and ref callbacks..
20+ years shipping production JavaScript and front-end systems at scale. Everything here is grounded in real deployments.
- ✓React 16.8+ (Hooks), JavaScript ES6, Basic understanding of React components and state management, Node.js 14+ for running examples, Code editor (VS Code recommended)
React useRef and DOM Manipulation: A core React concept for building modern user interfaces. It helps you structure your components efficiently and handle data flow predictably.
React is a JavaScript library for building user interfaces. This article covers useref dom — a key concept for building modern web applications.
| Chrome | Firefox | Safari | Edge |
|---|---|---|---|
| ✓ | ✓ | ✓ | ✓ |
A comprehensive guide to react useref and dom manipulation with production examples and best practices.
What is useRef? The Mutable Ref Object
useRef is a React hook that returns a mutable ref object whose .current property is initialized to the passed argument. Unlike state, changing .current does not cause a re-render. This makes useRef ideal for storing values that persist across renders without triggering updates. The ref object itself is stable across the component's lifecycle — the same object reference is returned on every render. This stability is key for DOM access, as it allows you to attach a ref to a JSX element and reliably access the underlying DOM node after mounting. useRef is not just for DOM; it can hold any mutable value, like timers, previous state, or instance variables. However, its most common use case is direct DOM manipulation, which we'll explore in depth.
Accessing DOM Nodes with the ref Attribute
React provides a built-in ref attribute on host elements (like <div>, <input>, etc.) that accepts a ref object. When the element mounts, React sets ref.current to the corresponding DOM node. When the element unmounts, it sets ref.current back to null. This is the primary way to get a handle on a DOM element for imperative operations like focusing, measuring, or animating. You can also use callback refs for more control, but the object ref is simpler and sufficient for most cases. Important: ref only works on host elements, not on custom components unless they forward refs using forwardRef. We'll cover that later.
Imperative DOM Manipulation: When and How
React is declarative — you describe what the UI should look like, and React handles the DOM updates. However, some operations are inherently imperative: focusing an input, scrolling to a position, playing a video, or integrating with a third-party library that expects a DOM node. useRef gives you an escape hatch for these cases. The rule of thumb: if you can do it declaratively (e.g., using state to control visibility), do it declaratively. Use imperative refs only when there's no declarative equivalent. Common patterns include auto-focus on mount, managing media playback, and triggering animations. Always clean up any side effects (like event listeners) in the useEffect cleanup function to avoid memory leaks.
Forwarding Refs to Custom Components
By default, refs are not passed to custom components — they only work on host elements. To expose a DOM node from a child component, you must use React.forwardRef. This higher-order component receives props and ref as arguments, allowing you to attach the ref to a host element inside the child. This is essential for building reusable component libraries where consumers need direct access to the underlying DOM (e.g., for focus management or tooltip positioning). Without forwardRef, you'd have to use callback refs or prop drilling, which is messy. forwardRef is the standard pattern.
Storing Mutable Values Without Re-renders
Beyond DOM access, useRef can store any mutable value that you want to persist across renders without causing re-renders. Common use cases: holding interval IDs, previous state values, or flags that track whether a component is mounted. Because changing .current doesn't trigger a re-render, it's more performant than useState for values that don't affect the UI. However, be careful: mutating refs doesn't notify React, so the UI won't update automatically. If you need the UI to reflect the value, use state instead. A classic pattern is using a ref to store the previous value of a state variable for comparison in useEffect.
useRef vs useState: When to Use Which
A common confusion is when to use useRef vs useState. The key difference: useState triggers a re-render when the value changes; useRef does not. If the value affects the UI, use state. If it's a value that only needs to be read or written in effects/event handlers (like timers, DOM nodes, or previous values), use ref. Using state for non-UI values causes unnecessary re-renders and performance issues. Conversely, using ref for UI values means the UI won't update. A good rule: if you need the component to re-render when the value changes, use state; otherwise, use ref.
Callback Refs: Fine-Grained Control
React also supports callback refs — a function instead of a ref object. React calls the function with the DOM node when the element mounts, and with null when it unmounts. Callback refs give you more control, especially when you need to run logic on every ref change (e.g., when the ref is attached to a dynamic list of items). They also allow you to clean up previous refs. However, they can cause more re-renders if not memoized. Use useCallback to stabilize the callback ref and avoid unnecessary invocations. Callback refs are useful for measuring dynamic content or integrating with libraries that need a node reference.
Common Pitfalls and Anti-Patterns
Even experienced developers make mistakes with useRef. Common pitfalls: (1) Reading ref.current during render — it may be null or stale. Always access in effects or handlers. (2) Using refs to store state that should trigger re-renders — leads to stale UI. (3) Forgetting to clean up refs on unmount, causing memory leaks (e.g., not clearing intervals). (4) Overusing refs for DOM manipulation when a declarative approach exists — makes code harder to maintain. (5) Mutating refs inside render — can cause inconsistencies. Stick to the patterns: refs for DOM access and mutable values, state for UI. Test your ref logic with strict mode to catch double-mount issues.
createRef vs useRef: Class Component Legacy
In React class components, refs are created using React.createRef(). This method returns a ref object with a current property, similar to useRef in functional components. However, createRef is only available in class components and is not a hook. The key difference is that createRef creates a new ref object on every render, while useRef persists the same ref object across renders. In functional components, useRef is the standard approach because it leverages hooks and avoids unnecessary object creation. createRef exists primarily for legacy class component support. If you're migrating from class to functional components, replace React.createRef() with useRef(null). Note that createRef cannot be used inside functional components; doing so will cause a warning and break ref persistence. For new code, always prefer useRef.
React.createRef() with useRef(null) to maintain ref behavior and avoid unnecessary re-creation.useRef in functional components; createRef is a legacy API for class components that creates a new ref each render.flushSync: Coordinating Ref Access with DOM Updates
React batches state updates and defers DOM commits for performance. This means after calling setState, the DOM may not be updated immediately, so reading a ref's current property might return stale data. flushSync from react-dom forces React to flush any pending updates and update the DOM synchronously before proceeding. This is useful when you need to measure DOM properties (e.g., scroll position, element dimensions) immediately after a state change. However, use flushSync sparingly as it can degrade performance by bypassing batching. Example: after adding an item to a list, you want to scroll to the new item. Without flushSync, scrollIntoView might run before the new element is rendered. Wrap the state update and ref access in flushSync to ensure the DOM is up-to-date.
useEffect for side effects that depend on DOM state, as it runs after the DOM is committed. Reserve flushSync for imperative scenarios like scroll management or measurement that cannot be deferred.flushSync to synchronously flush DOM updates when you need to read or manipulate the DOM immediately after a state change.ResizeObserver + useRef for Responsive Layout
ResizeObserver is a browser API that watches for changes to an element's size. Combined with useRef, you can create responsive components that react to container size changes without relying on window resize events. This is ideal for charts, grids, or any layout that needs to adapt to its container. Use useRef to hold a reference to the observed element and store the observer instance. In a useEffect, create the observer, attach it to the ref's current node, and update state with new dimensions. Clean up by disconnecting the observer on unmount. Example: a responsive card that changes its layout based on width. Note that ResizeObserver is supported in modern browsers; for older ones, consider a polyfill.
@react-hook/resize-observer for a more robust hook-based API, and always disconnect observers in cleanup to prevent memory leaks.ResizeObserver with useRef to create components that dynamically respond to container size changes, enabling responsive layouts without window resize events.| File | Command / Code | Purpose |
|---|---|---|
| BasicRefExample.jsx | export default function InputFocus() { | What is useRef? The Mutable Ref Object |
| MeasureElement.jsx | export default function MeasureExample() { | Accessing DOM Nodes with the ref Attribute |
| VideoPlayer.jsx | export default function VideoPlayer({ src }) { | Imperative DOM Manipulation |
| FancyInput.jsx | const FancyInput = forwardRef((props, ref) => { | Forwarding Refs to Custom Components |
| PreviousValue.jsx | export default function Counter() { | Storing Mutable Values Without Re-renders |
| TimerExample.jsx | export default function Timer() { | useRef vs useState |
| DynamicListRefs.jsx | export default function List() { | Callback Refs |
| PitfallExample.jsx | function BadComponent() { | Common Pitfalls and Anti-Patterns |
| ClassComponent.js | class MyComponent extends Component { | createRef vs useRef |
| FlushSyncExample.js | function ScrollList() { | flushSync |
| ResizeObserverExample.js | function ResponsiveCard() { | ResizeObserver + useRef for Responsive Layout |
Key takeaways
Interview Questions on This Topic
What is the Virtual DOM and how does React use it?
Frequently Asked Questions
20+ years shipping production JavaScript and front-end systems at scale. Everything here is grounded in real deployments.
That's React. Mark it forged?
4 min read · try the examples if you haven't