Go to main content

The previous week, Dan Abramov merged a new rfc about a hook named useEvent. I propose you to look at this coming soon hook, I hope :)

Before reading this article, I recommend you to read my Things you need to know about React ref and When to use useCallback? if it's not already done.

Have you ever felt that you add a dependency to a hook (useEffect or useCallback for example) not to have a stale closure but feeling that it's not good?

useEffect(() => {
  const twitchClient = new TwitchClient();
  twitchClient.connect();

  twitchClient.on("message", (message) => {
    if (shouldNotReadMessage) {
      console.log(`The message is: ${message}`);
    }
  });

  return () => twitchClient.disconnect();
}, [shouldNotReadMessage]);

Why I'm feeling bad about this code?

My client will disconnect / reconnect each time the shouldNotReadMessage changes, which is odd because just using it in an event listener.

So I decide to use a React ref:

const [shouldNotReadMessage, setShouldNotReadMessage] =
  useState(true);

const shouldNotReadMessageRef = useRef(
  shouldNotReadMessage
);
// Do not forget to update the reference
// This `useEffect` has to be before the next one
useEffect(() => {
  shouldNotReadMessageRef.current = shouldNotReadMessage;
});

useEffect(() => {
  const twitchClient = new TwitchClient();
  twitchClient.connect();

  twitchClient.on("message", (message) => {
    if (shouldNotReadMessageRef.current) {
      console.log(`The message is: ${message}`);
    }
  });

  return () => twitchClient.disconnect();
}, []);

No more disconnect / reconnect every time shouldNotReadMessage changes but some boilerplate code.

It's possible to make a custom hook useStateRef to share the code, because it will be used often:

function useStateRef(state) {
  const ref = useRef(state);

  useLayoutEffect(() => {
    ref.current = state;
  });

  return ref;
}

Previous example analysis

In the previous example, the callback that needs the latest value of the state shouldNotReadMessage is an event listener. Because we want to execute the callback only when a message is received.

Most of the time, we work with event listener, their particularity is that their name can start by on. You are probably more used to deal with DOM event listener, for example when adding an onClick listener on a button.


Have you ever deal with memoized components?

A memoized component optimizes re-render. The principle is simple: if there is no prop that has changed then the component does not render. It can be useful when dealing with component having costly renders.

So any references should be fixed.

So if you have the following code, the memoization is useless. Because each time the App renders a new onClick callback is created.

function App() {
  const onClick = () => {
    console.log("You've just clicked me");
  };

  return <MemoizedComponent onClick={onClick} />;
}

You have to use the useCallback hook.

import { useCallback } from "react";

function App() {
  const onClick = useCallback(() => {
    console.log("You've just clicked me");
  }, []);

  return <MemoizedComponent onClick={onClick} />;
}

What happened if your callback needs an external variable?

Well it depends. If you want to access a ref it's totally fine. But if it's a state you will have to add it in the array dependency of useCallback.

When this callback is an event listener then the problem is the same as before with useEffect. It seems useless to recreate a new callback each time because will make the memoized component re-render because of that.

So we will use the useStateRef hook implemented before.

Because of that you can have complex code. Trust me it happened to me :(


In my article When to use useCallback?, I tell that I try to always useCallback functions that I return from hooks that will be used in multiple places, because I don't know the place where it will be used: in useEffect? in useCallback? in event listener? But sometimes it's complicated to make a fully fixed reference. So it can happen, like in the previous example, that an event listener that is memoized is recreated unnecessarily.

import { useCallback, useState } from "react";

function useCalendar() {
  const [numberDayInMonth, setNumberDayInMonth] =
    useState(31);
  const [currentYear, setCurrentYear] = useState(2022);
  const [currentMonth, setCurrentMonth] =
    useState("January");

  const onNextYear = useCallback(() => {
    setCurrentYear((prevYear) => {
      const nextYear = prevYear + 1;
      if (currentMonth === "February") {
        const isLeapYear = ... // some process with nextYear

        const isLeapYear = false;
        if (isLeapYear) {
          setNumberDayInMonth(29);
        } else {
          setNumberDayInMonth(28);
        }
      }

      return nextYear;
    });
  }, [currentMonth]);

  // In a real implementation there will be much more stuffs
  return {
    numberDayInMonth,
    currentYear,
    currentMonth,
    onNextYear,
  };
}

In this case, a new callback for onNextYear will be created each time currentMonth changes.

Here again the solution would be to use the useStateRef hook implemented before.


The solution to all the above problems is that React exposes a new hook probably named useEvent that returns a memoized callback (with useCallback) that called the latest version of our callback.

It's quite similar to the implementation I show earlier with useStateRef but with callback.

An example of implementation would be:

function useEvent(handler) {
  const handlerRef = useRef(null);

  useLayoutEffect(() => {
    handlerRef.current = handler;
  });

  return useCallback((...args) => {
    return handlerRef.current(...args);
  }, []);
}

In reality, this will not use a useLayoutEffect because it must run before other useLayoutEffect so that they have the latest value of our callback for every case. They will probably do an internal implementation to execute the update of the ref before all useLayoutEffect.

As a reminder, useLayoutEffect and useEffect are executed from bottom to top in the tree. Started from the bottom 🎶 So, with the implementation above, we could have a stale callback in the following code and not log the right count:

function Parent() {
  const [count, setCount] = useState(0);
  const onPathnameChange = useEvent((pathname) => {
    // Note that we use a state value
    console.log(
      "The new pathname is:",
      pathname,
      "and count:",
      count
    );
  });

  return (
    <>
      <Child onPathnameChange={onPathnameChange} />
      <button
        type="button"
        onClick={() => setCount(count + 1)}
      >
        Increment
      </button>
    </>
  );
}

function Child({ onPathnameChange }) {
  const { pathname } = useLocation();

  useLayoutEffect(() => {
    // Here we would have a stale `onPathnameChange`
    // Because this is executed before the `useEvent` one
    // So it can happen we have the previous `count` in the log
    onPathnameChange(pathname);
  }, [pathname, onPathnameChange]);

  return <p>Child component</p>;
}

Because the hook uses under the hood React reference it should not be called in render, due to problem we could encounter with Concurrent features. For example a renderItem callback should not be stabilized with useEvent but with useCallback.


The major question I have is: should it be the component / hook that declares the function that wraps in useEvent or the component / hook that executes the callback?

I am sure that when using a memoized component it should be done at the declaration level, otherwise the memoization won't work:

function MyComponent() {
  const onClick = useEvent(() => {});

  return <MemoizedComponent onClick={onClick} />;
}

In other case, should we do at the declaration like today for useCallback and make a nice documentation telling that it's an event callback? I think the easiest solution will be at the execution side. Like this we can ensure that the behavior inside the component is the right we want without taking care of how a person uses this one.

The linter part of the RFC, goes in my way:

In the future, it might make sense for the linter to warn if you have handle* or on* functions in the effect dependencies. The solution would be to wrap them into useEvent in the same component.

So it's likely that React pushes to use useEvent at the call site.

function Button({ onClick: onClickProp, label }) {
  const onClick = useEvent(onClickProp);

  return (
    <button type="button" onClick={onClick}>
      {label}
    </button>
  );
}

In any case, If it's done in both side, double wrap a callback with useEvent should work too :)


I am really waiting for this new hook that will for sure simplify some code. I have already a lot of place in my codebase where it will help a lot. Do not overuse useEffect when you can call some code in event listener just do it ;) Do not change a state, to "watch" it with a useEffect. Every callback that can be named with the prefix on or handle could be wrapped with this new hook but should we always do it? Dan Abramov told in a comment that it could be the case, but it's not the aim of the RFC.

In the longer term, it probably makes sense for all event handlers to be declared with useEvent. But there is a wrinkle here with the adoption story in regard to static typing. We don't have a concrete plan yet (out of scope of this RFC) but we'd like to have a way for a component to specify that some prop must not be an event function (because it's called during render).

Maybe the name could change for something like useHandler, because this is not returning an event but a handler.

Once the RFC is validated, the React team should work on recommendation about how to use it.

Are you hyped by this RFC? Do you have any questions?

To be continued :)


You can find me on Twitter if you want to comment this post or just contact me. Feel free to buy me a coffee if you like the content and encourage me.