22
头图

Hello everyone, I'm Casson.

We know that there is a so-called closure trap when using Hooks , consider the following code:

 function Chat() {
  const [text, setText] = useState('');

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

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

We expect that after clicking sendMessage will pass the latest value of text .

However, in fact, since the callback function is cached by useCallback to form a closure, the effect of the click is always sendMessage('') .

This is the closure trap .

A workaround for the above code is to add a dependency to useCallback :

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

但是这么做了后,每当依赖项( text )变化, useCallback会返回一个全新的onClick引用, useCallback The role of caching function references .

The emergence of the closure trap has increased the threshold for getting started with Hooks , and also made it easier for developers to write code with bug .

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

Welcome to join the human high-quality front-end framework group , with flying

useEvent

The solution is to introduce a new native Hook useEvent .

It is used to define a function, this function has 2 properties:

  1. Keep references consistent across components multiple times render
  2. The function can always get the latest props and state

The above example uses useEvent after transformation:

 function Chat() {
  const [text, setText] = useState('');

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

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

When Chat component multiple times render , onClick always points to the same reference.

And onClick can always get the latest value of text 4f938572aa9e57462acf7ce99560fd79--- when it is triggered.

The reason why it is called useEvent is because React The team believes that the main application scenario of this Hook is to encapsulate event processing functions .

Implementation of useEvent

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

 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. Returns a useCallback with no dependencies, so that the reference to the function is consistent every time render
 useCallback((...args) => {
  const fn = handlerRef.current;
  return fn(...args);
}, []);
  1. Update handlerRef.current at the right time, so that the actually executed function is always the latest reference

Differences with Open Source Hooks

Many open source Hooks libraries have implemented similar functions (such as ahooks in useMemoizedFn )

useEvent The differences with these open source implementations are mainly reflected in:

useEvent is located in the single scene of processing event callback functions , while useMemoizedFn is located in the cache of various functions .

So the question is, since the functions are similar, useEvent should you limit your usage scenarios?

The answer is: for more stability.

useEvent Whether you can get the latest state and props depends on the timing of handlerRef.current update.

In the above simulation implementation, the logic of useEvent update handlerRef.current is placed in the useLayoutEffect callback.

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

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

The timing of the event callback is obviously after the view is rendered , so the latest state and props can be obtained stably.

Note: The actual update time in the source code is earlier, but it does not affect the conclusion here

Let's take a look at ahooks in useMemoizedFn , and the update timing of fnRef.current is when useMemoizedFn is executed (that is, when the component is rendered):

 function useMemoizedFn<T extends noop>(fn: T) {
  const fnRef = useRef<T>(fn);

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

  // ...省略代码
}

When React18 enables concurrent updates , the number and timing of the component render are uncertain.

Therefore, the update timing of useMemoizedFn fnRef.current is also uncertain.

This increases the potential risk when used with concurrent updates .

It can be said that useEvent By limiting handlerRef.current update timing, and then limit the application scenarios, and finally achieve the purpose of stability.

Summarize

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

Many enthusiastic developers have suggested the naming of this Hook , for example: useStableCallback :

Another example: useLatestClosure :

From these names, they obviously expand the application scenarios of useEvent .

After the analysis of this article, we know that expanding application scenarios means increasing the risk of developers making mistakes .


卡颂
3.1k 声望16.7k 粉丝