We know that there is a so-called closure trap in the use of Hooks, consider the following code.

1
2
3
4
5
6
7
8
9
function Chat() {
  const [text, setText] = useState('');

  const onClick = useCallback(() => {
    sendMessage(text);
  }, []);

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

We expect sendMessage to pass the latest value of text after a click.

However, in reality, the effect of the click is always sendMessage('') because the callback function is cached by useCallback, forming a closure.

This is the closure trap.

One solution to the above code is to add a dependency for useCallback.

1
2
3
const onClick = useCallback(() => {
  sendMessage(text);
}, [text]);

But after doing so, whenever the dependency (text) changes, useCallback will return a new onClick reference, which defeats the purpose of useCallback caching function references.

The advent of the closure trap has increased the threshold for getting started with Hooks and made it easier for developers to write buggy code.

Now, the official React team is going to solve this problem.

useEvent

The solution is to introduce a new native HookuseEvent.

He is used to define a function that has 2 features.

  1. it keeps the reference consistent across multiple renders of the component
  2. the latest props and state are always available within the function

The above example is modified with useEvent as follows.

1
2
3
4
5
6
7
8
9
function Chat() {
  const [text, setText] = useState('');

  const onClick = useEvent(() => {
    sendMessage(text);
  });

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

The onClick always points to the same reference when the Chat component is rendered multiple times.

And the latest value of text is always available when onClick is triggered.

It’s called useEvent because the React team believes that the main application scenario for this Hook is to wrap event handler functions.

useEvent implementation

The implementation of useEvent is not difficult, the code is similar to the following.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
function useEvent(handler) {
  const handlerRef = useRef(null);

  // 视图渲染完成后更新`handlerRef.current`指向
  useLayoutEffect(() => {
    handlerRef.current = handler;
  });

  // 用useCallback包裹,使得render时返回的函数引用一致
  return useCallback((...args) => {
    const fn = handlerRef.current;
    return fn(...args);
  }, []);
}

The whole consists of two parts.

  1. returning a useCallback with no dependencies, so that the reference to the function is the same for each render

    1
    2
    3
    4
    
    useCallback((...args) => {
    const fn = handlerRef.current;
    return fn(...args);
    }, []);
    
  2. update handlerRef.current at the right time so that the actual function executed is always the latest reference

Differences between ## and open source Hooks

Many open source Hooks libraries already implement similar functionality (e.g. useMemoizedFn in ahooks)

The main differences between useEvent and these open source implementations are that useEvent is focused on the single scenario of handling event callback functions, while useMemoizedFn is focused on caching various functions.

So the question is, if the functions are similar, why does useEvent have to limit itself to a single scenario?

The answer is: for more stability.

Whether useEvent can get the latest state and props depends on when handlerRef.current is updated.

In the above mockup, the logic for useEvent to update handlerRef.current is placed in the useLayoutEffect callback.

This ensures that handlerRef.current is always updated after the view has finished rendering.

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

The event callback is apparently triggered after the view has finished rendering, so the latest state and props are consistently available.

Note: the actual update timing in the source code is earlier, but that doesn’t affect the conclusion here

Looking at useMemoizedFn in ahooks, fnRef.current is updated when useMemoizedFn is executed (i.e., when the component renders).

1
2
3
4
5
6
7
8
function useMemoizedFn<T extends noop>(fn: T) {
  const fnRef = useRef<T>(fn);

  // 更新fnRef.current
  fnRef.current = useMemo(() => fn, [fn]);

  // ...省略代码
}

When React18 has concurrent updates enabled, the number and timing of component render is uncertain.

So the timing of updates to fnRef.current in useMemoizedFn is also uncertain.

This increases the potential risk when used with concurrent updates.

It can be said that useEvent limits the application scenario by limiting the update timing of handlerRef.current, which ultimately leads to stability.

Summary

useEvent is still in the RFC (Request For Comments) phase.

Many enthusiastic developers have suggested names for this Hook, for example: useStableCallback.

useStableCallback

Another example: useLatestClosure.

useLatestClosure

From these nomenclature, it is clear that they have expanded the application scenario of useEvent.

As we know from the analysis in this article, expanding the application scenario means increasing the risk of error when developers use it.