Back to all notes

Abandon Render Pattern : Track Previous Values Without usePrevious

Update state during render when inputs change; React will abandon and restart the render.

1 min read

I used to reach for a usePrevious hook whenever I needed the previous value of some state. It works, but it adds extra state and extra renders. React actually allows setting state during render for this case: it abandons the in-progress render and restarts with the new state, no extra hook.

The usePrevious habit

The ref holds the last value, and an effect keeps it in sync with the current one:

import { useState, useRef, useEffect } from "react";

function usePrevious(value) {
  const ref = useRef(value);
  useEffect(() => {
    ref.current = value;
  }, [value]);
  return ref.current;
}

function App() {
  const [count, setCount] = useState(0);
  const prevCount = usePrevious(count);

  return (
    <div>
      <h1>Abandon Render Pattern</h1>
      <p>Current: {count}</p>
      <p>Previous: {prevCount}</p>
      <button onClick={() => setCount(count + 1)}>Increment</button>
    </div>
  );
}

export default App;

Set state during render

Same UI, one fewer hook. prevCount is plain state initialized from count, and during render the component checks whether count has moved:

import { useState } from "react";

function App() {
  const [count, setCount] = useState(0);
  const [prevCount, setPrevCount] = useState(count);

  if (count !== prevCount) {
    setPrevCount(count); // abandon render, restart with new prevCount
  }

  return (
    <div>
      <h1>Abandon Render Pattern</h1>
      <p>Current: {count}</p>
      <p>Previous: {prevCount}</p>
      <button onClick={() => setCount(count + 1)}>Increment</button>
    </div>
  );
}

export default App;

The initialization matters: prevCount starts as count, so they only diverge after an increment. When they diverge, the render calls setPrevCount(count), React abandons that render pass and restarts with the new state, and both displayed values agree. No ref, no effect.

What doesn’t belong here

These updates stay minimal and deterministic. Side effects don’t go in render; they belong in effects. If the only goal is logging, an effect keyed on the dependency is the cleaner tool.