DEV Community

Muhammad Haseeb
Muhammad Haseeb

Posted on

The UseEvent Hook

This article needs prior understanding of Hooks. So, if you are not already familiar please checkout the official docs.

I came across this hook while reviewing a PR of one of my colleague ciprian. One of my colleague introduced a hook in his PR and we had a long healthy technical discussion around that as it was solving this issue.

Motivation for introducing useEvent

The React core team published a Request for Comment (RFC) for a new React hook: useEvent. This post attempts to capture what this hook is and how it works.

Image description

Also here is the proposal for useEvent hook and its not been released yet (by the time I am writing this post) and its behavior could change.

The real problem

let’s wrap our heads around the problem before we jump into what useEvent is. React’s execution model is largely powered by comparing the current and previous values of state/props. This happens in components and in hooks like useEffect, useMemo, and useCallback.
Lets see this example to make better understanding of the problem.

Example

This onClick event handler needs to read the currently typed text:



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

  // 🟡 Always a different function
  const onClick = () => {
    sendMessage(text);
  };

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


Enter fullscreen mode Exit fullscreen mode

Let's say you want to optimize SendButton by wrapping it in React.memo. For this to work, the props need to be shallowly equal between re-renders. The onClick function will have a different function identity on every re-render, so it will break memoization.
The usual way to approach a problem like this is to wrap the function into useCallback hook to preserve the function identity. However, it wouldn't help in this case because onClick needs to read the latest text:



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

  // 🟡 A different function whenever `text` changes
  const onClick = useCallback(() => {
    sendMessage(text);
  }, [text]);

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


Enter fullscreen mode Exit fullscreen mode

The text changes with every keystroke, so onClick will still be a different function on every keystroke. (We can't remove text from the useCallback dependencies because otherwise the onClick handler would always "see" the initial text.

What's the solution

A new hook useEvent is being proposed to ensure we have a stable reference to a function without having to create a new function based on its dependents.

What is useEvent hook?

A Hook to define an event handler with an always-stable function identity.

By comparison, useEvent does not take a dependency array and always returns the same stable function, even if the text changes. Nevertheless, text inside useEvent will reflect its latest value:



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

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

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


Enter fullscreen mode Exit fullscreen mode

As a result, memoizing SendButton will now work because its onClick prop will always receive the same function.


When to use useCallBack?

You must be curious about what would be the use-cases where useCallBack will be useful after we have the useEvent hook? I had the same question back then.

Well, useCallaback will be used for memoizing render functions. For example:


 javascript
  const renderItem = useCallback(() => (
    <span>{itemName}</span>
  ), [itemName])


Enter fullscreen mode Exit fullscreen mode

Why? because useEvent cannot be used for this scenario as it would keep the same reference for render function, and this is not desired, because we need a changing ref so that we know when to rerender.

Event handlers wrapped in useEvent will throw if called during render. (Calling it from an effect or at any other time is fine.) So it is enforced that during rendering these functions are treated as opaque and never called. This makes it safe to preserve their identity despite the changing props/state inside.

You can see many more evidences on that issue and RFC itself, I am not listing those here to keep this post concise. Overall, I think this is going to be a great addition to the React ecosystem.


Here is what my colleague did to mock the same behavior that useEvent would probably be doing soon.



export default function useEventCallback<Args extends unknown[], Return>(
  fn: (...args: Args) => Return,
): (...args: Args) => Return {
  // ref is not initialized, in order to ensure that we can't call this in the render phase
  // ref.current will be undefined if we call the `fn` during render phase (as a render function)
  const ref = useRef<(typeof fn) | undefined>(undefined);

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

  return useCallback(
    (...args: Args) =>
    // make sure that the value of `this` provided for the call to fn is not `ref`
      ref.current.apply(void 0, args),
    [],
  );
}



Enter fullscreen mode Exit fullscreen mode

Hit the ❤️ if it was useful. Please share your feedback so I can make my blog posts more helpful. 😊

Top comments (1)

Collapse
 
bamthedev profile image
bam

ref.current will be undefined if we call the fn during render phase

Does it mean you'll get Uncaught TypeError: Cannot read properties of undefined (reading 'apply') trying call undefined?

ref.current.apply(void 0, args)
Seems you have to use if or ?.