Разработка фронтенда сайта на Remix
Remix — React-фреймворк с акцентом на веб-стандарты, вложенные маршруты и прогрессивное улучшение. Отличается от Next.js: нет SSG (только SSR и CSR), вложенная маршрутизация с параллельной загрузкой данных, Form Actions основаны на Web Forms API.
Вложенная маршрутизация
Remix маршруты могут быть вложенными — дочерний маршрут рендерится внутри родительского <Outlet />:
app/routes/
├── _layout.tsx → layout для всех дочерних
├── _layout.blog.tsx → /blog
├── _layout.blog.$slug.tsx → /blog/:slug
└── _layout.dashboard._index.tsx → /dashboard
// _layout.tsx — родительский layout
export default function Layout() {
return (
<div>
<Nav />
<main>
<Outlet /> {/* Дочерний маршрут рендерится здесь */}
</main>
</div>
);
}
При навигации Remix загружает данные дочернего и родительского маршрутов параллельно — нет waterfall запросов.
Loaders и Actions
// app/routes/products.$id.tsx
import type { LoaderFunctionArgs, ActionFunctionArgs } from '@remix-run/node';
// Загрузка данных (SSR)
export async function loader({ params }: LoaderFunctionArgs) {
const product = await db.product.findUnique({ where: { id: params.id } });
if (!product) throw new Response('Not Found', { status: 404 });
return json({ product });
}
// Обработка формы
export async function action({ request, params }: ActionFunctionArgs) {
const formData = await request.formData();
const quantity = Number(formData.get('quantity'));
await addToCart(params.id, quantity);
return redirect('/cart');
}
export default function ProductPage() {
const { product } = useLoaderData<typeof loader>();
const actionData = useActionData<typeof action>();
return (
<div>
<h1>{product.name}</h1>
<Form method="post">
<input name="quantity" type="number" defaultValue={1} />
<button type="submit">В корзину</button>
</Form>
</div>
);
}
Прогрессивное улучшение
Remix Form работает без JavaScript. <Form> — это обычный <form> с method="post". С JS — fetch-based обновление без перезагрузки. Без JS — обычный POST → redirect → полная страница.
Error Boundaries и Catch Boundaries
export function ErrorBoundary() {
const error = useRouteError();
if (isRouteErrorResponse(error)) {
return <div>HTTP {error.status}: {error.data}</div>;
}
return <div>Что-то пошло не так</div>;
}
Error boundary — на уровне каждого маршрута. Ошибка в дочернем маршруте не ломает родительский layout.
Оптимистичные обновления
function LikeButton({ postId, liked }) {
const fetcher = useFetcher();
// Оптимистично показываем новое состояние
const optimisticLiked = fetcher.state !== 'idle'
? !liked
: liked;
return (
<fetcher.Form method="post" action="/api/like">
<input type="hidden" name="postId" value={postId} />
<button>{optimisticLiked ? '❤️' : '🤍'}</button>
</fetcher.Form>
);
}
Когда выбирать Remix
- Приложения с тяжёлыми формами и мутациями (e-commerce, дашборды)
- Когда важно прогрессивное улучшение (работа при медленном интернете)
- Команды, предпочитающие Web-стандарты (Request, Response, FormData)
Не подходит: контентные сайты с тысячами страниц (нет SSG).
Деплой
Remix через адаптеры деплоится на Vercel, Netlify, Cloudflare Workers, Express, Fly.io. @remix-run/cloudflare — edge deployment.
Сроки
Fullstack-приложение на Remix (10–20 маршрутов, формы, авторизация): 2–4 недели. Сложное приложение с nested layouts, real-time, оптимистичными обновлениями: 1–3 месяца.







