Back to all notes

useEffectEvent : Read Latest Values in Effects Without Re-running Them

Extract non-reactive logic from effects so you control when they re-run.

2 min read

Introduction

useEffectEvent lets you extract non‑reactive logic out of effects so you can access the latest values of props or state without forcing the effect to re‑run.

Why this matters

Sometimes you want an effect to run only when a specific dependency changes, but still access the latest values of other variables. useEffectEvent lets you define a stable function that reads the latest values without adding them to the dependency array.

The problem

If you include every value in an effect’s dependency array, it re‑runs too often. If you exclude values, the effect reads stale data.

Inefficient approach

Either stale reads or noisy re‑runs:

import { useState, useEffect } from "react";

function trackPageView(url, itemCount) {
  console.log(`Page view: ${url}, Items: ${itemCount}`);
}

function Page({ url }) {
  const [itemCount, setItemCount] = useState(0);

  // Re-runs on every itemCount change, even though we only care on navigation
  useEffect(() => {
    trackPageView(url, itemCount);
  }, [url, itemCount]);

  return (
    <div>
      <h1>Page: {url}</h1>
      <p>Item Count: {itemCount}</p>
      <button onClick={() => setItemCount(itemCount + 1)}>Increment</button>
    </div>
  );
}

function App() {
  return (
    <div>
      <h1>useEffectEvent Pattern</h1>
      <Page url="/home" />
    </div>
  );
}

export default App;

The solution

Wrap the analytics call in an effect event and invoke it from an effect keyed by url.

import { useState, useEffect } from "react";

function trackPageView(url, itemCount) {
  console.log(`Page view: ${url}, Items: ${itemCount}`);
}

// Simplified useEffectEvent implementation
function useEffectEvent(callback) {
  const callbackRef = { current: callback };
  const stableCallback = (...args) => callbackRef.current(...args);
  stableCallback._update = (newCallback) => {
    callbackRef.current = newCallback;
  };
  return stableCallback;
}

function Page({ url }) {
  const [itemCount, setItemCount] = useState(0);

  const onVisit = useEffectEvent((visitedUrl) => {
    trackPageView(visitedUrl, itemCount);
  });

  // Update the callback ref on every render
  useEffect(() => {
    if (onVisit._update) {
      onVisit._update((visitedUrl) => trackPageView(visitedUrl, itemCount));
    }
  });

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

  return (
    <div>
      <h1>Page: {url}</h1>
      <p>Item Count: {itemCount}</p>
      <button onClick={() => setItemCount(itemCount + 1)}>Increment</button>
    </div>
  );
}

function App() {
  return (
    <div>
      <h1>useEffectEvent Pattern</h1>
      <Page url="/home" />
    </div>
  );
}

export default App;

Step-by-step

  1. Create a stable effect event with useEffectEvent that reads latest values.
  2. Inside it, call the action (trackPageView) with current data.
  3. In a separate useEffect, call the effect event when the true trigger changes (url).
  4. Remove unrelated values from the dependency array.

Tips

  • Keep effect events small and pure—just read latest values and perform the action.
  • Don’t add effect‑event functions to dependency arrays; they’re intentionally stable.
  • If you can derive values during render, prefer deriving over effect logic.

Knowledge base

Problem snippet:

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

Solution snippet:

const onVisit = useEffectEvent((visitedUrl) => trackPageView(visitedUrl, itemCount));
useEffect(() => {
  onVisit(url);
}, [url]);