Summary

  • Remix was built by the React Router authors and, as of RR v7, Remix v2 features ship in React Router’s framework mode. :contentReference[oaicite:0]{index=0}
  • Your app = nested routes + data loaders (reads) + actions (writes) + forms/fetchers. :contentReference[oaicite:1]{index=1}
  • Server handles data. Router coordinates fetch β†’ render β†’ revalidate on nav and mutations; tune with shouldRevalidate. :contentReference[oaicite:2]{index=2}

Mental model

  • Route = UI + data contract.
    • loader() β†’ fetch data for GET.
    • action() β†’ handle POST/PUT/PATCH/DELETE.
  • Nested routing gives nested data + layouts.
  • Forms submit to actions and work without JS; with JS you get instant SPA transitions. :contentReference[oaicite:3]{index=3}
  • Revalidation: after an action, Remix re-runs affected loaders. :contentReference[oaicite:4]{index=4}

Quick start (route module)

// app/routes/posts.$id.tsx
import { Form, useLoaderData, useActionData } from "@remix-run/react";
import { json, type ActionFunctionArgs, type LoaderFunctionArgs } from "@remix-run/node";

export async function loader({ params }: LoaderFunctionArgs) {
  const post = await db.post.find(params.id!);
  if (!post) throw new Response("Not found", { status: 404 });
  return json(post); // serialized to the client with proper headers
}

export async function action({ request, params }: ActionFunctionArgs) {
  const form = await request.formData();
  const body = form.get("body") as string;
  await db.post.update(params.id!, { body });
  return json({ ok: true });
}

export default function Post() {
  const post = useLoaderData<typeof loader>();
  const res = useActionData<typeof action>();
  return (
    <>
      <article>{post.body}</article>
      {res?.ok && <p role="status">Saved.</p>}
      <Form method="post" replace>
        <textarea name="body" defaultValue={post.body} />
        <button type="submit">Save</button>
      </Form>
    </>
  );
}

Patterns to use

  • Layouts via nesting: routes/_layout.tsx β†’ routes/_layout.posts.tsx β†’ routes/_layout.posts.$id.tsx.
  • Fetcher for background work (edit-in-place, optimistic UI). :contentReference[oaicite:5]{index=5}
  • ErrorBoundary per route with useRouteError (CatchBoundary deprecated). :contentReference[oaicite:6]{index=6}
  • Action-driven nav: return redirect("/posts") from action() after create/delete.

Testing hooks

  • Loaders/Actions = pure-ish functions β†’ unit test with fake request/params.
  • Routes: render with createMemoryRouter to test nested UI and transitions. :contentReference[oaicite:7]{index=7}

When to pick Remix

  • You want form-first, server-led data flow with SPA feel.
  • You need nested layouts and automatic data revalidation.
  • You prefer one router to coordinate UI, data, and navigation. :contentReference[oaicite:8]{index=8}