30% offEnding soon
useTrackedEffectLoading saved progress…

useTrackedEffect

useTrackedEffect is a useEffect that also tells its effect which dependencies changed since the last run. A plain useEffect re-runs when any dependency changes but never says which one, so when an effect fires more often than you expected you are left adding console.logs to find the culprit. This hook hands the effect the indices of the deps that actually moved, making it a debugging tool for "why did this effect just run?" — the runtime answer to a question you would otherwise chase by hand.

Signature

function useTrackedEffect(
  effect: (
    changes: number[],              // indices of deps that changed since last run
    previousDeps: unknown[] | undefined, // last run's deps (undefined on first run)
    currentDeps: unknown[],         // this run's deps
  ) => void | (() => void),         // may return a cleanup, like useEffect
  deps: unknown[],
): void;

Examples

// Log which dependency triggered a refetch.
useTrackedEffect(
  (changes) => {
    console.log('effect ran; changed indices:', changes); // e.g. [1]
    fetchResults(userId, filters, sort, page);
  },
  [userId, filters, sort, page],
);
// On the first run there is no previous list, so `changes` is every index
// and `previousDeps` is undefined.
useTrackedEffect((changes, previousDeps) => {
  if (previousDeps === undefined) console.log('mount:', changes); // [0, 1]
}, [a, b]);

Notes

  • Same trigger as useEffect — the effect fires on exactly the same schedule (any dep changed by Object.is); you only add the changed-indices report on top. You are augmenting the effect, not changing when it runs.
  • changes is indices, not values — an array of the positions in deps that differ from the previous run, in ascending order. Read them against your own deps array to name them.
  • First run — the effect runs on mount with changes equal to every index and previousDeps equal to undefined; there is no previous list to diff against.
  • Cleanup works — if the effect returns a function, it runs before the next effect and on unmount, exactly like useEffect.
  • Object deps report changed every render — a fresh object or array literal is a new reference each render, so Object.is marks it changed every time; that is the same rule useEffect follows, not a quirk of this hook.
  • The named export is useTrackedEffect — keep it, and keep the effect(changes, previousDeps, currentDeps) argument order.