useDeferredValue, and the list that stops fighting the input

The same mechanism as a transition, applied to a value rather than to an update, which is what a component receiving a prop needs.

function Results({ query }) {
  const deferred = useDeferredValue(query)
  const stale = query !== deferred

  return (
    <div style={{ opacity: stale ? 0.6 : 1 }}>
      <ExpensiveList query={deferred} />
    </div>
  )
}

// and the memo that makes it work: without it, the child
// re-renders anyway and the deferral buys nothing.
const ExpensiveList = memo(function ExpensiveList({ query }) { ... })

The memo is not optional and is the part that gets left out — deferring a value only helps if the expensive child skips rendering when the value has not changed. Comparing the current and deferred values gives a stale flag for free, which is a better loading indicator than a spinner because the previous content stays readable.