DEV Community

Cover image for useEvent: the new upcoming hook?
Romain Trotard
Romain Trotard

Posted on

useEvent: the new upcoming hook?

The previous week, Dan Abramov merged a new rfc about 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.

Explanations of the problem

A first example

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]);
Enter fullscreen mode Exit fullscreen mode

Note: You can see me on Twitch where I do development and creative hobby.

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();
}, []);
Enter fullscreen mode Exit fullscreen mode

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

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

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

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

  return ref;
}
Enter fullscreen mode Exit fullscreen mode

Note: You will have to put the useStateRef returned value as dependency if you want to satisfy the linter.

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.

Note: If you want to know how React handles DOM event listener you can read my article Under the hood of event listeners in React.


A second example

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.

Note: You can fine grained when the component should render. For example with React.memo it will be thanks to the second parameter. The default behavior is to render when any prop has changed.

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} />;
}
Enter fullscreen mode Exit fullscreen mode

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} />;
}
Enter fullscreen mode Exit fullscreen mode

Note: In this example I could just extract the callback outside of the component, but we are going to complexify the code soon ;)

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 than 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 :(


A last example

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,
  };
}
Enter fullscreen mode Exit fullscreen mode

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.


useEvent to the rescue

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);
  }, []);
}
Enter fullscreen mode Exit fullscreen mode

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>;
}
Enter fullscreen mode Exit fullscreen mode

When not to use useEvent?

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.

Note: In this example it's logic because it's not an event handler.

Note: useEvent should be used when the callback can be prefixed by on or handle.


Question I ask myself

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} />;
}
Enter fullscreen mode Exit fullscreen mode

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.

Note: The drawback is that it can lead to re-declare new callback each times:

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

  return (
    <button type="button" onClick={onClick}>
      {label}
    </button>
  );
}
Enter fullscreen mode Exit fullscreen mode

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


Conclusion

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 overused 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 with regards 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 an 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 :)


Do not hesitate to comment and if you want to see more, you can follow me on Twitch or go to my Website.

Discussion (0)