TypeOfNaN

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

Nick Scialli is a senior UI engineer at Microsoft.

© 2024 Nick Scialli