Пошук уроків, статей та іншого контенту
Застосуєте хук useOptimistic для синхронізації тимчасового стану UI з результатом Server Action.
useOptimisticuseOptimistic дає змогу миттєво змінити інтерфейс ще до завершення асинхронної операції. Наприклад, користувач натискає перемикач завдання:
UI одразу показує новий стан.
Server Action змінює дані на сервері.
Next.js повторно отримує актуальні дані.
Тимчасовий стан замінюється результатом із сервера.
Без оптимістичного оновлення користувач бачив би старий стан, поки Server Action не завершиться.
const [optimisticState, addOptimistic] = useOptimistic(
state,
updateFunction
);state — актуальний стан із сервера;
optimisticState — стан, який слід відображати в UI;
addOptimistic(value) — додає тимчасову зміну;
updateFunction(currentState, value) — обчислює новий оптимістичний стан.
Оптимістичний стан не є новим джерелом істини. Джерелом істини залишається сервер.
Під час виконання Server Action React тимчасово використовує результат updateFunction.
Після завершення Server Action:
якщо серверні дані успішно оновилися, Next.js може отримати їх через revalidatePath;
якщо операція завершилася помилкою, оптимістичний стан повертається до останнього підтвердженого стану;
після оновлення Server Component нові серверні пропси стають базовим станом для useOptimistic.
Важливо викликати addOptimistic усередині action або transition. У випадку атрибута action у форми React автоматично виконує код форми як action.
Створимо сторінку зі списком завдань. Стан завдання змінюватиметься оптимістично, а остаточне значення зберігатиметься у Server Action.
Файл app/tasks/actions.ts:
'use server';
import { revalidatePath } from 'next/cache';
export type Task = {
id: string;
title: string;
completed: boolean;
};
let tasks: Task[] = [
{
id: '1',
title: 'Налаштувати Server Action',
completed: false,
},
{
id: '2',
title: 'Додати оптимістичне оновлення',
completed: true,
},
];
export async function getTasks(): Promise<Task[]> {
return tasks;
}
export async function toggleTask(
id: string,
completed: boolean
): Promise<Task> {
await new Promise((resolve) => setTimeout(resolve, 800));
const taskIndex = tasks.findIndex((task) => task.id === id);
if (taskIndex === -1) {
throw new Error('Завдання не знайдено');
}
const updatedTask: Task = {
...tasks[taskIndex],
completed,
};
tasks = tasks.map((task) =>
task.id === id ? updatedTask : task
);
revalidatePath('/tasks');
return updatedTask;
}У реальному застосунку замість масиву tasks використовуватиметься база даних. Затримка в прикладі імітує мережевий запит.
revalidatePath('/tasks') повідомляє Next.js, що сторінку потрібно повторно перевірити та отримати з сервера актуальний список завдань.
Файл app/tasks/page.tsx:
import { getTasks } from './actions';
import { TaskList } from './task-list';
export const dynamic = 'force-dynamic';
export default async function TasksPage() {
const tasks = await getTasks();
return (
<main>
<h1>Завдання</h1>
<TaskList tasks={tasks} />
</main>
);
}Сторінка отримує дані на сервері та передає їх у клієнтський компонент.
useOptimisticФайл app/tasks/task-list.tsx:
'use client';
import {
useOptimistic,
useState,
type FormEvent,
} from 'react';
import { useFormStatus } from 'react-dom';
import { toggleTask, type Task } from './actions';
type OptimisticUpdate = {
id: string;
completed: boolean;
};
type TaskListProps = {
tasks: Task[];
};
function SubmitButton() {
const { pending } = useFormStatus();
return (
<button type="submit" disabled={pending}>
{pending ? 'Збереження...' : 'Змінити стан'}
</button>
);
}
export function TaskList({ tasks }: TaskListProps) {
const [error, setError] = useState<string | null>(null);
const [optimisticTasks, addOptimisticTask] = useOptimistic<
Task[],
OptimisticUpdate
>(
tasks,
(currentTasks, update) =>
currentTasks.map((task) =>
task.id === update.id
? {
...task,
completed: update.completed,
}
: task
)
);
function createAction(task: Task) {
return async function taskAction() {
const nextCompleted = !task.completed;
setError(null);
addOptimisticTask({
id: task.id,
completed: nextCompleted,
});
try {
await toggleTask(task.id, nextCompleted);
} catch {
setError('Не вдалося оновити завдання');
}
};
}
return (
<section>
{error && <p role="alert">{error}</p>}
<ul>
{optimisticTasks.map((task) => (
<li key={task.id}>
<span>
{task.title}: {task.completed ? 'виконано' : 'не виконано'}
</span>
<form action={createAction(task)}>
<SubmitButton />
</form>
</li>
))}
</ul>
</section>
);
}Для завдання, яке ще не виконано, під час натискання кнопки відбувається таке:
createAction обчислює nextCompleted.
addOptimisticTask одразу змінює відображуваний список.
UI показує виконано, не очікуючи відповіді сервера.
Викликається toggleTask.
Server Action змінює дані та викликає revalidatePath.
Next.js надсилає сторінці підтверджений стан із сервера.
useOptimistic використовує нові tasks як базовий стан.
Функція, передана другим аргументом useOptimistic, повинна бути чистою:
(currentState, optimisticValue) => nextStateУ прикладі:
(currentTasks, update) =>
currentTasks.map((task) =>
task.id === update.id
? { ...task, completed: update.completed }
: task
)Вона не повинна:
змінювати currentTasks напряму;
виконувати HTTP-запити;
змінювати базу даних;
залежати від змінних, які змінюються поза її аргументами.
Правильний підхід — повертати новий масив і нові об’єкти для змінених елементів.
Небажано покладатися на перемикання значення всередині updateFunction:
(currentTasks, id) =>
currentTasks.map((task) =>
task.id === id
? { ...task, completed: !task.completed }
: task
);Такий варіант може створити проблеми, якщо для одного завдання швидко запустити кілька операцій. Кожна операція може отримати різний базовий стан.
Краще передавати конкретне очікуване значення:
addOptimisticTask({
id: task.id,
completed: true,
});Тоді функція оновлення встановлює саме потрібний стан:
(currentTasks, update) =>
currentTasks.map((task) =>
task.id === update.id
? { ...task, completed: update.completed }
: task
);Це особливо важливо для операцій, які не є простим перемиканням: зміни статусу, сортування, редагування тексту або встановлення кількості.
Оптимістичне оновлення не означає, що операція гарантовано завершиться успішно.
У прикладі Server Action може завершитися помилкою:
try {
await toggleTask(task.id, nextCompleted);
} catch {
setError('Не вдалося оновити завдання');
}Якщо Server Action не підтвердила зміну, React повертає оптимістичний стан до базового. Тому UI знову покаже значення з tasks.
У складніших компонентах можна додатково:
показати повідомлення про помилку;
залишити кнопку доступною для повторної спроби;
додати індикатор помилки конкретно для одного елемента;
зберігати локальний статус операції для кількох незалежних форм.
Не слід вручну змінювати оптимістичний стан на попереднє значення. Його базовий стан має походити із серверних пропсів.
useOptimistic не замінює серверну валідаціюОптимістичний стан змінює лише те, що бачить користувач. Він не гарантує, що операція дозволена.
Server Action все одно повинна:
перевіряти ідентифікатор запису;
перевіряти права користувача;
валідовувати вхідні дані;
виконувати транзакцію в базі даних;
повертати помилку, якщо зміна неможлива.
Клієнт може передати:
{
id: '1',
completed: true
}Але сервер не повинен довіряти цьому значенню без перевірки.
useOptimistic не повертає setter на кшталт setState.
Неправильно намагатися використовувати його так:
const [tasks, setTasks] = useOptimistic(initialTasks);Правильний API містить функцію додавання оптимістичної зміни:
const [optimisticTasks, addOptimisticTask] = useOptimistic(
initialTasks,
updateFunction
);addOptimistic поза action або transitionОптимістичне оновлення потрібно запускати в контексті action або transition. Атрибут:
<form action={action}>створює відповідний контекст автоматично.
Якщо action запускається зі звичайного обробника події, використовуйте transition:
'use client';
import { useOptimistic, useTransition } from 'react';
function Component() {
const [isPending, startTransition] = useTransition();
function handleClick() {
startTransition(async () => {
addOptimisticUpdate(nextValue);
await serverAction();
});
}
return (
<button onClick={handleClick} disabled={isPending}>
Зберегти
</button>
);
}Для форм із action цей шаблон зазвичай не потрібен.
Виклик Server Action сам по собі не змінює аргумент tasks, який уже отримав компонент.
Для актуалізації Server Component потрібно, щоб серверні дані були повторно отримані. Один із поширених способів — викликати в Server Action:
revalidatePath('/tasks');Без цього оптимістичний стан після завершення action може повернутися до застарілих пропсів.
updateFunctionНеправильно:
(currentTasks, update) => {
const task = currentTasks.find((item) => item.id === update.id);
if (task) {
task.completed = update.completed;
}
return currentTasks;
}Функція змінює наявний масив та об’єкт. Це може спричинити проблеми з визначенням змін React.
Правильно:
(currentTasks, update) =>
currentTasks.map((task) =>
task.id === update.id
? { ...task, completed: update.completed }
: task
);optimisticTasks призначений для відображення тимчасового стану. Він не замінює базу даних і не повинен використовуватися як остаточне підтвердження операції.
Після завершення Server Action саме серверні дані мають визначати наступний базовий стан.
useOptimistic показує очікуваний результат асинхронної операції негайно.
Перший аргумент хука — підтверджений базовий стан.
Другий аргумент — чиста функція обчислення нового оптимістичного стану.
addOptimistic потрібно викликати всередині action або transition.
Server Action залишається джерелом істини та повинна виконувати валідацію.
revalidatePath допомагає синхронізувати UI з оновленими серверними даними.
У разі помилки оптимістичний стан повертається до підтвердженого.
Для надійної роботи передавайте конкретне очікуване значення, а не лише команду «перемкнути».