Why Remix v2?
Remix v2 is a full-stack web framework built on React Router that embraces web standards. Nested routes, server-side loaders, and progressive enhancement make it a serious alternative to Next.js - especially when you want your app to work even without JavaScript.
Quick Start
npx create-remix@latest my-app
cd my-app
npm run dev
Loaders: Server-Side Data Fetching
// app/routes/posts.tsx
import type { LoaderFunctionArgs } from '@remix-run/node';
import { json } from '@remix-run/node';
import { useLoaderData } from '@remix-run/react';
export async function loader({ request }: LoaderFunctionArgs) {
const url = new URL(request.url);
const query = url.searchParams.get('q') || '';
const posts = await db.posts.findMany({
where: { title: { contains: query } },
orderBy: { createdAt: 'desc' },
take: 20,
});
return json({ posts, query });
}
export default function Posts() {
const { posts, query } = useLoaderData<typeof loader>();
return (
<div>
<h1>Posts</h1>
<form method="get">
<input name="q" defaultValue={query} placeholder="Search..." />
</form>
<ul>
{posts.map((post) => (
<li key={post.id}>{post.title}</li>
))}
</ul>
</div>
);
}
Actions: Server-Side Mutations
// app/routes/posts.new.tsx
import type { ActionFunctionArgs } from '@remix-run/node';
import { redirect } from '@remix-run/node';
import { Form, useActionData } from '@remix-run/react';
export async function action({ request }: ActionFunctionArgs) {
const formData = await request.formData();
const title = formData.get('title') as string;
const body = formData.get('body') as string;
const errors: Record<string, string> = {};
if (!title) errors.title = 'Title is required';
if (!body) errors.body = 'Body is required';
if (Object.keys(errors).length > 0) {
return json({ errors }, { status: 400 });
}
const post = await db.posts.create({ data: { title, body } });
return redirect(`/posts/${post.id}`);
}
export default function NewPost() {
const actionData = useActionData<typeof action>();
return (
<Form method="post">
<input name="title" />
{actionData?.errors?.title && <p>{actionData.errors.title}</p>}
<textarea name="body" />
{actionData?.errors?.body && <p>{actionData.errors.body}</p>}
<button type="submit">Create Post</button>
</Form>
);
}
Nested Routes with Layout
// app/routes/dashboard.tsx (layout)
import { Outlet, NavLink } from '@remix-run/react';
export default function DashboardLayout() {
return (
<div style={{ display: 'flex' }}>
<nav>
<NavLink to="/dashboard/overview">Overview</NavLink>
<NavLink to="/dashboard/settings">Settings</NavLink>
</nav>
<main>
<Outlet />
</main>
</div>
);
}
// app/routes/dashboard.overview.tsx
export async function loader() {
const stats = await getStats();
return json({ stats });
}
export default function Overview() {
const { stats } = useLoaderData<typeof loader>();
return <div>Total users: {stats.totalUsers}</div>;
}
Error Boundaries Per Route
export function ErrorBoundary() {
const error = useRouteError();
if (isRouteErrorResponse(error)) {
return <div>Error {error.status}: {error.statusText}</div>;
}
return <div>Something went wrong</div>;
}
Resource Routes (API endpoints)
// app/routes/api.posts.ts
export async function loader({ request }: LoaderFunctionArgs) {
const posts = await db.posts.findMany();
return json(posts);
}
export async function action({ request }: ActionFunctionArgs) {
if (request.method === 'DELETE') {
const { id } = await request.json();
await db.posts.delete({ where: { id } });
return json({ success: true });
}
}
Real-World Use Case
A team migrating from a Create React App SPA to Remix saw their Lighthouse score jump from 34 to 92. The secret? Server-side rendering with progressive enhancement. Forms work without JS, data loads in parallel through nested routes, and error boundaries isolate failures. No client-side state management library needed.
Building full-stack apps? I create custom data pipelines and automation tools. Check out my web scraping toolkit on Apify or reach me at spinov001@gmail.com for custom solutions.
Top comments (0)