DEV Community

Cover image for React 19.2's useEffectEvent vs our Custom useEventCallback
Hamid Shoja
Hamid Shoja

Posted on

React 19.2's useEffectEvent vs our Custom useEventCallback

Hey fellow React devs! πŸ‘‹

So React 19.2 dropped and brought us useEffectEvent - a new hook that solves a problem many of us have been tackling with custom solutions. If you're like me and have been using a custom useEventCallback hook (or something similar), you're probably wondering: "Should I ditch my trusty custom hook for this new official one?"

BTW: if you interested to read it from medium.

Let me break it down for you based on some real-world experience.

The Problem Both Hooks Solve

First, let's talk about why these hooks exist. You know that annoying situation where you want to access the latest state/props/reference in a useEffect without adding them to the dependency array? Yeah, that one:

function Page({ url }) {
  const { items } = useContext(ShoppingCartContext);
  const numberOfItems = items.length;

  const onNavigate = useCallback((visitedUrl) => {
    logVisit(visitedUrl, numberOfItems);
  },[numberOfItems]);

  useEffect(() => {
    onNavigate(url);
  }, [url, onNavigate]); // if not passing onNavigate getting lint error

  // ...
}
Enter fullscreen mode Exit fullscreen mode

In this example, the Effect should re-run after a render when url changes (to log the new page visit), but it should not re-run when numberOfItems changes that creates a new ref for onNavigate causing new log for every numberOfItems up and down!

So we need a way to get a persistent ref for onNavigate function including the latest numberOfItems.

Now if you use new hook useEffectEvent instead of useCallback this problem gets solved:

function Page({ url }) {
  const { items } = useContext(ShoppingCartContext);
  const numberOfItems = items.length;

  const onNavigate = useEffectEvent((visitedUrl) => {
    logVisit(visitedUrl, numberOfItems);
  });

  useEffect(() => {
    onNavigate(url);
  }, [url]);

  // ...
}
Enter fullscreen mode Exit fullscreen mode

This problem already gets solved by another hook called useEventCallback by creating stable function reference.

Meet the Custom Champion: useEventCallback

This is what many of us have been building:

export function useEventCallback<T extends (...args: any[]) => any>(fn: T): T {
  const ref = useRef(fn)

  useEffect(() => {
    ref.current = fn
  })

  return useCallback((...args: any[]) => ref.current.apply(void 0, args), []) as T
}
Enter fullscreen mode Exit fullscreen mode

It's reliable, battle-tested, and works everywhere.

CAN We Now Swap Them?

Here's where it gets interesting. These hooks are not drop-in replacements for each other.

useEventCallback instead of useEffectEvent:

Yes you can, the result is almost the same, example:

function Page({ url }) {
  const { items } = useContext(ShoppingCartContext);
  const numberOfItems = items.length;

  const onNavigate = useEventCallback((visitedUrl) => {
    logVisit(visitedUrl, numberOfItems);
  });

  useEffect(() => {
    onNavigate(url);
  }, [url]);

  // ...
}
Enter fullscreen mode Exit fullscreen mode

But there are some caveats:

  1. React's internal magic > our clever hacks, so maybe we're missing some React optimizations
  2. The official API is designed for React's future - You need perfect Concurrent Mode compliance

useEffectEvent instead of useEventCallback:

βœ… You CAN use useEffectEvent when you need stable event handlers for Effect deps

// βœ… useEffectEvent works
const handleClick = useEventCallback(() => doSomething());
useEffect(() => { 
  handleClick() 
}, [])
Enter fullscreen mode Exit fullscreen mode

❌ You CANNOT use useEffectEvent when:

1. You need event handlers for JSX

// βœ… useEventCallback works
const handleClick = useEventCallback(() => doSomething());
return <button onClick={handleClick}>Click me</button>;

// ❌ useEffectEvent is NOT for this
Enter fullscreen mode Exit fullscreen mode

2. You're passing callbacks to child components

// βœ… Safe with useEventCallback
const onChildEvent = useEventCallback(handleChildEvent);
return <ChildComponent onEvent={onChildEvent} />;

// ❌ Don't pass useEffectEvent callbacks as props
Enter fullscreen mode Exit fullscreen mode

3. You need async operations

// βœ… useEventCallback handles async fine
const handleAsync = useEventCallback(async (id) => {
  const result = await fetchData(id);
  return result;
});

// ❌ useEffectEvent has restrictions with async
Enter fullscreen mode Exit fullscreen mode

My Take: Use Both (For Now)

Here's my practical recommendation:

  • Keep using useEventCallback for general stable function references, event handlers, and callback props
  • Adopt useEffectEvent specifically for functions called within effects

They serve different purposes, even though they solve similar problems.

The Migration Path

If you're on React 19.2+, start migrating effect-specific uses:

// Before
const logEvent = useEventCallback((data) => {
  analytics.track('event', data, currentUser.id);
});

useEffect(() => {
  logEvent(someData);
}, [someData]);

// After
const logEvent = useEffectEvent((data) => {
  analytics.track('event', data, currentUser.id);
});

useEffect(() => {
  logEvent(someData);
}, [someData]);
Enter fullscreen mode Exit fullscreen mode

But keep useEventCallback for everything else!

The Bottom Line

Don't feel bad about your custom hook - it's been doing great work! useEffectEvent isn't here to replace it entirely, but to provide a more specialized, optimized solution for a specific use case.

What's your experience been with these hooks? Are you planning to migrate, or sticking with your custom solutions? Drop a comment - I'd love to hear your thoughts!


P.S. - If you found this helpful, give it a clap! And if you have war stories about effect dependencies gone wrong, I'm all ears. We've all been there. πŸ˜…

Top comments (0)