Encodera

Спасибо, что читаете!

Пожертвуйте сайту, мы стараемся ради вас.

Оглавление

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.stateidle, 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 истинно.

Спасибо, что читаете!

Пожертвуйте сайту, мы стараемся ради вас.