Тріаж оновлення React 19: Які нові API замінюють існуючі обхідні шляхи
Практичний огляд використання Server Actions, useOptimistic та React Compiler, із важливими застереженнями та планом щодо того, які зміни в React 19 слід прийняти першими.
React 19 додав більше можливостей API, ніж будь-яка попередня версія протягом багатьох років, і така кількість нововведень ускладнює визначення того, що є важливим для існуючої бази коду. Для більшості команд кілька нових функцій замінюють тимчасові рішення, які вони вже використовують; одна змінює підхід до завантаження даних, а решта може почекати. Нижче наведені найважливіші зміни разом із прикладами коду до та після оновлення та застереженнями, які легко проігнорувати, щоб ви могли спланувати оновлення з урахуванням його користі.
use: читання Promises та Context під час відображення
API use є найзначнішим додатком, і його поведінка відрізняється від усіх інших хуків, які ви знаєте. Звичайні хуки потрібно викликати на верхньому рівні компонента, у тому самому порядку під час кожного відображення. use не підпадає під це правило: його можна викликати після раннього повернення, всередині умови чи під час циклу.
У наведеному нижче прикладі компонент повертає перегляд для гостя, коли відсутній userId, і лише тоді зчитує дані користувача:
// React 19 — use() can be called inside conditionals and loops
function UserProfile({ userId }) {
if (!userId) return <GuestView />;
// This is valid in React 19
const user = use(fetchUser(userId));
return <div>{user.name}</div>;
}
Справжня сила полягає у тому, що приймає функція use: Promise або Context. Якщо передано Promise, React призупиняє виконання компонента до його завершення. Наведене нижче порівняння показує, наскільки багато функціоналу зникає. Стара версія відстежує дані та прапорець завантаження у стані, отримує дані через ефект та вручну відображає індикатор завантаження; версія React 19 зчитує значення безпосередньо:
// Before React 19
function UserProfile({ userId }) {
const [user, setUser] = useState(null);
const [loading, setLoading] = useState(true);
useEffect(() => {
fetchUser(userId).then(data => {
setUser(data);
setLoading(false);
});
}, [userId]);
if (loading) return <Spinner />;
return <div>{user.name}</div>;
}
// React 19
function UserProfile({ userId }) {
const user = use(fetchUser(userId));
return <div>{user.name}</div>;
}
Робота просто перемістилась, а не зникла: найближчий межовий елемент <Suspense> відображає альтернативний варіант, поки Promise у стані очікування, а найближчий елемент обробки помилок керує ситуацією при відхиленні. Компонент описує лише сценарій успішного виконання.
Promise має бути стабільним
use очікує однаковий Promise під час кожного перерендування. Якщо fetchUser(userId) створює новий Promise щоразу під час перерендування, React знову призупиняє виконання компонента, що спричиняє повторні запити або ситуацію, коли компонент ніколи не досягає стану готовності; у клієнтських компонентах React попереджає про Promise, які створюються під час перерендування без кешування. Тож розглядайте ці приклади лише як ілюстрації формату. Promise має походити від джерела, яке його кешує: бібліотеки даних, здатних працювати з Suspense, завантажувачі фреймворків або серверські компоненти, які ініціюють запит та передають Promise у вигляді атрибута. Іноді пропонують обгортати цей виклик у useMemo, але React не гарантує збереження мемоізованих значень, тому це не є надійним способом кешування.
Дії на сервері як функція React
Користувачі Next.js App Router вже знайомі з Server Actions. У React 19 вони стали частиною самого React (у документації їх тепер називають Server Functions) і доступні в будь-якому фреймворку, який підтримує Server Components.
У прикладі визначена асинхронна функція, позначена як 'use server', яка додає користувача та оновлює список, а потім передає її у властивість action форми:
// Server Action — runs on the server, called from the client
async function submitForm(formData) {
'use server';
const name = formData.get('name');
await db.users.create({ name });
revalidatePath('/users');
}
// Client component
function UserForm() {
return (
<form action={submitForm}>
<input name="name" />
<button type="submit">Add User</button>
</form>
);
}
Властивість action елемента <form> тепер приймає функцію, включаючи асинхронну; React виконує її під час надсилання даних та передає їй об’єкт FormData. Статус очікування, оптимістичні оновлення та помилки забезпечуються супутніми API: useFormStatus або useActionState для статусу очікування та результату, useOptimistic для миттєвої зворотної відгуку, а межі помилок — для обробки невдач. Ви пишете код змін; React координує решту.
У реальному додатку потрібно виправити одну деталь у цьому фрагменті. Функція з вбудованою директивою 'use server' може бути визначена лише у серверному компоненті. Якщо UserForm є клієнтським компонентом, перенесіть submitForm у власний файл із директивою 'use server' у верхній частині та імпортуйте його.
Де серверні компоненти видаляють статичний інтерфейс з клієнтського пакету, серверні дії видаляють вручну написані API-шляхи для змін: менше коду, менше операцій передачі даних. Проте кожну дію слід розглядати як публічний кінцевий пункт та перевіряти вхідні дані та авторизацію всередині неї.
useOptimistic: миттєва зворотна відповідь без дублювання стану
Рендеринг результату до підтвердження від сервера раніше означав ручне керування додатковим станом. Функція useOptimistic бере поточний стан та функцію оновлення, схожу на редьюсер, і повертає стан для відображення разом із функцією, яка застосовує оптимістичні зміни. У цьому випадку додавання завдання означає вставку тимчасового елемента, позначеного як «у процесі», який відображається злегка прозорим до завершення збереження:
function TodoList({ todos }) {
const [optimisticTodos, addOptimisticTodo] = useOptimistic(
todos,
(currentTodos, newTodo) => [...currentTodos, newTodo]
);
async function handleAdd(text) {
addOptimisticTodo({ id: 'temp', text, pending: true });
await saveTodo(text); // Server Action
}
return (
<ul>
{optimisticTodos.map(todo => (
<li key={todo.id} style={{ opacity: todo.pending ? 0.7 : 1 }}>
{todo.text}
</li>
))}
</ul>
);
}
Коли ви викликаєте addOptimisticTodo, React негайно відображає оптимістичний список. Коли супутня дія завершується, оптимістичне значення видаляється, і React знову рендерить список на основі властивості todos, яка до цього моменту вже має містити збережений елемент.
Раніше ви зберігали підтверджені та очікувані копії даних та вручну виправляли їх у разі розбіжностей; тепер цим життєвим циклом керує механізм hook. Важливі дві умови. По-перше, оптимістичне оновлення має відбуватися під час виконання дії або переходу, наприклад у функції, яка передається до властивості action форми, або усередині startTransition; її виклик зі звичайного обробника подій спричиняє попередження в React, і оновлення може не відобразитися. По-друге, батьківський елемент має фактично отримати оновлені дані todos після збереження, зазвичай через перевірку, інакше новий елемент зникне під час скидання оптимістичного стану. Тимчасові ідентифікатори, такі як 'temp', також можуть викликати конфлікти, якщо два елементи додаються швидко, тому необхідно генерувати унікальні ідентифікатори. Ми розглядаємо ці крайні випадки у п’яти способах збою механізму useOptimistic rollback.
Компілятор React: мемоїзація за замовчуванням
Компілятор React, раніше відомий як React Forget, з’явився разом із React 19 як опційний крок компіляції та має найбільший вплив на повсякденний код. Він додає мемоїзацію під час компіляції, тож значення та функції-колбеки використовуються знову, коли вхідні дані залишаються незмінними, а дочірні елементи уникають повторної відрендеризації, якщо пропси однакові. Ручне використання useMemo, useCallback та React.memo стає майже непотрібним, як показує порівняння:
// Before: manual memoization required
const expensiveValue = useMemo(() => compute(a, b), [a, b]);
const stableCallback = useCallback(() => doSomething(id), [id]);
const MemoizedChild = React.memo(ChildComponent);
// After React Compiler: write normal code
const expensiveValue = compute(a, b);
const handleClick = () => doSomething(id);
// ChildComponent renders only when its props change - automatically
Вам все ще потрібно розуміти, коли та чому компоненти перерендеруються, але ручні способи обходу цього проблеми мають значно менше значення. Наш огляд шаблонів, які спричиняють непотрібну перерендеризацію залишається корисним додатковим матеріалом.
Для нових проєктів компілятор є надійним стандартним варіантом. У існуючому коді слід впроваджувати його обережно: він передбачає, що компоненти є «чистими» та дотримуються правил React, тому код, який змінюється під час відображення, може поводитися інакше після компіляції. Увімкніть його поступово, запустіть тести та спочатку використайте плагін ESLint для виявлення порушень. Перевірте поточну документацію React щодо статусу випуску та підтримуваних версій React.
Що слід впровадити та у якому порядку
Негайна користь, низький ризик
useOptimisticтам, де форми взаємодіють із станом сервера.- Server Actions, якщо ви використовуєте Next.js 14 або новішу версію та хочете замінити API-маршрути для виконання змін.
Що варто спланувати
use, коли ваш шар даних генерує стабільні, кешовані Promises; він природно поєднується з Suspense.
Для більшості додатків це не терміново
- Зміни в ref: тепер
refможна передавати як звичайне значення пропу до функційних компонентів, томуforwardRefбільше не потрібен. - Покращення контексту, які переважно стосуються покращення досвіду розробника, а не нової поведінки.
- Підтримка метаданих документації, яка дозволяє компонентам відображати теги
<title>та<meta>, які React додає у вхідну частину документа.
Основні висновки
- React 19 — це еволюція, а не кардинальна переробка. Його нові API формалізують паттерни, які команди з розробки вже використовують вручну: оптимістичні оновлення, серверські зміни та умовне читання даних.
useпереміщує обробку завантаження та помилок у Suspense та межі помилок, але ефективно працює лише з обіцянками (Promises), які зберігаються у кеші поза процесом відображення.- Server Actions та
useOptimisticгармонійно поєднуються: перший відповідає за запис даних, а другий приховує його затримку. - Компілятор робить мемоїзацію стандартною, за умови, що ваші компоненти дотримуються правил React.
Корисне запитання полягає не в тому, чи потрібно оновитися, а в тому, який з цих інструментів може замінити тимчасове рішення, яке ви зараз використовуєте. Почніть з цього.
Пов’язана література
- useOptimistic Rollback: П’ять способів збою в Next.js Server Actions — Дізнайтеся, чому useOptimistic тихо скасовує зміни в інтерфейсі, не пояснюючи користувачам причини збоїв, за допомогою п’яти перевірених сценаріїв збою Server Action та робочого рішення.
- Server Actions чи Route Handlers? Посібник з прийняття рішень для Next.js 16 — Дізнайтеся, коли мутація в Next.js 16 слід реалізовувати через Server Action, а коли — через Route Handler, з виправленими прикладами для форм, помилок, механізму оптимізму та вебхуків.