useFetcher: формы без перехода
Form после отправки ведёт себя как переход: меняется история браузера, а общее состояние навигации становится submitting. Для мелких действий на странице — комментарий, лайк, «прочитать позже» — это лишнее. Хук useFetcher даёт форму, которая вызывает action и обновляет данные, но не меняет адрес и имеет собственное состояние.
Данные для комментариев
Допишите в app/data.server.ts:
// app/data.server.ts
export type Comment = { id: number; postId: number; author: string; text: string };
const comments: Comment[] = [];
export async function getComments(postId: number) {
return comments.filter((comment) => comment.postId === postId);
}
export async function addComment(postId: number, author: string, text: string) {
comments.push({ id: comments.length + 1, postId, author, text });
}Загрузчик и действие статьи
В app/routes/post.tsx загрузчик возвращает и комментарии, а новое действие сохраняет комментарий:
// app/routes/post.tsx
import { addComment, getComments, getPost } from "../data.server";
export async function loader({ params }: Route.LoaderArgs) {
const id = Number(params.postId);
const post = await getPost(id);
if (!post) throw new Response("Статья не найдена", { status: 404 });
return { post, comments: await getComments(id) };
}
export async function action({ request, params }: Route.ActionArgs) {
const formData = await request.formData();
await addComment(
Number(params.postId),
String(formData.get("author")),
String(formData.get("text")),
);
return { ok: true };
}Форма комментария
// app/routes/post.tsx
import { useFetcher } from "react-router";
export default function Post({ loaderData }: Route.ComponentProps) {
const { post, comments } = loaderData;
const fetcher = useFetcher();
const isSending = fetcher.state !== "idle";
return (
<article>
<h1>{post.title}</h1>
<p>{post.body}</p>
<ul>
{comments.map((c) => (
<li key={c.id}>{c.author}: {c.text}</li>
))}
{isSending && (
<li>{String(fetcher.formData?.get("author"))}: {String(fetcher.formData?.get("text"))}</li>
)}
</ul>
<fetcher.Form method="post">
<input name="author" placeholder="Ваше имя" />
<input name="text" placeholder="Комментарий" />
<button type="submit" disabled={isSending}>Отправить</button>
</fetcher.Form>
</article>
);
}Напишите комментарий на /posts/1 — он сразу появится в списке (оптимистично, из fetcher.formData), а после ответа сервера загрузчик обновит список настоящими данными. Адрес и история браузера не меняются. Без атрибута action форма отправляется в действие текущего маршрута.
Что есть у fetcher
| Поле | Значение |
|---|---|
fetcher.Form | Форма без перехода |
fetcher.state | idle, submitting или loading — только для этой формы |
fetcher.formData | Отправляемые поля |
fetcher.data | То, что вернул action, здесь { ok: true } |
fetcher.submit() | Отправка из кода |
fetcher.load(адрес) | Вызов загрузчика другого маршрута |
Частая ошибка
После отправки через fetcher.Form поля не очищаются — страница ведь не перезагружалась. Сбросьте форму сами: заведите useRef на форму и в useEffect вызывайте formRef.current?.reset(), когда fetcher.state вернулся в idle, а fetcher.data?.ok истинно.