useEvent: the new upcoming hook?
Tags: react, javascript, tutorial, news
Published: 2022-05-11 16:16:13

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](https://dev.to/romaintrotard/things-you-need-to-know-about-react-ref-25ld) and [When to use useCallback?](https://dev.to/romaintrotard/when-to-use-usecallback-3o6e) 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?

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

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

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

> **Note:** You can see me on [Twitch](https://www.twitch.tv/RainbowApps) 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**:

```js
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 mutualize the code, because it will be used often:

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

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

  return ref;
}
```

> **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](https://dev.to/romaintrotard/under-the-hood-of-event-listeners-in-react-4g01).

----

### 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.

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

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

You have to use the `useCallback` hook.

```js
import { useCallback } from "react";

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

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

> **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?](https://dev.to/romaintrotard/when-to-use-usecallback-3o6e), 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.

```js
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.

----

## 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:

```js
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 :notes:
So, with the implementation above, we could have a stale callback in the following code and not log the right `count`:

```jsx
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>;
}
```

----

## 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:

```js
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.

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

```js
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 :)

----

## 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](https://twitch.tv/rainbowapps ) or go to my [Website](https://www.romaintrotard.com).