6. Формы и Form-компонент
Remix предоставляет мощный компонент <Form>, который расширяет стандартный HTML <form>. Он работает нативно без JavaScript, а когда JS доступен — использует fetch для плавных переходов без перезагрузки страницы.
Компонент Form
Заголовок раздела «Компонент Form»import { Form } from "@remix-run/react";
export default function ContactPage() { return ( <Form method="post" action="/contact"> <label> Имя: <input name="name" required /> </label> <label> Email: <input name="email" type="email" required /> </label> <textarea name="message" rows={4} /> <button type="submit">Отправить</button> </Form> );}Отличия от обычного form
Заголовок раздела «Отличия от обычного form»| Возможность | <form> HTML | <Form> Remix |
|---|---|---|
| Без JS | ✅ | ✅ |
| SPA-переход | ❌ | ✅ |
| Pending state | ❌ | ✅ (useNavigation) |
| Автообновление данных | ❌ | ✅ |
| Оптимистичный UI | ❌ | ✅ (useFetcher) |
useSubmit — программная отправка
Заголовок раздела «useSubmit — программная отправка»import { useSubmit, Form } from "@remix-run/react";
export default function SearchForm() { const submit = useSubmit();
return ( <Form method="get" onChange={(e) => { submit(e.currentTarget, { replace: true }); }}> <input type="search" name="q" placeholder="Поиск..." /> </Form> );}fetcher.Form — без навигации
Заголовок раздела «fetcher.Form — без навигации»useFetcher позволяет отправлять формы без навигации по страницам. Идеально для inline-редактирования, добавления в избранное и т.д.:
import { useFetcher } from "@remix-run/react";
function LikeButton({ postId, liked }) { const fetcher = useFetcher(); const isLiked = fetcher.formData ? fetcher.formData.get("liked") === "true" : liked;
return ( <fetcher.Form method="post" action="/api/like"> <input type="hidden" name="postId" value={postId} /> <input type="hidden" name="liked" value={String(!isLiked)} /> <button type="submit"> {isLiked ? "❤️" : "🤍"} </button> </fetcher.Form> );}Валидация на клиенте и сервере
Заголовок раздела «Валидация на клиенте и сервере»// action — серверная валидация (всегда нужна)export async function action({ request }) { const data = await request.formData(); const errors: Record<string, string> = {};
const email = data.get("email") as string; if (!email?.includes("@")) { errors.email = "Неверный email"; }
if (Object.keys(errors).length > 0) { return json({ errors }, { status: 400 }); }
// ...}
// Компонентexport default function Form() { const actionData = useActionData<typeof action>();
return ( <Form method="post"> <input name="email" /> {actionData?.errors?.email && ( <span className="error">{actionData.errors.email}</span> )} </Form> );}Сброс формы
Заголовок раздела «Сброс формы»import { useEffect, useRef } from "react";import { useFetcher } from "@remix-run/react";
function CommentForm() { const fetcher = useFetcher(); const formRef = useRef<HTMLFormElement>(null);
useEffect(() => { if (fetcher.state === "idle" && fetcher.data?.ok) { formRef.current?.reset(); } }, [fetcher.state, fetcher.data]);
return ( <fetcher.Form ref={formRef} method="post"> <textarea name="comment" /> <button type="submit"> {fetcher.state !== "idle" ? "Отправка..." : "Комментировать"} </button> </fetcher.Form> );}