Перейти к содержимому

12. Optimistic UI

Optimistic UI — техника, при которой интерфейс обновляется мгновенно, не дожидаясь ответа сервера. Remix делает это простым с помощью useFetcher и useOptimistic.

import { useFetcher } from "@remix-run/react";
function TodoItem({ todo }: { todo: Todo }) {
const fetcher = useFetcher();
// Оптимистично определяем состояние
const isCompleted = fetcher.formData
? fetcher.formData.get("completed") === "true"
: todo.completed;
return (
<div style={{ opacity: fetcher.state !== "idle" ? 0.7 : 1 }}>
<fetcher.Form method="post" action="/todos/toggle">
<input type="hidden" name="id" value={todo.id} />
<input type="hidden" name="completed" value={String(!isCompleted)} />
<button type="submit">
{isCompleted ? "✅" : "⬜"} {todo.title}
</button>
</fetcher.Form>
</div>
);
}
function TodoList({ todos }: { todos: Todo[] }) {
const fetcher = useFetcher();
// Фильтруем удалённый элемент оптимистично
const deletingId = fetcher.formData?.get("id");
const optimisticTodos = todos.filter(t => t.id !== deletingId);
return (
<ul>
{optimisticTodos.map(todo => (
<li key={todo.id}>
{todo.title}
<fetcher.Form method="post" action="/todos/delete">
<input type="hidden" name="id" value={todo.id} />
<button type="submit">Удалить</button>
</fetcher.Form>
</li>
))}
</ul>
);
}
function AddTodo() {
const fetcher = useFetcher<{ todo: Todo }>();
const formRef = useRef<HTMLFormElement>(null);
// Сбрасываем форму после успешного сохранения
useEffect(() => {
if (fetcher.state === "idle" && fetcher.data?.todo) {
formRef.current?.reset();
}
}, [fetcher.state, fetcher.data]);
// Создаём временный todo пока запрос выполняется
const optimisticTodo = fetcher.formData
? {
id: "temp",
title: fetcher.formData.get("title") as string,
completed: false,
}
: null;
return (
<>
{optimisticTodo && <TodoItem todo={optimisticTodo} />}
<fetcher.Form ref={formRef} method="post" action="/todos">
<input name="title" placeholder="Новая задача..." required />
<button type="submit">Добавить</button>
</fetcher.Form>
</>
);
}
import { useOptimistic } from "react";
import { useFetcher } from "@remix-run/react";
function LikeCounter({ postId, likes }: { postId: string; likes: number }) {
const fetcher = useFetcher();
const [optimisticLikes, addLike] = useOptimistic(
likes,
(state, increment: number) => state + increment
);
return (
<fetcher.Form
method="post"
action="/like"
onSubmit={() => addLike(1)}
>
<input type="hidden" name="postId" value={postId} />
<button type="submit">❤️ {optimisticLikes}</button>
</fetcher.Form>
);
}