Запропонувати правкуПокращити цю статтюДопрацюйте відповідь до «useTransition та useDeferredValue в React». Ваші зміни проходять модерацію перед публікацією.Потрібне підтвердженняКонтентЩо ви змінюєте🇺🇸EN🇺🇦UAПереглядЗаголовок (UA)Коротка відповідь (UA)**useTransition** і **useDeferredValue** - хуки React 18 для конкурентного рендерингу (concurrent rendering). `useTransition` огортає сеттер стану і повертає `[isPending, startTransition]`. `useDeferredValue` огортає значення, корисний коли отримуєш проп від батьківського компонента. ```tsx // Контролюєш сеттер const [isPending, startTransition] = useTransition(); startTransition(() => setResults(filter(query))); // Маєш лише значення const deferred = useDeferredValue(query); ``` **Правило:** контролюєш сеттер - `useTransition`; отримуєш проп - `useDeferredValue`.Показується над повною відповіддю для швидкого нагадування.Відповідь (UA)Зображення**useTransition** позначає оновлення стану як низькопріоритетну роботу, яку React може перервати; **useDeferredValue** відкладає (defer) перерендер значення до завершення термінових оновлень. ## Теорія ### TL;DR - Аналогія: `useTransition` - це як сказати React "фільтруй список у фоні, поки я друкую" - поле введення реагує миттєво, результати підтягуються потім. - `useTransition` огортає сеттер стану і повертає `[isPending, startTransition]`; `useDeferredValue` огортає значення і повертає його відкладену копію. - Контролюєш сеттер стану: `useTransition`. Отримуєш значення як проп: `useDeferredValue`. - Потрібен індикатор завантаження: тільки `useTransition` має `isPending`. У `useDeferredValue` цього немає. - Обидва хуки потребують React 18 з конкурентним рендерингом (concurrent rendering). ### Швидкий приклад ```tsx import { useState, useTransition } from 'react'; function Search() { const [query, setQuery] = useState(''); const [results, setResults] = useState([]); const [isPending, startTransition] = useTransition(); const handleChange = (e) => { setQuery(e.target.value); // Термінове: фіксується до наступного кадру startTransition(() => { setResults(heavySearch(e.target.value)); // Низький пріоритет: поступається введенню }); }; return ( <div> <input value={query} onChange={handleChange} /> {isPending && <p>Шукаємо...</p>} <ul>{results.map(r => <li key={r}>{r}</li>)}</ul> </div> ); } ``` Якщо друкувати швидко, поле введення оновлюється на кожен символ. `isPending` буде `true` поки фоновий фільтр працює. Результати з'являються після паузи. ### Головна різниця `useTransition` потребує доступу до сеттера стану. Ти викликаєш `startTransition(() => setState(...))` і отримуєш `isPending` у відповідь. `useDeferredValue` потребує лише саме значення, тому він підходить для дочірніх компонентів, які отримують проп від батьківського і не контролюють його сеттер. Індикатора очікування немає, але застарілість (staleness) можна виявити, порівнявши оригінал з відкладеною копією: `query !== deferredQuery`. ### Коли що використовувати - Пошукове поле з важкою фільтрацією на клієнті: `useTransition` (огортає `setResults`). - Дочірній компонент рендерить великий список з пропу: `useDeferredValue` (відкладає перерендер списку). - Перемикання вкладок з важким рендером: `useTransition` (нетермінова зміна вкладки + `isPending` для блокування кнопок). - Поле введення має реагувати миттєво, а відображення результатів може відставати: `useDeferredValue`. - Потрібен спінер: `useTransition`. Спінер не потрібен: обидва варіанти підходять, але `useDeferredValue` простіший. ### Таблиця порівняння | | **useTransition** | **useDeferredValue** | |---|---|---| | Що відкладає | Оновлення стану | Значення | | Де застосовувати | Контролюєш сеттер | Маєш лише значення | | Повертає | `[isPending, startTransition]` | Відкладену копію значення | | Індикатор очікування | Вбудований `isPending` | Порівняти оригінал з відкладеним вручну | | Типове місце | Обробник події | Тіло компонента або дочірній компонент | ### Як React планує це внутрішньо React 18 призначає кожному оновленню "смугу" (lane) всередині файберного (fiber) reconciler. Термінові оновлення (події введення) отримують `SyncLane` або `DefaultLane`. Все, що огорнуто в `startTransition`, отримує `TransitionLane` з нижчим пріоритетом. Під час циклу рендеру планувальник запускає `shouldYield()` після кожної одиниці роботи. Якщо смуга з вищим пріоритетом має незавершену роботу, низькопріоритетний рендер зупиняється і відновлюється пізніше. `useDeferredValue` використовує ту саму систему смуг без торкання сеттера. React рендерить компонент двічі: з поточним значенням (терміновий шлях) і з відкладеним (низькопріоритетний шлях). Відкладений рендер може бути перерваний у будь-який момент. Це власний файберний планувальник React, а не `requestIdleCallback` браузера. ### Типові помилки **1. Використовувати `isPending` замість перевірки порожнього стану** ```tsx // Неправильно: крутиться навіть коли results справді порожній {results.length === 0 && <Spinner />} // Правильно: крутиться тільки під час активного переходу {isPending && <Spinner />} ``` **2. Огортати сеттер поля введення в `startTransition`** Найчастіша помилка, яку я бачу на code review: сеттер введення потрапляє всередину переходу. ```tsx // Неправильно: поле введення гальмує, втрачається весь сенс startTransition(() => { setQuery(e.target.value); }); // Правильно: введення окремо, важка робота всередині setQuery(e.target.value); startTransition(() => setResults(heavySearch(e.target.value))); ``` **3. Вважати, що `useDeferredValue` скасовує важкі обчислення** ```tsx // Хибне припущення const deferred = useDeferredValue(expensiveCompute(query)); // expensiveCompute все одно виконується при кожному рендері ``` Відкладання змінює лише момент фіксації рендеру. `expensiveCompute(query)` запускається в будь-якому разі. Огортай важкий виклик у `useMemo` прив'язаний до відкладеного значення, або використовуй abort controller для асинхронної роботи. **4. Вкладати виклики `startTransition`** ```tsx // Неправильно startTransition(() => { startTransition(() => setDeepState(val)); // Не підвищує пріоритет }); ``` Використовуй один зовнішній `startTransition`. Вкладення не підвищує пріоритет і може призвести до непередбачуваних станів очікування. **5. Ігнорувати подвійний виклик у StrictMode** У режимі розробки з StrictMode, React 18 двічі викликає сеттери стану всередині переходів. `isPending` може мигати. Це нормальна поведінка в dev-режимі, у production вона відсутня. ### Де використовується в реальних проектах - React DevTools: `useTransition` для перемикання фільтрів у дереві компонентів. - TanStack Query v5: огортає оновлення списку після мутацій у переходи. - Next.js App Router: `useDeferredValue` відкладає результати пошуку в динамічних сегментах маршрутів. - Vercel Commerce: дебаунсинг (debouncing) пошуку через відкладений запит до великого каталогу продуктів. ### Питання на співбесіді **Q:** У чому різниця між `useTransition` і standalone `startTransition` з `react`? **A:** Standalone `startTransition` робить те саме, але нічого не повертає. Використовуй його у допоміжних функціях або обробниках подій поза компонентами, де `isPending` не потрібен. **Q:** Як `useDeferredValue` взаємодіє з `Suspense`? **A:** Якщо відкладений рендер звертається до призупиненого (suspended) ресурсу, React показує застарілий UI замість fallback `Suspense`, поки відкладений рендер не завершиться. Це запобігає спалаху до спінера на кожен символ введення. **Q:** Чому `isPending` може довго залишатись `true`? **A:** Якщо новий перехід стартує до завершення попереднього, прапор залишається `true`. Необроблена помилка всередині transition callback теж блокує чергу. **Q:** Які пріоритети смуг використовує планувальник React? **A:** Термінове введення: `SyncLane` або `DefaultLane`. Робота `startTransition`: `TransitionLane`. Фонові завдання: `IdleLane`. Reconciler зупиняється щоразу, коли `shouldYield()` повертає `true`. **Q:** Як реалізувати витіснення (preemption) переходів у власному планувальнику? **A:** Призначай низькопріоритетну смугу файберу переходу в `scheduleUpdateOnFiber`. У циклі роботи перевіряй `shouldYield()` після кожного файбера. Якщо вища смуга має роботу, виходь з циклу і став нове завдання для неї. Незавершена низькопріоритетна робота лишається в дереві і відновлюється після завершення пріоритетного проходу. React не викидає часткову роботу, а призупиняє її. ## Приклади ### Базовий: фільтрація великого списку з useTransition ```tsx import { useState, useTransition } from 'react'; const items = Array.from({ length: 10000 }, (_, i) => `Продукт ${i + 1}`); function ProductList() { const [query, setQuery] = useState(''); const [results, setResults] = useState(items); const [isPending, startTransition] = useTransition(); const handleChange = (e: React.ChangeEvent<HTMLInputElement>) => { const value = e.target.value; setQuery(value); // Фіксується до запуску фільтра startTransition(() => { setResults( items.filter(item => item.toLowerCase().includes(value.toLowerCase())) ); }); }; return ( <div> <input value={query} onChange={handleChange} placeholder="Пошук..." /> {isPending && <p>Фільтруємо...</p>} <ul> {results.map(item => <li key={item}>{item}</li>)} </ul> </div> ); } ``` Поле введення фіксується до запуску фільтра. При 10к елементах фільтрація займає час; `isPending` показує повідомлення поки вона працює. Жодного debounce не потрібно. ### Середній: useDeferredValue в дочірньому компоненті ```tsx import { useState, useDeferredValue, useMemo } from 'react'; interface Product { id: number; name: string; } function ProductResults({ query, products }: { query: string; products: Product[] }) { const deferredQuery = useDeferredValue(query); const isStale = query !== deferredQuery; // true поки відкладений відстає const filtered = useMemo( () => products.filter(p => p.name.toLowerCase().includes(deferredQuery.toLowerCase()) ), [products, deferredQuery] // Перераховується лише при зміні deferredQuery ); return ( <ul style={{ opacity: isStale ? 0.6 : 1, transition: 'opacity 0.15s' }}> {filtered.map(p => <li key={p.id}>{p.name}</li>)} </ul> ); } function App({ products }: { products: Product[] }) { const [query, setQuery] = useState(''); return ( <div> <input value={query} onChange={e => setQuery(e.target.value)} /> <ProductResults query={query} products={products} /> </div> ); } ``` `ProductResults` не має доступу до сеттера, тому `useTransition` тут не варіант. `useDeferredValue` відкладає фільтр до фіксації введення. Зниження прозорості сигналізує, що свіжіший результат вже готується. ### Просунутий: обидва хуки в одному компоненті ```tsx import { useState, useTransition, useDeferredValue, useMemo } from 'react'; type Tab = 'active' | 'archived'; interface Item { id: number; name: string; status: Tab; } function AdminDashboard({ items }: { items: Item[] }) { const [query, setQuery] = useState(''); const [tab, setTab] = useState<Tab>('active'); const [isPending, startTransition] = useTransition(); const deferredQuery = useDeferredValue(query); const handleTabChange = (next: Tab) => { startTransition(() => setTab(next)); // Нетермінове, але сеттер твій }; const visible = useMemo( () => items .filter(item => item.status === tab) .filter(item => item.name.toLowerCase().includes(deferredQuery.toLowerCase()) ), [items, tab, deferredQuery] ); return ( <div> <input value={query} onChange={e => setQuery(e.target.value)} placeholder="Фільтр..." /> <button onClick={() => handleTabChange('active')} disabled={isPending}> Активні {isPending && '...'} </button> <button onClick={() => handleTabChange('archived')} disabled={isPending}> Архів {isPending && '...'} </button> <ul style={{ opacity: query !== deferredQuery ? 0.7 : 1 }}> {visible.map(item => <li key={item.id}>{item.name}</li>)} </ul> </div> ); } ``` Кнопки вкладок використовують `useTransition`, бо ти контролюєш `setTab`. Список використовує `useDeferredValue` для запиту, бо фільтру потрібне лише значення. Обидва хуки в одному компоненті, кожен на своєму місці.Для рев’юераПримітка для модератора (необов’язково)Бачить лише модератор. Прискорює рев’ю.