useTransition is a React hook that allows you to mark state updates as transitions, which helps improve user experience by keeping the UI responsive during expensive operations. It separates urgent updates (like typing) from non-urgent updates (like filtering a list).
Syntax: const [isPending, startTransition] = useTransition();
Key features:
Usage example:
function SearchComponent() {
const [query, setQuery] = useState('');
const [isPending, startTransition] = useTransition();
const handleSearch = (e) => {
const value = e.target.value;
setQuery(value);
startTransition(() => {
// Expensive filtering operation
setSearchResults(filterData(value));
});
};
return (
<div>
<input onChange={handleSearch} value={query} />
{isPending && <div>Loading...</div>}
</div>
);
}
The useTransition
hook is one of React’s built-in hooks designed to improve user experience by marking state updates as transitions. It helps keep the UI responsive during expensive operations by separating urgent updates (like user input) from non-urgent updates (like filtering or sorting data). 🚀
useTransition
returns an array with two values:
import React, { useState, useTransition } from 'react';
function Component() {
const [isPending, startTransition] = useTransition();
const [data, setData] = useState([]);
const handleFilter = (filterValue) => {
// Urgent update - immediate feedback
// setFilter(filterValue);
// Non-urgent update - wrapped in transition
startTransition(() => {
setData(filterExpensiveData(filterValue));
});
};
return (
<div>
{isPending ? <div>Updating...</div> : <DataList data={data} />}
</div>
);
}
When startTransition is called:
function SearchableList({ items }) {
const [query, setQuery] = useState('');
const [filteredItems, setFilteredItems] = useState(items);
const [isPending, startTransition] = useTransition();
const handleSearch = (e) => {
const value = e.target.value;
setQuery(value);
// Keep typing responsive while filtering happens in background
startTransition(() => {
setFilteredItems(
items.filter(item =>
item.name.toLowerCase().includes(value.toLowerCase())
)
);
});
};
return (
<div>
<input
value={query}
onChange={handleSearch}
placeholder="Search items..."
/>
{isPending && <div>Searching...</div>}
<ItemList items={filteredItems} />
</div>
);
}
function TabComponent() {
const [tab, setTab] = useState('home');
const [isPending, startTransition] = useTransition();
const switchTab = (newTab) => {
// Immediate visual feedback
startTransition(() => {
// Expensive tab content rendering
setTab(newTab);
});
};
return (
<div>
<nav>
<button onClick={() => switchTab('home')}>Home</button>
<button onClick={() => switchTab('profile')}>Profile</button>
<button onClick={() => switchTab('settings')}>Settings</button>
</nav>
{isPending && <div>Loading tab...</div>}
<TabContent tab={tab} />
</div>
);
}
function DataFilter({ largeDataset }) {
const [filter, setFilter] = useState('');
const [isPending, startTransition] = useTransition();
const [filteredData, setFilteredData] = useState(largeDataset);
const applyFilter = (newFilter) => {
setFilter(newFilter);
startTransition(() => {
const result = largeDataset.filter(item =>
item.category.includes(newFilter)
);
setFilteredData(result);
});
};
return (
<div>
<input
value={filter}
onChange={(e) => applyFilter(e.target.value)}
placeholder="Filter by category..."
/>
{isPending ? <div>Filtering...</div> : <DataGrid data={filteredData} />}
</div>
);
}
✅ Use useTransition when:
function ExpensiveOperationComponent({ data }) {
const [searchTerm, setSearchTerm] = useState('');
const [isPending, startTransition] = useTransition();
const handleSearch = (term) => {
setSearchTerm(term);
// Keep UI responsive while searching
startTransition(() => {
performExpensiveSearch(data, term);
});
};
return (
<div>
<SearchInput onSearch={handleSearch} />
{isPending && <LoadingIndicator />}
<Results />
</div>
);
}
❌ Avoid useTransition when:
// ❌ Don't use useTransition unnecessarily
function SimpleToggle() {
const [isVisible, setIsVisible] = useState(true);
const [, startTransition] = useTransition(); // Overkill!
const toggle = () => {
startTransition(() => {
setIsVisible(!isVisible);
});
};
return <button onClick={toggle}>{isVisible ? 'Hide' : 'Show'}</button>;
}
// ✅ Just use regular state update
function SimpleToggle() {
const [isVisible, setIsVisible] = useState(true);
const toggle = () => {
setIsVisible(!isVisible);
};
return <button onClick={toggle}>{isVisible ? 'Hide' : 'Show'}</button>;
}
// ❌ Overuse
function Component() {
const [count, setCount] = useState(0);
const [, startTransition] = useTransition();
const increment = () => {
startTransition(() => {
setCount(c => c + 1); // Simple increment doesn't need transition
});
};
return <button onClick={increment}>Count: {count}</button>;
}
// ✅ Just use regular state update
function Component() {
const [count, setCount] = useState(0);
const increment = () => {
setCount(c => c + 1);
};
return <button onClick={increment}>Count: {count}</button>;
}
// ❌ Error: wrapping urgent updates
function SearchComponent() {
const [query, setQuery] = useState('');
const [, startTransition] = useTransition();
const handleSearch = (e) => {
const value = e.target.value;
startTransition(() => {
setQuery(value); // Should be immediate for typing feedback
});
};
return <input value={query} onChange={handleSearch} />;
}
// ✅ Correctly: separate urgent and non-urgent updates
function SearchComponent() {
const [query, setQuery] = useState('');
const [results, setResults] = useState([]);
const [, startTransition] = useTransition();
const handleSearch = (e) => {
const value = e.target.value;
// Urgent update for immediate typing feedback
setQuery(value);
// Non-urgent update for expensive filtering
startTransition(() => {
setResults(filterResults(value));
});
};
return <input value={query} onChange={handleSearch} />;
}
// ❌ No feedback during transition
function Component() {
const [data, setData] = useState([]);
const [isPending, startTransition] = useTransition();
const loadData = () => {
startTransition(() => {
setData(fetchExpensiveData());
});
};
return (
<div>
<button onClick={loadData}>Load Data</button>
{/* User doesn't know operation is in progress */}
<DataDisplay data={data} />
</div>
);
}
// ✅ Provide visual feedback
function Component() {
const [data, setData] = useState([]);
const [isPending, startTransition] = useTransition();
const loadData = () => {
startTransition(() => {
setData(fetchExpensiveData());
});
};
return (
<div>
<button onClick={loadData}>Load Data</button>
{isPending && <div className="loading">Loading...</div>}
<DataDisplay data={data} />
</div>
);
}
✅ useTransition is a React hook for:
✅ When to use:
❌ When to avoid:
✅ Best practices:
useTransition is a powerful tool for improving user experience, but it should be used thoughtfully. Identify real performance bottlenecks and apply useTransition to solve them. 🚀
Want more articles to prepare for interviews? Subscribe to EasyAdvice, bookmark the site and improve yourself every day 💪