Separating the Urgent from the Non-Urgent

useTransition and useDeferredValue are for a specific class of problem — big, obvious, and unmissable. Here's the mental model for reaching for them.

March 22, 20264 min read1 / 5

Pushing state down and memoization handle the everyday performance friction — the small re-renders that add up. They're the tools you reach for regularly.

There's a different category of performance problem that you encounter far less often but can't miss when it shows up. It's not subtle. It's the one place in the app where everything freezes. The input lags. Hover states stop responding. The whole UI grinds while something expensive catches up.

The React team sometimes describes these as the "Snorlax" problems — the big blockers sitting in the middle of the path. You can't sneak around them. The only way through is to handle them directly.

The Problem: Everything Is the Same Priority

By default, React treats all state updates as equal. When the user types in a search box, the state update that records the keypress and the state update that triggers a 500ms filter operation over thousands of records — same priority. React processes both before painting anything to the screen.

That's fine when everything is fast. It becomes a problem when one of those updates is slow. The fast thing (updating the input to show the character the user just typed) gets held up waiting for the slow thing (re-rendering the filtered results list). The user sees lag.

The underlying mechanism for fixing this is React's lane system — Lanes explains how React uses priority queues to schedule work. The user-facing tools that tap into this system are useTransition and useDeferredValue.

The Mental Model: Urgent vs Deferrable

Every time a user does something, they expect a reaction — immediately. That's what makes user input feel urgent. If they type a character, they want to see that character appear in the input field right now.

The expensive downstream work — filtering a list, re-rendering a chart, updating a complex tree — that's deferrable. The user wants it eventually. But if they have to choose between "input feels snappy" and "results update a few milliseconds later," they'll take the snappy input every time.

The idea: tell React explicitly which updates are non-urgent, and it will process the urgent ones first. The user's interaction stays responsive while the expensive work runs in the background, in lower-priority lanes.

You don't make expensive things faster. You make them not block the things that need to be immediate.

Two Hooks, Two Situations

React gives you two hooks for this, and they solve slightly different versions of the problem.

useTransition (and startTransition)

useTransition gives you a way to mark a state update as non-urgent. You control when the state is set, and you wrap that update in a transition:

TSX
const [isPending, startTransition] = useTransition(); // The filter query updates immediately (urgent) setQuery(newValue); // The expensive filtering runs as a low-priority transition startTransition(() => { setFilter(newValue); });

The user sees query update instantly — the input reflects their typing right away. filter updates too, but React is allowed to defer and interrupt that work in favour of more urgent updates. If the user keeps typing, React can cancel the previous transition and start a new one.

isPending is a boolean you can use to show a loading indicator while the transition is in progress — without blocking the UI.

useDeferredValue

useDeferredValue solves a similar problem from a different angle. Instead of wrapping the state update, it wraps the value itself:

TSX
const deferredFilter = useDeferredValue(filter); // deferredFilter lags slightly behind filter // Expensive computation only re-runs when deferredFilter changes const results = expensiveFilter(items, deferredFilter);

The original filter value updates immediately. deferredFilter follows when React has spare capacity. Any expensive work downstream of deferredFilter runs at lower priority.

The key difference from useTransition: you use useDeferredValue when you can't control where the state is set — the state update happens somewhere else (in a parent, in a library, in a prop you receive), and you only have access to the resulting value.

When to Reach for Which

A practical heuristic: reach for useTransition first.

If you own the code that sets the state, useTransition gives you the cleanest expression of intent: this update is a transition, not an urgent reaction. It also gives you isPending so you can show feedback to the user.

If the state is set somewhere you don't control — a prop passed down from a parent, a value from a library, something in a context — then useDeferredValue is your tool.

Both hooks tap into the same React lane mechanism. The difference is just where in the data flow you intercept.

Not an Everyday Tool

I want to be honest about frequency: these hooks are not things you reach for in regular development. Most state updates are fine. Most computations finish well within a frame.

But when you have that one thing — the big filterable list with thousands of items, the interactive chart that takes 200ms to re-render, the search that updates a deeply nested tree on every keystroke — and you can feel the UI getting sticky, that's when these hooks are exactly the right answer.

The goal isn't to add transitions everywhere. It's to recognise the pattern when it appears and know the tool exists.

The next posts go hands-on with both hooks — seeing useTransition and useDeferredValue in action with real before/after comparisons.

Practice what you just read.

Urgent vs Non-Urgent: Seeing Lane Priority in Action
1 exercise

Enjoyed this? Get more like it.

Deep dives on system design, React, web development, and personal finance — straight to your inbox. Free, always.