Для чего нужен useTransition?
Комментарии (1)
Ответ сгенерирован нейросетью и может содержать ошибки
Для чего нужен useTransition?
useTransition - это React хук (введён в React 18), который позволяет отметить обновление состояния как неблокирующее (non-urgent). Хук предоставляет способ приоритизировать обновления: срочные обновления (клики, вводы) получают высокий приоритет, а менее срочные обновления (асинхронные операции, рендеринг больших списков) получают низкий приоритет. Это значительно улучшает отзывчивость приложения.
Проблема, которую решает useTransition
Блокирующие обновления замораживают интерфейс
import { useState } from 'react';
function SearchUsers() {
const [query, setQuery] = useState('');
const [results, setResults] = useState([]);
const handleChange = (e) => {
const value = e.target.value;
setQuery(value); // Это синхронное обновление
// Если results содержит 10000 элементов, рендеринг блокирует UI
const filtered = mockDatabase.filter(user =>
user.name.includes(value)
);
setResults(filtered);
};
return (
<div>
<input value={query} onChange={handleChange} />
{/* Во время рендеринга большого списка input становится неотзывчивым! */}
<ul>
{results.map(user => <li key={user.id}>{user.name}</li>)}
</ul>
</div>
);
}
Проблема: если список результатов большой, рендеринг может занять несколько сотен миллисекунд, и пользователь будет чувствовать "зависание" при вводе.
Как работает useTransition
import { useState, useTransition } from 'react';
function SearchUsers() {
const [query, setQuery] = useState('');
const [results, setResults] = useState([]);
const [isPending, startTransition] = useTransition();
const handleChange = (e) => {
const value = e.target.value;
// Это обновление срочное - выполняется немедленно
setQuery(value);
// Это обновление неблокирующее - может быть прервано
startTransition(() => {
const filtered = mockDatabase.filter(user =>
user.name.includes(value)
);
setResults(filtered);
});
};
return (
<div>
<input
value={query}
onChange={handleChange}
placeholder="Search users..."
/>
{/* Показываем spinner пока идёт обновление */}
{isPending && <p>Loading results...</p>}
<ul>
{results.map(user => <li key={user.id}>{user.name}</li>)}
</ul>
</div>
);
}
Теперь при вводе:
- Input обновляется сразу (высокий приоритет)
- Фильтрация результатов происходит в фоне (низкий приоритет)
- UI остаётся отзывчивым
isPendingуказывает, что обновление в процессе
Структура useTransition
Хук возвращает массив с двумя элементами:
const [isPending, startTransition] = useTransition();
// isPending (boolean) - true когда transition обновление в процессе
// startTransition (function) - функция для запуска transition обновления
Практический пример 1: Поиск в большом списке
import { useState, useTransition } from 'react';
function UserDirectory() {
const [input, setInput] = useState('');
const [deferredValue, setDeferredValue] = useState('');
const [isPending, startTransition] = useTransition();
const handleInputChange = (e) => {
const value = e.target.value;
// Срочное обновление - input реагирует сразу
setInput(value);
// Отложенное обновление - поиск происходит в фоне
startTransition(() => {
setDeferredValue(value);
});
};
// Фильтруем по отложенному значению
const filteredUsers = useMemo(() => {
console.log('Filtering users...');
return allUsers.filter(user =>
user.name.toLowerCase().includes(deferredValue.toLowerCase())
);
}, [deferredValue]);
return (
<div>
<input
type="text"
value={input}
onChange={handleInputChange}
placeholder="Search users..."
/>
{isPending && <div className="spinner">Searching...</div>}
<div className={isPending ? 'opacity-50' : ''}>
{filteredUsers.map(user => (
<div key={user.id} className="user-card">
<h3>{user.name}</h3>
<p>{user.email}</p>
</div>
))}
</div>
</div>
);
}
Практический пример 2: Переход между вкладками с большим содержимым
import { useState, useTransition } from 'react';
function Dashboard() {
const [activeTab, setActiveTab] = useState('overview');
const [isPending, startTransition] = useTransition();
const handleTabClick = (tabName) => {
// Старая вкладка остаётся видимой, пока новая рендерится
startTransition(() => {
setActiveTab(tabName);
});
};
return (
<div>
<div className="tabs">
<button
onClick={() => handleTabClick('overview')}
className={activeTab === 'overview' ? 'active' : ''}
disabled={isPending}
>
Overview
</button>
<button
onClick={() => handleTabClick('analytics')}
className={activeTab === 'analytics' ? 'active' : ''}
disabled={isPending}
>
Analytics
</button>
<button
onClick={() => handleTabClick('settings')}
className={activeTab === 'settings' ? 'active' : ''}
disabled={isPending}
>
Settings
</button>
</div>
<div className="tab-content">
{isPending && (
<div className="loading-overlay">Loading tab...</div>
)}
{activeTab === 'overview' && <OverviewTab />}
{activeTab === 'analytics' && <AnalyticsTab />}
{activeTab === 'settings' && <SettingsTab />}
</div>
</div>
);
}
Практический пример 3: Сортировка большого списка
import { useState, useTransition, useMemo } from 'react';
function ProductList({ products }) {
const [sortBy, setSortBy] = useState('name');
const [isPending, startTransition] = useTransition();
const handleSort = (sortField) => {
startTransition(() => {
setSortBy(sortField);
});
};
const sortedProducts = useMemo(() => {
// Это может быть дорогостоящая операция
const copy = [...products];
if (sortBy === 'name') {
return copy.sort((a, b) => a.name.localeCompare(b.name));
} else if (sortBy === 'price') {
return copy.sort((a, b) => a.price - b.price);
} else if (sortBy === 'rating') {
return copy.sort((a, b) => b.rating - a.rating);
}
return copy;
}, [sortBy]);
return (
<div>
<div className="sort-controls">
<button
onClick={() => handleSort('name')}
className={sortBy === 'name' ? 'active' : ''}
disabled={isPending}
>
Sort by Name
</button>
<button
onClick={() => handleSort('price')}
className={sortBy === 'price' ? 'active' : ''}
disabled={isPending}
>
Sort by Price
</button>
<button
onClick={() => handleSort('rating')}
className={sortBy === 'rating' ? 'active' : ''}
disabled={isPending}
>
Sort by Rating
</button>
</div>
<div className={isPending ? 'updating' : ''}>
{sortedProducts.map(product => (
<div key={product.id} className="product-card">
<h3>{product.name}</h3>
<p>Price: ${product.price}</p>
<p>Rating: {product.rating}/5</p>
</div>
))}
</div>
</div>
);
}
useTransition vs useDeferredValue
Eсть похожий хук useDeferredValue для отложенного обновления значения:
// useTransition - когда ты контролируешь setState
const [isPending, startTransition] = useTransition();
startTransition(() => setState(newValue));
// useDeferredValue - когда ты получаешь значение как prop
const deferredValue = useDeferredValue(value);
// дефолтный изоляционный способ
Пример с useDeferredValue
import { useState, useDeferredValue } from 'react';
function App() {
const [input, setInput] = useState('');
const deferredInput = useDeferredValue(input);
return (
<div>
<input
type="text"
value={input}
onChange={(e) => setInput(e.target.value)}
/>
{/* Результаты будут отображаться с задержкой */}
<SearchResults query={deferredInput} />
</div>
);
}
Когда использовать useTransition
- Поиск/фильтрация в больших списках - позволяет input оставаться отзывчивым
- Переключение между вкладками с тяжёлым содержимым
- Сортировка/группировка данных которые требуют перерендеринга
- Асинхронные операции которые не требуют срочного обновления
- Формы где один input должен быть быстрым, а остальное может быть отложено
Важные замечания
// Обновления внутри startTransition становятся неблокирующими
startTransition(() => {
// Эти setState вызовы получают низкий приоритет
setFilteredResults(filtered);
setSearchMetadata(metadata);
});
// React может прервать transition если придёт срочное обновление
// Это нормальное поведение - UI остаётся отзывчивым
// Состояние isPending обновляется, когда transition начинается/заканчивается
if (isPending) {
// Можно показать loading indicator, уменьшить opacity и т.д.
}
useTransition - это мощный инструмент для оптимизации производительности в React 18+, особенно полезный для больших приложений с интенсивным рендерингом.