4

useEvent solves a problem: how to keep the function reference unchanged and access the latest state at the same time.

This week, we combine the original RFC with the interpretation of the article What the useEvent React hook is (and isn't) to understand the proposal.

Borrowing the code in the proposal, it is clear what useEvent is:

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

  // ✅ Always the same function (even if `text` changes)
  const onClick = useEvent(() => {
    sendMessage(text);
  });

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

onClick not only keeps the reference unchanged, but also can access the latest text value every time it is triggered.

Why this function is provided and what problem it solves is explained in the overview.

Overview

Defining a function that accesses the latest state is trivial:

 function App() {
  const [count, setCount] = useState(0)

  const sayCount = () => {
    console.log(count)
  }

  return <Child onClick={sayCount} />
}

But sayCount function reference changes every time, which directly breaks the Child component memo effect, and even triggers its more serious chain reaction ( Child component onClick when the callback is called in useEffect ).

To keep sayCount reference unchanged, we need to wrap it with useCallback :

 function App() {
  const [count, setCount] = useState(0)

  const sayCount = useCallback(() => {
    console.log(count)
  }, [count])

  return <Child onClick={sayCount} />
}

But even so, we can only guarantee that count sayCount references will not change when ---33ce4a2340403b522ef16925ab25ad6a--- does not change. If you want to keep the sayCount reference stable, you must remove the dependency [count] , which will cause the accessed count to always be the initial value, which logically triggers more big problem.

A helpless solution is to maintain a countRef to keep its value synchronized with count, and access ---37952deb0c864bc22c7e58206261c307 sayCount in countRef :

 function App() {
  const [count, setCount] = useState(0)
  const countRef = React.useRef()
  countRef.current = count

  const sayCount = useCallback(() => {
    console.log(countRef.current)
  }, [])

  return <Child onClick={sayCount} />
}

This code can solve the problem, but is definitely not recommended for two reasons:

  1. A matching Ref must be added to each value, which is very redundant.
  2. It's not a good idea to update the ref synchronously directly in the function, but it's too cumbersome to write in useEffect .

Another way is to create your own hook, such as useStableCallback , which is essentially the protagonist of this proposal useEvent :

 function App() {
  const [count, setCount] = useState(0)

  const sayCount = useEvent(() => {
    console.log(count)
  })

  return <Child onClick={sayCount} />
}

So the internal implementation of useEvent is likely to be similar to the custom hook useStableCallback . Possible implementation ideas are also given in the proposal:

 // (!) Approximate behavior
function useEvent(handler) {
  const handlerRef = useRef(null);

  // In a real implementation, this would run before layout effects
  useLayoutEffect(() => {
    handlerRef.current = handler;
  });

  return useCallback((...args) => {
    // In a real implementation, this would throw if called during render
    const fn = handlerRef.current;
    return fn(...args);
  }, []);
}

In fact, it is easy to understand. We divide the requirements into two parts:

  1. Since a stable reference is to be returned, the last returned function must use useCallback and set the dependency array to [] .
  2. In order to access the latest value when the function is executed, the latest function must be executed every time, so Ref is used in the Hook to store the latest function reference received each time. When the function is executed, the latest function is actually executed. function reference.

Note the two paragraphs of comments, the first one is useLayoutEffect part is actually earlier than layoutEffect execution timing, this is to ensure that when the function is directly consumed in an event loop, it may be accessed to the old Ref value; the second is to throw an exception when called when rendering, this is to avoid the useEvent function being used when rendering, because then it cannot be data-driven.

intensive reading

In fact useEvent concept and implementation are very simple, let's talk about some interesting details in the proposal.

Why is it named useEvent

It is mentioned in the proposal that if the length of the name is not considered and the function is completely named, useStableCallback or useCommittedCallback would be more appropriate, both of which means getting a stable callback function. But useEvent is named from the user's point of view, that is, the generated functions are generally used for component callback functions, and these callback functions generally have "event characteristics", such as onClick , onScroll , so when developers see useEvent , they can subconsciously remind themselves that they are writing an event callback, which is quite intuitive. (Of course I think the main reason is to shorten the name, easy to remember)

Values are not really real-time

Although useEvent can get the latest value, there is still a difference between ---01328b9141e5974999f81857c63d424a--- and useCallback ref . The difference is reflected in:

 function App() {
  const [count, setCount] = useState(0)

  const sayCount = useEvent(async () => {
    console.log(count)
    await wait(1000)
    console.log(count)
  })

  return <Child onClick={sayCount} />
}

await The output value before and after must be the same. In terms of implementation, the count value is only a snapshot of the call, so when the function is asynchronously waiting, even if the external count , the current function call still can't get the latest count , and the ref method is OK. In terms of understanding, in order to avoid nightmares, the callback function should not be written asynchronously.

useEvent can't save the handicapped

If you insist on writing code like onSomething={cond ? handler1 : handler2} , then after cond changes, the passed function reference will also change, which is useEvent and avoid it anyway. No, maybe the solution is Lint and throw error.

Actually wrapping cond ? handler1 : handler2 as a whole in useEvent will solve the reference change problem, but no one except Lint can prevent you from bypassing it.

Can a custom hook be used instead of the useEvent implementation?

cannot. Although an approximate solution is given in the proposal, there are actually two problems:

  1. When assigning ref, useLayoutEffect the timing is still not advanced enough. If the access function is understood after the value changes, the old value will be obtained.
  2. The generated function is used for rendering without giving an error.

Summarize

useEvent Obviously, an official concept has been added to React, which not only increases the cost of understanding, but also fills in the important part that React Hooks is missing in practice. Whether you like it or not, the problem Right there, the solution is also given, which is good.

The discussion address is: Intensive Reading "React useEvent RFC" Issue #415 dt-fe/weekly

If you'd like to join the discussion, click here , there are new topics every week, with a weekend or Monday release. Front-end intensive reading - help you filter reliable content.

Follow Front-end Intensive Reading WeChat Official Account

<img width=200 src="https://img.alicdn.com/tfs/TB165W0MCzqK1RjSZFLXXcn2XXa-258-258.jpg">

Copyright notice: Free to reprint - non-commercial - non-derivative - keep attribution ( Creative Commons 3.0 license )

黄子毅
7k 声望9.5k 粉丝