Introduction
useRef is a versatile hook in React that allows handling DOM references and storing data across component re-renders without triggering a re-render itself.
Unlike state variables managed by useState
or useReducer
, changes to a ref don't cause the component to update and re-render.
This makes useRef
perfect for certain tasks, such as accessing DOM elements directly, storing previous state, managing timers, keeping track of mutable data and accessing current value in the non React callbacks.
Understanding useRef React Hook
useRef
hook can be declared in the following way:
jsxconst ref = useRef(initialValue);
This hook returns a mutable ref object that will persist for the full lifetime of the component.
This object has a property called .current
that can be used to access or set a value of the ref object.
useRef
like the useState
hook has a single initialValue
parameter that sets an initial value to the .current
property.
When to USe useRef React Hook
Accessing DOM Elements
One of the most common uses of useRef
is to directly interact with a DOM element.
This approach is handy for focusing an input element or accessing its data.
jsxexport const InputWithFocusButton = () => { const inputRef = useRef(null); const onButtonClick = () => { inputRef.current.focus(); }; return ( <> <input ref={inputRef} type="text" /> <button onClick={onButtonClick}>Focus the input</button> </> ); }
In this component when you click "Focus the input" button - a text input is manually receiving a focus. This is possible by using a reference to the input HTML element.
First, an empty ref is defined const inputRef = useRef(null);
and then assigned with an HTML element:
jsx<input ref={inputRef} type="text" />
After that you can use inputRef.current
to access the input properties and functions. Finally, inputRef.current.focus();
sets the focus to the input element.
Storing Previous State
useRef
hook can be used to keep track of previous states or props without causing re-renders:
jsxexport const CoffeeCounter = () => { const [count, setCount] = useState(0); const prevCountRef = useRef(); useEffect(() => { prevCountRef.current = count; }, [count]); return ( <div> <h1>Now: {count}, before: {prevCountRef.current}</h1> <button onClick={() => setCount(count + 1)}> + 1 coffee </button> <button onClick={() => setCount(count - 1)}> - 1 coffee </button> </div> ); };
In this example a 2 coffee counters are displayed on the screen: the most recent value and a previous one.
When the count
state is updated - the prevCountRef
saves its value.
When count
value is updated - it causes a re-render and its value is immediately displayed on the screen.
However, as prevCountRef
doesn't cause a re-render - its previous value is displayed.
Holding Mutable Data
You can use useRef
hook to hold mutable data that you want to persist across re-renders but don't want to cause re-renders when this data changes.
jsxexport const Timer = () => { const intervalRef = useRef(); const startTimer = () => { intervalRef.current = setInterval(() => { console.log("Timer tick"); }, 1000); }; const stopTimer = () => { clearInterval(intervalRef.current); }; return ( <> <button onClick={startTimer}>Start Timer</button> <button onClick={stopTimer}>Stop Timer</button> </> ); }
In this example a useRef
is used to store a reference to an intervalId
returned by setInterval
function.
Using a ref in this case is safer and more efficient way to start and stop the interval without causing redundant renders.
Tips and Tricks When Using useRef React Hook
Avoid Recreation of Ref objects
React stores the initial value in the useRef(initialValue)
only once and ignores this initial value on the next renders.
Let's have a look on example where we might use a complex data structure or a third-party library that involves an expensive setup process:
jsximport ComplexLibrary from 'some-complex-library'; export const ExpensiveComponent = () => { const complexRef = useRef(new ComplexLibrary()); return ( <div> <button onClick={() => complexRef.current.doSomething()}> Do Something </button> </div> ); }
While a value returned by creating ComplexLibrary
is used only during an initial render and is ignored on the following ones - the initialization of ComplexLibrary
is still called on every render.
It might be very inefficient and slow down the entire application. It is recommended to use the following pattern to make this code more efficient:
jsximport ComplexLibrary from 'some-complex-library'; export const ExpensiveComponent = () => { const complexRef = useRef(null); if (complexRef.current === null) { complexRef.current = new ComplexLibrary(); } return ( <div> <button onClick={() => complexRef.current.doSomething()}> Do Something </button> </div> ); }
Here we make sure that complexRef
is initialized with a value only when it is null.
And when it is already initialized - the complex initialization is not executed at all.
Avoid Null Checks When Initializing useRef Later
When using a type checker you will receive null reference warning if you initialize useRef
with a real value later.
For example, you will get such a warning in the previous example, because a complexRef has null
as initial value.
To get rid of this null warning, you can use the following pattern:
jsxconst complexRef = useRef(null); const getComplexRef = () => { if (complexRef.current !== null) { return complexRef.current; } const complexObject = new ComplexLibrary(); complexRef.current = complexObject; return complexObject; }
Then you can call getComplexRef()
to access a complexRef.current
without null access warnings:
jsxexport const ExpensiveComponent = () => { // ... return ( <div> <button onClick={() => getComplexRef().doSomething()}> Do Something </button> </div> ); }
How To Pass Reference to Child Components
By default, own components don't expose refs to the DOM nodes inside them. You aren't able to do something like this:
jsxexport const CustomInput = ({ value, onChange }) => { return ( <input value={value} onChange={onChange} /> ); } export const ForwardRefExample = () => { const inputRef = useRef(null); return <CustomInput ref={inputRef} />; }
Instead, you should wrap the CustomInput
component with a forwardRef
function:
jsxexport const CustomInput = forwardRef(({ value, onChange }, ref) => { return ( <input value={value} onChange={onChange} ref={ref} /> ); }); export const ForwardRefExample = () => { const inputRef = useRef(null); return <CustomInput ref={inputRef} />; }
Now a ref can be safely retrieved from the CustomInput
component: <CustomInput ref={inputRef} />
.
Caveats and Pitfalls When Using useRef React Hook
Take into account the following nuances when using a useRef
Hook:
- When you update the
ref.current
property, React does not re-render the component. - Do not read or write
ref.current
during rendering, except for initialization. This can make your component's behavior unpredictable. - You can mutate the
ref.current
property. However, if it holds an object that is used for rendering, then you should avoid value mutation. - When using Strict Mode in development, React will call your component function twice in order to help you find accidental impurities. Each ref object will be created twice, but one of the versions will be discarded. If your component function is pure (as it should be), this should not affect the behavior. This behaviour does NOT affect production.
Summary
useRef
is a powerful hook in the React ecosystem, providing an elegant solution for accessing DOM elements and storing mutable data across renders without additional rendering.
By understanding and applying useRef
in your React applications, you can achieve more control over your components and optimize your application's performance.
Remember to use useRef
wisely, as typically most of the use cases can be solved without using it.
When using useRef
too much - you can make the code less readable and less predictable.
Consider the following useRef
tips:
- Not Just for DOM: While accessing DOM elements is a common use,
useRef
is also valuable for keeping any mutable value around for the lifetime of the component. - Avoid Overuse: Use
useRef
wisely. For instance, if you find yourself synchronizing ref values with state, consider whether state alone could serve your purpose. - Combine with useEffect:
useRef
can be combined withuseEffect
hook to track changes in state or props over time without triggering additional renders.
Hope you find this blog post useful. Happy coding!