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.