Remix строится вокруг простой идеи: маршрут — это не только компонент, но и функции загрузки и изменения данных. Сервер отдаёт готовый HTML, формы работают без JavaScript, а переходы между страницами подгружают только нужные данные. Сейчас актуальны Remix v2 и React Router v7, куда переехала большая часть API. Ниже — практические приёмы, которые переносятся между ними.
Маршруты как файлы
Структура каталога app/routes задаёт URL. Точка в имени файла означает сегмент, подчёркивание — вложенность без сегмента, а знак доллара — динамический параметр. Корневой route.tsx содержит Outlet и общий каркас. Вложенные маршруты рендерятся в родительский Outlet, поэтому layout не перезагружается при переходах.
app/routes/
_index.tsx
notes.tsx
notes.$id.tsx
notes.new.tsx
api.health.tsx
Файл notes.tsx становится layout для notes.$id.tsx и notes.new.tsx. Ресурсный маршрут api.health.tsx не возвращает компонент, а отдаёт Response — удобно для проверок и вебхуков.
loader: данные до рендера
Функция loader выполняется на сервере. Она получает params и request, может ходить в базу и возвращать JSON. Компонент читает результат через useLoaderData. Если данных нет, loader выбрасывает Response со статусом 404 — ближайший ErrorBoundary покажет страницу ошибки.
import { json, type LoaderFunctionArgs } from "@remix-run/node";
import { useLoaderData } from "@remix-run/react";
export async function loader({ params }: LoaderFunctionArgs) {
const note = await db.note.findUnique({ where: { id: params.id } });
if (!note) throw new Response("Not Found", { status: 404 });
return json({ note });
}
export default function Note() {
const { note } = useLoaderData<typeof loader>();
return <article><h1>{note.title}</h1></article>;
}
Типизация через typeof loader связывает серверную функцию и компонент: если поменять форму ответа, TypeScript укажет на несовпадение. Это дешевле ручного дублирования интерфейсов.
action и формы
Изменения описываются в action. Компонент Form отправляет данные обычным POST, поэтому форма работает даже при отключённом JavaScript. После успешного action фреймворк автоматически перезапускает loader текущего маршрута, и интерфейс обновляется без ручного кеша.
export async function action({ request }: ActionFunctionArgs) {
const form = await request.formData();
const title = String(form.get("title") ?? "").trim();
if (title.length < 3) {
return json({ error: "Слишком короткий заголовок" }, { status: 400 });
}
const note = await db.note.create({ data: { title } });
return redirect(`/notes/${note.id}`);
}
Возвращать объект из action можно для ошибок валидации, а redirect — для успешного сценария. Компонент читает ошибку через useActionData. Такой подход убирает отдельный слой клиентского состояния: источник правды остаётся на сервере.
Навигация и оптимистичный интерфейс
useNavigation показывает, выполняется ли переход или отправка формы. На этом строят индикаторы загрузки и блокировку кнопок. Для мгновенной реакции используют useFetcher: он отправляет запрос без смены маршрута и хранит состояние рядом с компонентом.
const navigation = useNavigation();
const busy = navigation.state !== "idle";
return (
<Form method="post">
<input name="title" />
<button disabled={busy}>
{busy ? "Сохраняем..." : "Сохранить"}
</button>
</Form>
);
Оптимистичное обновление делают через fetcher.formData: показывают новые данные до ответа сервера, а при ошибке откатываются к прежним. Важно не дублировать серверную логику на клиенте — она должна оставаться единственной.
Ошибки и границы
ErrorBoundary перехватывает ошибки своего маршрута и вложенных. Это позволяет показать локальное сообщение, не роняя весь layout. Для ожидаемых ситуаций лучше выбрасывать Response с кодом, а для неожиданных — оставлять обычное исключение.
export function ErrorBoundary() {
const error = useRouteError();
if (isRouteErrorResponse(error)) {
return <h1>{error.status} — {error.statusText}</h1>;
}
return <h1>Непредвиденная ошибка</h1>;
}
Отдельно стоит продумать 404 на уровне корня. Если маршрут не найден, Remix отдаст ответ с корректным статусом, но поисковики увидят его только при серверном рендеринге. Поэтому не стоит прятать ошибки за клиентским редиректом.
Потоковая отдача
Если часть данных грузится долго, loader возвращает defer, а компонент оборачивает промис в Await. Страница отдаётся сразу, тяжёлый блок догружается позже. Это улучшает восприятие, но усложняет обработку ошибок и кеширование.
export function loader() {
return defer({
note: db.note.findFirst(),
comments: db.comment.findMany(),
});
}
<Await resolve={comments} errorElement={<p>Недоступно</p>}>
{(items) => <CommentList items={items} />}
</Await>
Потоковую отдачу стоит включать точечно: для второстепенных блоков, которые не влияют на первый экран.
Сессии и заголовки
Сессия хранится в подписанной cookie. Секрет берут из переменной окружения, а не из репозитория. Для публичных страниц полезно управлять Cache-Control: персонализированные ответы кешировать нельзя, а статические — можно.
import { createCookieSessionStorage } from "@remix-run/node";
const storage = createCookieSessionStorage({
cookie: {
name: "session",
secure: process.env.NODE_ENV === "production",
httpOnly: true,
sameSite: "lax",
secrets: [process.env.SESSION_SECRET!],
path: "/",
},
});
Заголовки удобно задавать в loader через объект headers или в entry.server.tsx, если правило общее для всех ответов. CORS «на всякий случай» расширяет поверхность атаки.
Сборка и деплой
Проект на Vite собирается в два артефакта: клиентский бандл и серверный модуль. Запуск в продакшене выполняет Node.js или адаптер под конкретную платформу. Перед выкладкой стоит проверить, что переменные окружения заданы, а миграции базы применены.
npx create-remix@latest
npm run dev
npx remix vite:build
npm run start
- Проверить SSR: отключить JavaScript и пройти основные сценарии.
- Убедиться, что формы возвращают корректные статусы.
- Добавить ErrorBoundary на корень и ключевые маршруты.
- Ограничить размер загружаемых файлов на уровне action.
- Настроить логирование ошибок на сервере.
Чем Remix отличается от SPA
Классическое SPA грузит пустой каркас и добирает данные в браузере. Remix делает ставку на серверный рендеринг и нативную семантику браузера: ссылки, формы, коды ответов. Это даёт предсказуемость и лучшую индексацию, но требует сервера и дисциплины в работе с данными. Статический хостинг возможен только через пререндер отдельных маршрутов.
Стоит учитывать и миграционную картину: React Router v7 вобрал в себя API Remix, поэтому новые проекты часто начинают сразу с него. Код на loader, action и Form переносится почти без изменений, но импорты и конфигурация отличаются. Перед стартом лучше выбрать линию и зафиксировать её в документации команды.
Итог
Remix даёт цельную модель: маршрут описывает и разметку, и загрузку данных, и обработку отправки. Формы работают без JavaScript, ошибки имеют понятные границы, а вложенные layout не перезагружаются. Цена — необходимость сервера и аккуратность при работе с кешем и сессиями. Для приложений с формами, списками и авторизацией такой подход окупается меньшей связанностью клиентского кода.