Concurrent React: The Magic of Doing Many Things at Once
The Traffic Controller Analogy
Imagine you’re a traffic controller at a busy intersection. Cars (tasks) are coming from all directions. Some cars are ambulances (urgent updates) that need to go through immediately. Other cars are delivery trucks (slower updates) that can wait a bit.
Concurrent React is like being a super-smart traffic controller. It helps React decide: “Which updates should go first? Which ones can wait?”
1. useId: Giving Everyone a Unique Name Tag
The Story
You’re organizing a school party. Every kid needs a name tag so teachers can find them easily. But here’s the tricky part—some kids signed up online (server), and some signed up at the door (client). You need to make sure no two kids get the same name tag!
What useId Does
useId creates a unique ID that stays the same whether your code runs on the server or in the browser.
import { useId } from 'react';
function EmailField() {
const id = useId();
return (
<div>
<label htmlFor={id}>Email:</label>
<input id={id} type="email" />
</div>
);
}
Why It Matters for Accessibility
Screen readers (tools that help blind people use computers) need these IDs to connect labels to inputs. When a blind person clicks a label, the screen reader knows which input to focus on!
Before useId: You’d write id="email-1" and hope it didn’t clash with another id="email-1" somewhere else.
After useId: React guarantees uniqueness. Magic!
2. Server-Safe IDs: Same Name Tag, Two Worlds
The Story
Imagine you have a twin who lives in another city (the server). When someone asks your twin your name, and then asks you (on the client), you both need to say the exact same name. Otherwise, people get confused!
The Problem
Server says: "Your ID is abc123"
Browser says: "Your ID is xyz789"
React: "Wait, these don't match!" 💥
This causes a hydration mismatch—React gets confused when the server-rendered HTML doesn’t match what the browser creates.
The Solution
useId creates IDs using a special formula that works the same way on both server and client:
function Form() {
const nameId = useId();
const emailId = useId();
// Both IDs will be identical on server AND client!
return (
<form>
<label htmlFor={nameId}>Name</label>
<input id={nameId} />
<label htmlFor={emailId}>Email</label>
<input id={emailId} />
</form>
);
}
Simple Rule: Never use Math.random() or Date.now() for IDs in React. They’ll be different on server vs client!
3. useTransition: The “Please Wait” Button
The Story
You’re at a restaurant. You order a fancy dish that takes 10 minutes to cook. Do you:
A) Stare at the kitchen door, unable to move? 😬 B) Sip your drink and chat while waiting? 😊
useTransition lets you pick option B!
What It Does
It marks some updates as “not urgent” so React can keep the app responsive while working on them.
import { useTransition, useState } from 'react';
function SearchPage() {
const [query, setQuery] = useState('');
const [results, setResults] = useState([]);
const [isPending, startTransition] = useTransition();
function handleSearch(text) {
// This updates immediately (typing)
setQuery(text);
// This can wait (searching)
startTransition(() => {
const filtered = searchDatabase(text);
setResults(filtered);
});
}
return (
<div>
<input
value={query}
onChange={(e) => handleSearch(e.target.value)}
/>
{isPending && <p>Searching...</p>}
<ResultsList results={results} />
</div>
);
}
The Magic
- Typing stays snappy: Your keystrokes appear instantly
- Search happens in background: No freezing!
- isPending tells you: “Hey, I’m still working on that slow stuff”
4. useDeferredValue: The Lazy Copy
The Story
You have a magic mirror that shows a slightly delayed reflection. When you move fast, the mirror shows where you were a moment ago. But when you stop, it catches up!
What It Does
useDeferredValue creates a delayed version of a value. React shows the old value while computing the new one.
import { useDeferredValue, useState, memo } from 'react';
function ProductList() {
const [filter, setFilter] = useState('');
const deferredFilter = useDeferredValue(filter);
// Shows if we're showing stale data
const isStale = filter !== deferredFilter;
return (
<div>
<input
value={filter}
onChange={(e) => setFilter(e.target.value)}
placeholder="Filter products..."
/>
<div style={{ opacity: isStale ? 0.5 : 1 }}>
<SlowProductGrid filter={deferredFilter} />
</div>
</div>
);
}
useTransition vs useDeferredValue
| useTransition | useDeferredValue |
|---|---|
| You control when to update | You control what gets delayed |
Wraps the setState call |
Wraps the value itself |
| Use when you own the state | Use when you receive props |
5. startTransition: The Global Traffic Light
The Story
useTransition is like having a traffic light at your own intersection. But what if you need to control traffic from outside the car?
startTransition is the remote control version—you can use it anywhere, not just inside components!
When to Use It
import { startTransition } from 'react';
// In a regular function (not a component)
function handleTabClick(setTab, newTab) {
startTransition(() => {
setTab(newTab);
});
}
// In an event handler
button.addEventListener('click', () => {
startTransition(() => {
updateBigList(newData);
});
});
The Difference
// useTransition - inside components, gives you isPending
const [isPending, startTransition] = useTransition();
// startTransition - anywhere, no isPending
import { startTransition } from 'react';
startTransition(() => { /* ... */ });
Pro tip: If you need to show a loading spinner, use useTransition. If you just want the update to be interruptible, use plain startTransition.
6. Transition Priorities: Who Goes First?
The Story
In a hospital emergency room, patients aren’t seen in order of arrival. A heart attack (urgent) goes before a sprained ankle (can wait).
React works the same way with updates!
The Priority Ladder
graph TD A["Discrete Events"] --> B["Click, Type, Key Press"] B --> C["Highest Priority"] D["Continuous Events"] --> E["Scroll, Drag, Mouse Move"] E --> F["High Priority"] G["Default Updates"] --> H["Regular setState"] H --> I["Normal Priority"] J["Transitions"] --> K["startTransition wrapped"] K --> L["Low Priority - Can be interrupted!"]
How It Works
function App() {
const [text, setText] = useState('');
const [list, setList] = useState([]);
function handleType(e) {
// HIGH priority - happens immediately
setText(e.target.value);
// LOW priority - can wait
startTransition(() => {
setList(generateBigList(e.target.value));
});
}
return (
<div>
<input value={text} onChange={handleType} />
<BigList items={list} />
</div>
);
}
What happens when you type “abc” fast:
- “a” → text updates to “a”, list computation starts
- “b” → text updates to “ab”, list for “a” is interrupted, starts for “ab”
- “c” → text updates to “abc”, list for “ab” is interrupted, starts for “abc”
- You stop → list for “abc” finishes and displays
7. Suspense Internals: The Magic Behind the Curtain
The Story
Remember waiting for a video to buffer? You see a loading spinner, then suddenly—the video plays! Suspense is React’s way of showing loading spinners automatically.
How Suspense Works Inside
graph TD A["Component Renders"] --> B{Data Ready?} B -->|Yes| C["Show Component"] B -->|No| D["Throw Promise"] D --> E["Suspense Catches It"] E --> F["Show Fallback"] F --> G["Promise Resolves"] G --> A
The Three Players
1. Suspense Boundary - The catcher
<Suspense fallback={<Loading />}>
<Comments />
</Suspense>
2. The Thrower - A component that “throws” a promise
function Comments() {
// If data isn't ready, this "throws" a promise
const comments = use(fetchComments());
return <CommentList data={comments} />;
}
3. The Fallback - What shows while waiting
<div className="spinner">Loading comments...</div>
Suspense + Transitions = Best Friends
function App() {
const [tab, setTab] = useState('home');
const [isPending, startTransition] = useTransition();
function switchTab(newTab) {
startTransition(() => {
setTab(newTab);
});
}
return (
<div>
<Tabs current={tab} onChange={switchTab} />
<div style={{ opacity: isPending ? 0.7 : 1 }}>
<Suspense fallback={<Skeleton />}>
{tab === 'home' && <Home />}
{tab === 'profile' && <Profile />}
</Suspense>
</div>
</div>
);
}
Without transition: Click tab → instant blank → loading spinner → content
With transition: Click tab → old content stays (slightly faded) → new content appears
The Big Picture
graph TD A["User Action"] --> B{Urgent?} B -->|Yes| C["Update Immediately"] B -->|No| D["Wrap in Transition"] D --> E["Can Be Interrupted"] E --> F["Show Old While Computing New"] F --> G["User Stays Happy!"] C --> G
Quick Reference
| Tool | What It Does | When to Use |
|---|---|---|
useId |
Creates unique, server-safe IDs | Forms, accessibility |
useTransition |
Makes updates interruptible + gives isPending | Heavy UI updates you control |
useDeferredValue |
Creates a delayed copy of a value | Expensive child components |
startTransition |
Makes any update interruptible | Outside components, libraries |
| Suspense | Shows fallback while loading | Data fetching, code splitting |
You Did It!
You now understand how React juggles multiple tasks without dropping any! Think of these tools as your superpowers for building apps that feel fast, even when doing heavy work.
Remember: The user’s typing, clicking, and scrolling should NEVER freeze. Use transitions to keep everything smooth!
