π Published at: February 12, 2022
React 18 introduced two powerful hooksβuseTransition and useDeferredValueβthat help keep UIs responsive by preventing expensive renders from blocking urgent updates.
In this post, weβll explore:
β
How useTransition and useDeferredValue work
β
When to use them for better performance
β
Real-world examples: search inputs, filtering large lists, and more
π Why Does UI Lag Happen?
A common React problem is slow UI updates when performing expensive operations, such as:
- Filtering a large list
- Fetching & displaying API data
- Rendering complex UI components
Without optimization, each keystroke in a search input could cause noticeable lag because React is synchronously updating state and rendering all filtered items immediately.
πΉ React 18 introduces useTransition()
and useDeferredValue()
to solve this.
1οΈβ£ useTransition()
: Prioritizing UI Updates
What it does:
- Allows React to split updates into urgent vs. non-urgent.
- Keeps UI responsive by deferring slow updates until after urgent ones.
π Example: Search Input Without useTransition()
(Laggy UI)
Imagine a list of 10,000 items that updates on every keystroke:
import { useState } from "react";
function SearchList({ items }) {
const [query, setQuery] = useState("");
const [filteredItems, setFilteredItems] = useState(items);
const handleSearch = (e) => {
setQuery(e.target.value);
setFilteredItems(items.filter(item => item.includes(e.target.value)));
};
return (
<div>
<input type="text" value={query} onChange={handleSearch} placeholder="Search..." />
<ul>
{filteredItems.map((item, index) => <li key={index}>{item}</li>)}
</ul>
</div>
);
}
π΄ Problem: Every keystroke blocks the UI while filtering the list.
β
Optimized with useTransition()
(Smooth UI)
import { useState, useTransition } from "react";
function SearchList({ items }) {
const [query, setQuery] = useState("");
const [filteredItems, setFilteredItems] = useState(items);
const [isPending, startTransition] = useTransition();
const handleSearch = (e) => {
setQuery(e.target.value);
startTransition(() => {
setFilteredItems(items.filter(item => item.includes(e.target.value)));
});
};
return (
<div>
<input type="text" value={query} onChange={handleSearch} placeholder="Search..." />
{isPending && <p>Loading...</p>}
<ul>
{filteredItems.map((item, index) => <li key={index}>{item}</li>)}
</ul>
</div>
);
}
β How it helps:
- Keystrokes update immediately (high priority).
- Filtering runs in the background without blocking input.
- UI remains responsive, even with large data sets.
2οΈβ£ useDeferredValue()
: Deferring Expensive Computations
πΉ What it does:
- Tells React to use the “stale” value until an expensive update is ready.
- Unlike
useTransition()
, it doesnβt delay state updatesβit delays how they propagate to children.
π Example: Filtering a Large List Without useDeferredValue()
import { useState } from "react";
function SearchList({ items }) {
const [query, setQuery] = useState("");
// Expensive operation directly in render
const filteredItems = items.filter(item => item.includes(query));
return (
<div>
<input type="text" value={query} onChange={(e) => setQuery(e.target.value)} placeholder="Search..." />
<ul>
{filteredItems.map((item, index) => <li key={index}>{item}</li>)}
</ul>
</div>
);
}
π΄ Problem:
- Input lags on each keystroke as React recomputes the filtered list.
β
Optimized with useDeferredValue()
import { useState, useDeferredValue } from "react";
function SearchList({ items }) {
const [query, setQuery] = useState("");
const deferredQuery = useDeferredValue(query);
// Filtering only updates when deferredQuery changes
const filteredItems = items.filter(item => item.includes(deferredQuery));
return (
<div>
<input type="text" value={query} onChange={(e) => setQuery(e.target.value)} placeholder="Search..." />
<ul>
{filteredItems.map((item, index) => <li key={index}>{item}</li>)}
</ul>
</div>
);
}
β How it helps:
- Typing stays instant because filtering waits for deferredQuery to update.
- UI updates without lag, keeping input interactions smooth.
π When to Use useTransition()
vs. useDeferredValue()
?
Feature | useTransition() | useDeferredValue() |
---|---|---|
Type | Hook for deferring state updates | Hook for deferring computed values |
Purpose | Splits UI updates into urgent vs. non-urgent | Delays expensive calculations to avoid UI blocking |
Best Use Cases | Handling async tasks, filtering large lists, rendering complex UIs | Large data sets, reducing unnecessary renders |
π‘ Rule of thumb:
- Use
useTransition()
when updating state directly affects UI. - Use
useDeferredValue()
when an expensive computation depends on a state value.
π― Real-World Use Cases
β
Filtering large lists efficiently (like search bars).
β
Debouncing slow UI updates without extra state management.
β
Rendering animations smoothly while loading new content.
β
Optimizing expensive calculations in dashboards and charts.
π Final Thoughts
React 18βs useTransition() and useDeferredValue() give developers fine-grained control over rendering performance.
By prioritizing urgent updates and deferring expensive renders, you can:
βοΈ Eliminate UI lag
βοΈ Keep inputs fast & responsive
βοΈ Improve perceived app performance
π‘ Whatβs Next?
π Try implementing these hooks in your own project and see the difference!
π¬ Have you used useTransition()
or useDeferredValue()
yet? Let me know in the comments!