DEV Community

Alex Spinov
Alex Spinov

Posted on

Remix v2 Has a Free Framework: Full-Stack Web Apps with Nested Routes and Server-Side Data Loading

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
Enter fullscreen mode Exit fullscreen mode

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>
  );
}
Enter fullscreen mode Exit fullscreen mode

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>
  );
}
Enter fullscreen mode Exit fullscreen mode

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>;
}
Enter fullscreen mode Exit fullscreen mode

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>;
}
Enter fullscreen mode Exit fullscreen mode

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 });
  }
}
Enter fullscreen mode Exit fullscreen mode

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)