<- GOBACK

You might not need useRef for that

According to the React maintainers, React developers reach for the useEffect hook too quickly. It is far from the only hook with many naive usages. Why won't we go through my favorite example of an incorrect usage for useRef?

What is the useRef hook?

In React, the data used for rendering is immutable. A change in a piece of state is committed through a setter or reducer.

const Component = () => {
	const [name, setName] = useState<string>('');

	return (
		<input
			type="text"
			value={name}
			onChange={e => setName(e.target.value)}
		/>
	);
};

These changes are observable to execute effects, invalidate memoization and rerender your component.

useRef initializes and gives you access to a mutable variable across executions of your component or hook. Mutable because on each render, useRef will return the same RefObject to which you can store data.

const Component = () => {
	const buttonPresses = useRef<number>(0);
	return <button onClick={() => buttonPresses.current++} />;
};

They do not trigger all the cool stuff that the observable state does.

A powerful and straightforward way to look at React is that it wants us to keep our state and UI in sync.

Where and when can you use useRef?

useRef is not just an escape hatch but can be used to read and write data in event handlers and effects. Remember that changing the ref won't trigger the useEffect through its dependency array.

It's not unreasonable to think that you can only pass a RefObject to a jsx ref attribute.

const container = useRef<HTMLDivElement>(null);
return <div ref={container}>{children}</div>;

Note that we initialise useRef with null. container.current is reset to null whenever the node unmounts. Therefore container.current will only ever be an HTMLDivElement or null. It is important for typesafety and generally considered good practice to always pass something to the useRef initializer.

You should use this pattern whenever you want to access your DOM nodes inside useEffect or event handlers.

For example, when we want a click on the ::backdrop of a <dialog> to close it. In the onClick event handler, we first check if the click target is the dialog element. Then we can use the native dialog.close() API to close it.

const Dialog = ({ children, state }) => {
	const dialogRef = useRef<HTMLDialogElement>(null);
	
	const handleClick = (e) => {
		if (e.target === dialogRef.current) 
			dialogRef.current.close();
	};
	
	return (
		<dialog ref={dialogRef} onClick={handleClick}>
			{children}
		</dialog>
	);
}

Where it breaks down

When I explained you could use mutable refs inside of useEffect, and you can assign a DOM node to that mutable ref through the ref attribute, you might try to add and remove event listeners inside of useEffect.

This pattern was deployed inside formkit's auto-animate. I discovered this when I checked why their react hook was only sometimes working for me.

This is a slightly simplified version of the bundled hook:

import { useEffect, useRef, RefObject } from 'react'
import autoAnimate, { AutoAnimateOptions } from '../index'

/**
* AutoAnimate hook for adding dead-simple transitions and animations to react.
* @param options - Auto animate options
* @returns
*/

export function useAutoAnimate<T extends Element>(
	options: Partial<AutoAnimateOptions> = {}
): [RefObject<T>] {
	const element = useRef<T>(null)
	
	useEffect(() => {
		if (element.current instanceof HTMLElement)
			autoAnimate(element.current, options)
	}, [element])
	
	return [element]
}

And this is how I consumed the useAutoAnimate hook.

import { useAutoAnimate } from '@formkit/auto-animate/react'

const Component = () => {
	const [container] = useAutoAnimate<HTMLUlElement>();
	const [items, error] = useSwr(/* swr config */);

	return (
		<section>
		{!items && !error && (<spinner />)}
		{items && (
			<ul ref={container}>
				{items.map(/* item renderer */)}
			</ul>
		)}
		</section>
	)
};

Did you spot the problem? This fragment has the unobservable reference element inside the dependency array of a useEffect, which does not work. This useEffect will only execute after the initial render. If the initial render has items, the ref will hold the <ul> DOM node.

Due to waiting for data to fetch, element hasn't received the DOM node and fails to register the autoAnimate.

Callback refs to the rescue

Let us figure out what the jsx ref attribute accepts as value.

interface RefObject<T> { 
	readonly current: T | null;  
}  
type RefCallback<T> = (instance: T | null) => void;

type Ref<T> = RefCallback<T> | RefObject<T> | null;

From the React docs, we know that RefCallback receives the DOM node when being rendered and null on unmount. With this knowledge, we can rewrite the hook to accept DOM updates.

import { useCallback } from 'react';  
import autoAnimate, { AutoAnimateOptions } from '@formkit/auto-animate';  
  
type Options = Partial<AutoAnimateOptions>;  
  
const useAutoAnimate = <T extends HTMLElement>(options?: Options): ((element: T | null) => void) => {  
    return useCallback(  
        (element: T | null) => {  
            if (!element) return;  
            autoAnimate(element, options);  
        },  
        [options]  
    );  
};

An example with ResizeObserver

I was building a new pagination interaction for Flare's redesign, and a ResizeObserver came into play. This example shows that useRef is useful in other ways than accessing the DOM and how registering and cleaning up DOM observers/listeners can be accomplished using a callback ref.

const Component = () => {
	const [size, setSize] = useState<ResizeObserverSize | null>(null);

	const observer = useRef<ResizeObserver>(new ResizeObserver((entries) => {
		setSize(entries[0].borderBoxSize);
	}));

	const registerResizeObserver = useCallback(instance => {  
	    if (instance) return observer.current.observe(instance);  

	    observer.current.disconnect();
	}, []);

	return (
		<ul ref={registerResizeObserver}>
			{/* ... */}
		</ul>
	);
};

Note that by defining the ResizeObserver's callback inside of Component it can access state setters but cannot access the updated state because it created a closure on initialisation.

Read more

The React team is hard at work on building better documentation so that we can focus on writing better React code. referencing values with refs goes into more detail on how to use useRef safely. The challenges at the bottom of manipulating the dom with refs can build up your understanding of using DOM apis in react.

The old callback refs documentation outlines the idea of using a function for more fine-grained control over when refs are set and unset. The caveats of which can be solved using the useCallback hook.