usePreviousDistinctLoading saved progress…

usePreviousDistinct

usePreviousDistinct(value, compare?) returns the last value that was actually different from the current one — not the value from the previous render. Renders where the value stayed the same are skipped, so the answer survives however many times the component rerenders in between.

That gap is the whole question. usePrevious hands back the previous render's value, which is the same thing right up until the component rerenders for a reason that has nothing to do with your value: a parent rerendered, an unrelated piece of state moved, a notification count ticked. One idle rerender later, "what was it before?" answers "the same thing it is now", and the change you were tracking is gone.

Signature

function usePreviousDistinct<T>(
  value: T,
  compare?: (previous: T | undefined, next: T) => boolean,
): T | undefined;

compare returns true when the two values count as equal — that is, when nothing changed. It defaults to Object.is.

Examples

You are animating a tab switch and need to know which panel to slide out:

function Tabs({ tab }) {
  const previousTab = usePreviousDistinct(tab);
  return <Panel from={previousTab} to={tab} />;
}
// tab:      inbox      inbox      archive  archive  spam
// render:   1          2          3        4        5
// returns:  undefined  undefined  inbox    inbox    archive

Renders 2 and 4 change nothing, so the answer does not move. usePrevious would return inbox, inbox, archive, archive for renders 2–5 — on renders 2 and 4 that is just the tab you are already on.

A comparator decides what "different" means:

// Two objects with the same id are the same tab, so this is not a change:
const previousTab = usePreviousDistinct(tab, (a, b) => a.id === b.id);
// tab:     {id: 1}    {id: 1}    {id: 2}
// returns: undefined  undefined  {id: 1}

Notes

  • Undefined until the first real change. The first render has nothing before it, and neither does the tenth render of a value that never moved.
  • An unchanged rerender must not overwrite the memory. This is the one behavior that separates this hook from usePrevious. Getting it right is the question.
  • compare(previous, next) returns true when they are EQUAL. Not "did it change" — the polarity is backwards from what most people guess, and it matches the react-use hook this mirrors.
  • The default comparator is Object.is. So objects and arrays are compared by reference: a rebuilt object with identical contents is a change.
  • Falsy values are real values. 0, '', false, null and undefined are all values to remember, not stand-ins for "nothing".
  • Nothing re-renders. Reading the previous value must not schedule a render of its own.
Loading editor…