Skip to the code
cf.

usePrevious

Write usePrevious(value) and export it from usePrevious.js. It returns what value was before it last changed, so a component can show both, like a tea's price going from 4 to 5.

On the first render there's no earlier value, so it returns undefined. When a render brings a new value, it returns the one before it.

Renders that don't change value don't count. If the component renders again for another reason, say the customer stars the tea, usePrevious keeps returning the same earlier value. Compare values with Object.is, as React does, so two different objects with the same contents count as a change.

function PriceTag({ price }) {
  const before = usePrevious(price);
  return (
    <p>
      Now {price}
      {before !== undefined && <span>, was {before}</span>}
    </p>
  );
}

The Preview shows Demo.jsx, a price tag that uses your hook.

What the tests check

  1. returns undefined on the first render
    const { result } = renderHook(() => usePrevious(4));
    expect(result.current).toBeUndefined();
    
  2. returns the old value after a change
    const { result, rerender } = renderHook(({ value }) => usePrevious(value), { initialProps: { value: 4 } });
    rerender({ value: 5 });
    expect(result.current).toBe(4);
    
  3. follows each change
    const { result, rerender } = renderHook(({ value }) => usePrevious(value), { initialProps: { value: 4 } });
    rerender({ value: 5 });
    rerender({ value: 6 });
    expect(result.current).toBe(5);
    rerender({ value: 4 });
    expect(result.current).toBe(6);
    
  4. stays undefined until the value first changes
    const { result, rerender } = renderHook(({ value }) => usePrevious(value), { initialProps: { value: 4 } });
    rerender({ value: 4 });
    rerender({ value: 4 });
    expect(result.current).toBeUndefined();
    
  5. keeps the old value through renders that don't change it
    const { result, rerender } = renderHook(({ value }) => usePrevious(value), { initialProps: { value: 4 } });
    rerender({ value: 5 });
    rerender({ value: 5 });
    rerender({ value: 5 });
    expect(result.current).toBe(4);
    
  6. compares objects by identity
    const first = { name: "Green" };
    const second = { name: "Green" };
    const { result, rerender } = renderHook(({ value }) => usePrevious(value), { initialProps: { value: first } });
    rerender({ value: second });
    expect(result.current).toBe(first);
    
  7. in a component, an unrelated update keeps the old price
    function PriceTag() {
      const [price, setPrice] = React.useState(4);
      const [starred, setStarred] = React.useState(false);
      const before = usePrevious(price);
      return (
        <div>
          <p>Price: {price}</p>
          <p>Before: {before === undefined ? "none" : before}</p>
          <button onClick={() => setPrice(price + 1)}>Raise the price</button>
          <button onClick={() => setStarred(!starred)}>{starred ? "Unstar" : "Star"}</button>
        </div>
      );
    }
    render(<PriceTag />);
    expect(screen.getByText("Before: none")).toBeInTheDocument();
    await user.click(screen.getByRole("button", { name: "Raise the price" }));
    expect(screen.getByText("Price: 5")).toBeInTheDocument();
    expect(screen.getByText("Before: 4")).toBeInTheDocument();
    await user.click(screen.getByRole("button", { name: "Star" }));
    expect(screen.getByText("Before: 4")).toBeInTheDocument();
    

Learn more

usePrevious.jsSaved in this browser

Run tests runs every test on your code, here in your browser. The problem is solved when they all pass.

Ctrl + Enter runs the tests (⌘ + Enter on a Mac).