DEV Community

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

Posted on • Edited 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!

Top comments (3)

Collapse
 
vitalets profile image
Vitaliy Potapov

Thanks for the article, I was also curious about the difference between useEffectEvent and useRef.current = callback. Btw, in the custom useEffectCallback why do you wrap .current assignment into useEffect()?

useEffect(() => {
    ref.current = fn
})
Enter fullscreen mode Exit fullscreen mode

instead of just:

ref.current = fn
Enter fullscreen mode Exit fullscreen mode
Collapse
 
andrewy0u profile image
Andrew P

Read or write ref.current during rendering is not something you should do.
See

Collapse
 
hash01 profile image
Hamid Shoja

Thanks for reading, Yeah ad @andrewy0u mentioned, If you do the assignment in render (outside of an effect) you are modifying during render, which is flagged as a violation

Some comments may only be visible to logged-in visitors. Sign in to view all comments.