useEffectEvent in React

Essential Do's and Don'ts Guide

What is useEffectEvent?

useEffectEvent is an experimental React Hook that allows you to extract non-reactive logic from Effects. It solves the common problem where you need to read the latest value of props or state inside an Effect without causing that Effect to re-run when those values change.

Key Concept: useEffectEvent creates a stable function reference that always reads the latest values, but doesn't trigger Effect re-execution when those values change.

The Problem It Solves

Consider a chat application where you want to log a message when the room changes, but you need to include the current theme in your log. Traditionally, you'd need to add theme to the dependency array, causing the Effect to re-run every time the theme changes, even though you only want to react to room changes.

❌ Traditional Problem:
useEffect(() => {
  logVisit(roomId, theme); // Re-runs when theme changes
}, [roomId, theme]); // Had to include theme!
✅ With useEffectEvent:
const onVisit = useEffectEvent((roomId) => {
  logVisit(roomId, theme); // Reads latest theme
});

useEffect(() => {
  onVisit(roomId); // Only re-runs when roomId changes
}, [roomId]);

Do's and Don'ts

✓ DO

  • Use it to read the latest props/state without adding them to dependencies
  • Call it directly from inside Effects
  • Use it for event handlers that need to access Effect context
  • Use it to separate reactive and non-reactive logic
  • Call it synchronously within your Effect

✗ DON'T

  • Don't call it from regular event handlers
  • Don't call it during rendering
  • Don't pass it as a prop to components
  • Don't call it asynchronously or after a delay
  • Don't use it as a replacement for proper memoization

Detailed Examples

✅ DO: Extract Non-Reactive Logic

function ChatRoom({ roomId, theme }) {
  const onConnected = useEffectEvent(() => {
    showNotification('Connected!', theme);
  });

  useEffect(() => {
    const connection = createConnection(roomId);
    connection.on('connected', onConnected);
    connection.connect();
    return () => connection.disconnect();
  }, [roomId]); // Only roomId is reactive
}

This Effect only reconnects when roomId changes, but onConnected always uses the latest theme.

❌ DON'T: Call from Event Handlers

function Component() {
  const onClick = useEffectEvent(() => {
    // ❌ Wrong! Don't use in event handlers
    doSomething();
  });

  return <button onClick={onClick}>Click</button>;
}

Use regular functions or useCallback for event handlers instead.

✅ DO: Reading Latest Props in Effects

function Timer({ interval, onTick }) {
  const onTickEvent = useEffectEvent(() => {
    onTick(); // Always calls latest onTick
  });

  useEffect(() => {
    const id = setInterval(onTickEvent, interval);
    return () => clearInterval(id);
  }, [interval]); // Only interval is reactive
}

❌ DON'T: Call Asynchronously

function Component() {
  const onData = useEffectEvent((data) => {
    processData(data);
  });

  useEffect(() => {
    fetchData().then(data => {
      onData(data); // ❌ Risky! Called asynchronously
    });
  }, []);
}

The function might be called after the component unmounts or after values have changed.

✅ DO: Combine with Cleanup Logic

function Analytics({ userId, page }) {
  const logPageView = useEffectEvent(() => {
    analytics.track('page_view', { userId, page });
  });

  useEffect(() => {
    logPageView();
    
    return () => {
      // Cleanup can also use Effect Events
      const logPageExit = useEffectEvent(() => {
        analytics.track('page_exit', { userId, page });
      });
      logPageExit();
    };
  }, []); // Empty deps - runs once per mount
}

❌ DON'T: Pass as Dependencies to Other Hooks

function Component() {
  const onEvent = useEffectEvent(() => {
    doSomething();
  });

  // ❌ Don't do this
  const memoized = useMemo(() => {
    return onEvent();
  }, [onEvent]);
}

Common Use Cases

1. Logging and Analytics

When you need to log events with the latest user preferences or settings without re-subscribing.

const logEvent = useEffectEvent((eventName) => {
  analytics.log(eventName, { theme, locale, userId });
});

useEffect(() => {
  logEvent('page_visit');
}, [pathname]); // Only react to pathname changes

2. Callbacks with Latest State

When passing callbacks to third-party libraries that shouldn't cause re-subscriptions.

const onMessage = useEffectEvent((msg) => {
  showToast(msg, { variant: userPreference });
});

useEffect(() => {
  const unsubscribe = messageService.subscribe(onMessage);
  return unsubscribe;
}, []); // Subscribe once, callback uses latest userPreference

3. Debouncing with Latest Values

When implementing debouncing while always using the latest callback logic.

const onSearch = useEffectEvent(() => {
  performSearch(query, filters, sortBy);
});

useEffect(() => {
  const timeoutId = setTimeout(onSearch, 500);
  return () => clearTimeout(timeoutId);
}, [query]); // Debounce query, but use latest filters/sortBy

Best Practices

  • Only use useEffectEvent when you've identified a genuine need to read non-reactive values
  • Consider if your logic really belongs in an Effect or if it could be in an event handler
  • Keep the Event function focused on a single purpose
  • Document why you're using it to help future maintainers
  • Wait for stable release before using in production applications

Migration Path

If you're currently using useCallback with constantly changing dependencies or suppressing the linter with eslint-disable comments, useEffectEvent might be the solution you need. However, first consider if restructuring your component logic might be more appropriate.

Conclusion

useEffectEvent is a powerful tool for solving the challenge of reading the latest values in Effects without causing unnecessary re-executions. By following these do's and don'ts, you'll be able to use it effectively when it becomes stable. Remember that it's meant for specific scenarios where you need to separate reactive from non-reactive logic within Effects.