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

6. Формы и Form-компонент

Remix предоставляет мощный компонент <Form>, который расширяет стандартный HTML <form>. Он работает нативно без JavaScript, а когда JS доступен — использует fetch для плавных переходов без перезагрузки страницы.

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> HTML<Form> Remix
Без JS
SPA-переход
Pending state✅ (useNavigation)
Автообновление данных
Оптимистичный UI✅ (useFetcher)
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>
);
}

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>
);
}