blog post

Demystifying useRef: A React Hook Guide

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:

jsx
const 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.

jsx
export 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:

jsx
export 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.

Screenshot_1

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.

jsx
export 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:

jsx
import 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:

jsx
import 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.

Screenshot_1

To get rid of this null warning, you can use the following pattern:

jsx
const 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:

jsx
export 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:

jsx
export 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:

jsx
export 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 with useEffect hook to track changes in state or props over time without triggering additional renders.

Hope you find this blog post useful. Happy coding!

Improve Your .NET and Architecture Skills

Join my community of 1800+ developers and architects.

Each week you will get 2 practical tips with best practises and architecture advice.