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 не перезагружаются. Цена — необходимость сервера и аккуратность при работе с кешем и сессиями. Для приложений с формами, списками и авторизацией такой подход окупается меньшей связанностью клиентского кода.

Насколько публикация полезна?

Нажмите на звезду, чтобы оценить!

Средняя оценка 0 / 5. Количество оценок: 0

Оценок пока нет. Поставьте оценку первым.

Поделиться: Telegram ВКонтакте