What the useEvent React hook is (and isn't)
Nick Scialli
May 06, 2022
This past week, the React core team published a Request for Comment (RFC) for a new React hook: useEvent
. This post attempts to capture what this hook is, what it isn’t, and what my initial reactions are.
Note that this is an RFC and isn’t released yet, so it isn’t available yet and its behavior could change.
Trying to solve a real problem
There’s a real problem useEvent
is trying to solve. Before we jump into what useEvent
is, let’s wrap our heads around the problem.
React’s execution model is largely powered by comparing the current and previous values of things. This happens in components and in hooks like useEffect
, useMemo
, and useCallback
.
Consider the following component:
function MyApp() {
const [count, setCount] = useState(0);
return <Counter count={count} />;
}
The Counter
component will re-render if the count
variable changes. Let’s say we also want some kind of effect to run when count
changes. We can use the useEffect
hook:
function MyApp() {
const [count, setCount] = useState(0);
useEffect(() => {
console.log(count);
}, [count]);
return <Counter count={count} />;
}
Since we have included count
in the useEffect
hook’s dependency array, the effect will re-run every time count
changes.
So what’s the problem?
With this model, many React developers find themselves running into the same problems: too many component re-renders or too many hook re-runs (sometimes infinite!).
The RFC has some excellent examples, so I’m going to use them! First, let’s consider a chat app where we have some text
state ina component and then a separate component for the SendMessage
button:
function Chat() {
const [text, setText] = useState('');
const onClick = () => {
sendMessage(text);
};
return <SendButton onClick={onClick} />;
}
The problem is that, whenever our text
changes, the onClick
function is recreated. It will never be referentially equal to the last onClick
function that was passed to SendButton
, and therefore we could easily be re-rendering SendButton
on every keystroke.
Now let’s consider an example when useEffect
runs too often. This is an example from Dan Abravov (React core team) on Twitter. In this example, we have an effect that logs a page visit every time the route.url
changes.
function Page({ route, currentUser }) {
useEffect(() => {
logAnalytics('visit_page', route.url, currentUser.name);
}, [route.url, currentUser.name]);
}
However, we’ll also log a page visit with the user’s name is updated. We don’t want this. We could remove currentUser.name
from the dependency array, but this is a pretty bad practice in React—if your dependency arrays don’t reflect all the dependencies in your effect function body, you wind up with stale closures and bugs that are hard to track down. This is important enought hat there’s an “exhaustive deps” rule in the react-hooks
ESLint plugin that the React core team strongly encourages.
The proposed solution: useEvent
That was a lot of setup; glad you stuck with me! Now we can finall talk about useEvent
. This new hook is being created to ensure we have a stable reference to a function without having to create a new function based on its dependents.
That’s a mouthful—perhaps it’s easier to just show.
Let’s revisit our chat app with the SendButton
that re-renders way too much. When useEvent
exists, we’ll be able to wrap our click handler and have a function that doesn’t change reference even though the text
around which it has closure is changing:
function Chat() {
const [text, setText] = useState('');
const onClick = useEvent(() => {
sendMessage(text);
});
return <SendButton onClick={onClick} />;
}
Now, onClick
will always refer to the same function instead of being recreated on each render, and therefore SendButton
will not continually be re-rendered.
Next up, let’s work on the page visit logger:
function Page({ route, currentUser }) {
const logVisit = useEvent((pageUrl) => {
logAnalytics('visit_page', pageUrl, currentUser.name);
});
useEffect(() => {
logVisit(route.url);
}, [route.url]);
}
Now that we’ve create a stable logVisit
function, we can remove currentUser.name
from the useEffect
function body and only run the effect when route.url
changes.
Initial impressions
My initial reaction to the useEvent
hook was “huh, what does useEvent
even mean?” Like many people, I’m not sold on the name of the hook. That aside, this hook would have saved me a lot of headaches over the past few years wrestling with effect dependencies. Overall, I think this is going to be a great addition to the React ecosystem.
What the hook isn’t
The useEvent
hook isn’t a silver bullet, that’s for sure. It adds yet another concept to React. It also doesn’t change the fact that React’s isn’t truly reactive. I’ll do a quick plug for SolidJS, a truly reactive framework, and how simple the logger would become:
function Page(props) {
createEffect(() => {
logAnalytics(
'visit_page',
props.route.url,
untrack(() => props.currentUser.name)
);
});
}
Disclaimer: I’m on the SolidJS docs team, so I’m partial to it. But, I still think this is a lot simpler! The effect’s dependency on props.route.url
and props.currentUser.name
are automatically tracked. To “untrack” one of those dependencies, Solid provides an untrack
function. The code above will trigger only when props.route.url
changes but will have current closure around the user’s name.
Nick Scialli is a senior UI engineer at Microsoft.