Skip to main content
React Router v7 is the latest evolution of the most popular routing library for React, now merged with Remix to create a powerful full-featured framework. It combines the best of both worlds: React Router’s proven routing with Remix’s data loading patterns, all while maintaining a simple, focused API. This guide shows you how to build production-ready React Router v7 apps with Mizu as your backend.

Why React Router v7?

React Router v7 launched in December 2024 as the merger of Remix and React Router, bringing modern framework features to React applications: Type-Safe Routing - Auto-generated types for routes, loaders, and actions with full end-to-end type safety. File-Based Routing - Organize routes by creating files in the routes/ directory. No manual route configuration needed. Built-In Data Loading - Loaders run before routes render, providing data with guaranteed type safety. Type-Safe Actions - Handle form submissions with type-safe actions that integrate with loaders. Error Boundaries - Per-route error handling with automatic error boundary generation. Optimized Builds - Automatic code splitting, route-based prefetching, and Vite-powered builds. Static Export - Can generate static HTML files for serving with Mizu backend. Progressive Enhancement - Works without JavaScript, enhances with React hydration.

React Router v7 vs Other Frameworks

FeatureReact Router v7React + RR v6Next.jsRemix
Type Safetyβœ… Auto-generated⚠️ Manual⚠️ Manualβœ… Built-in
RoutingFile-basedManualFile-basedFile-based
Data LoadingBuilt-in loadersManual fetchgetStaticPropsLoaders
Bundle Size~50kB~45kB~90kB~50kB
Learning Curve⚠️ Moderateβœ… Easy⚠️ Moderate⚠️ Moderate
Static Exportβœ… Yesβœ… Yesβœ… Yes❌ No
SSR⚠️ Optional❌ Noβœ… Yesβœ… Yes
Best forData-heavy SPAsSimple SPAsFull-stackSSR apps
CompanyRemix/ShopifyIndependentVercelRemix/Shopify
Choose React Router v7 when:
  • You want modern framework features with React Router familiarity
  • Type safety from routes to data is important
  • You need built-in data loading patterns
  • File-based routing appeals to you
  • You’re building a data-heavy SPA with static export
  • You want the option to add SSR later
Choose something else when:
  • You need the simplest possible setup β†’ Use React + React Router v6
  • You need full SSR now β†’ Use Next.js or full Remix
  • Bundle size is critical β†’ Use Preact
  • You prefer Vue β†’ Use Vue Router or Nuxt

Quick Start

Create a new React Router v7 project with the CLI:
mizu new ./my-app --template frontend/reactrouter
cd my-app
make install
make dev
Visit http://localhost:3000 to see your app!

Project Structure

my-app/
β”œβ”€β”€ cmd/
β”‚   └── server/
β”‚       └── main.go                    # Go entry point
β”œβ”€β”€ app/
β”‚   └── server/
β”‚       β”œβ”€β”€ app.go                     # Mizu app configuration
β”‚       β”œβ”€β”€ config.go                  # Server configuration
β”‚       └── routes.go                  # API routes (Go)
β”œβ”€β”€ frontend/                            # React Router v7 application
β”‚   β”œβ”€β”€ app/
β”‚   β”‚   β”œβ”€β”€ root.tsx                   # Root layout
β”‚   β”‚   β”œβ”€β”€ routes.ts                  # Route definitions
β”‚   β”‚   β”œβ”€β”€ routes/                    # Route modules
β”‚   β”‚   β”‚   β”œβ”€β”€ _layout.tsx            # Main layout
β”‚   β”‚   β”‚   β”œβ”€β”€ _index.tsx             # Home page (/)
β”‚   β”‚   β”‚   β”œβ”€β”€ about.tsx              # About page (/about)
β”‚   β”‚   β”‚   └── users/
β”‚   β”‚   β”‚       β”œβ”€β”€ _layout.tsx        # Users layout
β”‚   β”‚   β”‚       β”œβ”€β”€ _index.tsx         # Users list (/users)
β”‚   β”‚   β”‚       └── $id.tsx            # User detail (/users/:id)
β”‚   β”‚   └── styles/
β”‚   β”‚       └── app.css                # Global styles
β”‚   β”œβ”€β”€ public/
β”‚   β”‚   └── favicon.ico                # Public assets
β”‚   β”œβ”€β”€ package.json                   # npm dependencies
β”‚   β”œβ”€β”€ vite.config.ts                 # Vite configuration
β”‚   β”œβ”€β”€ tsconfig.json                  # TypeScript config
β”‚   └── react-router.config.ts         # React Router config
β”œβ”€β”€ dist/                              # Built files (after build)
β”œβ”€β”€ go.mod
└── Makefile

How React Router v7 Works with Mizu

React Router v7 integrates seamlessly with Mizu through static export mode:
Development Mode
    ↓
β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
β”‚ React Router Dev (5173)     β”‚
β”‚ - Hot Module Replacement    β”‚
β”‚ - TypeScript compilation    β”‚
β”‚ - Type generation           β”‚
β”‚ - Fast Vite builds          β”‚
β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜
              β”‚
              ↓
β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
β”‚ Mizu Server (3000)          β”‚
β”‚ - Proxies to RR dev server  β”‚
β”‚ - Handles /api requests     β”‚
β”‚ - Serves API endpoints      β”‚
β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜

Production Mode
    ↓
β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
β”‚ react-router build          β”‚
β”‚ - Static HTML generation    β”‚
β”‚ - Type-safe builds          β”‚
β”‚ - Code splitting            β”‚
β”‚ - Optimized assets          β”‚
β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜
              ↓
β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
β”‚ Embedded in Go Binary       β”‚
β”‚ - All files in single bin   β”‚
β”‚ - Served by Mizu            β”‚
β”‚ - No Node.js needed         β”‚
β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜
At runtime in production:
  1. User requests http://yourdomain.com
  2. Mizu serves index.html from embedded FS
  3. Browser loads React Router bundles
  4. React Router hydrates and takes over
  5. Client-side routing handles navigation
  6. API calls go to Mizu Go handlers

Backend Setup

app/server/app.go

package server

import (
    "embed"
    "io/fs"

    "github.com/go-mizu/mizu"
    "github.com/go-mizu/mizu/frontend"
)

//go:embed all:../../dist
var distFS embed.FS

func New(cfg *Config) *mizu.App {
    app := mizu.New()

    // API routes come first
    setupRoutes(app)

    // Frontend middleware (handles all non-API routes)
    dist, _ := fs.Sub(distFS, "dist")
    app.Use(frontend.WithOptions(frontend.Options{
        Mode:        frontend.ModeAuto,       // Auto-detect dev/prod
        FS:          dist,                     // Embedded files
        Root:        "./dist",                 // Fallback in dev
        DevServer:   "http://localhost:" + cfg.DevPort,
        IgnorePaths: []string{"/api"},        // Don't proxy /api
    }))

    return app
}

app/server/routes.go

package server

import "github.com/go-mizu/mizu"

func setupRoutes(app *mizu.App) {
    api := app.Prefix("/api")

    // User endpoints
    api.Get("/users", handleUsers)
    api.Post("/users", createUser)
    api.Get("/users/{id}", getUser)
    api.Put("/users/{id}", updateUser)
    api.Delete("/users/{id}", deleteUser)
}

func handleUsers(c *mizu.Ctx) error {
    users := []map[string]any{
        {"id": 1, "name": "Alice", "email": "[email protected]", "role": "admin"},
        {"id": 2, "name": "Bob", "email": "[email protected]", "role": "user"},
    }
    return c.JSON(200, users)
}

func getUser(c *mizu.Ctx) error {
    id := c.Param("id")
    user := map[string]any{
        "id":    id,
        "name":  "User " + id,
        "email": "user" + id + "@example.com",
        "role":  "user",
    }
    return c.JSON(200, user)
}

Frontend Setup

File-Based Routing

React Router v7 uses file-based routing conventions:
app/routes/
β”œβ”€β”€ _layout.tsx           β†’  Wraps all routes
β”œβ”€β”€ _index.tsx            β†’  / (home)
β”œβ”€β”€ about.tsx             β†’  /about
β”œβ”€β”€ users/
β”‚   β”œβ”€β”€ _layout.tsx       β†’  Layout for /users/*
β”‚   β”œβ”€β”€ _index.tsx        β†’  /users
β”‚   └── $id.tsx           β†’  /users/:id (dynamic)
Naming conventions:
  • _index.tsx β†’ Index route (matches parent path exactly)
  • _layout.tsx β†’ Layout component (wraps child routes)
  • $id.tsx β†’ Dynamic segment (captures param)
  • .tsx β†’ Route file

Route Configuration

app/routes.ts

Define your route tree programmatically:
import {
  type RouteConfig,
  route,
  layout,
  index,
} from "@react-router/dev/routes";

export default [
  layout("routes/_layout.tsx", [
    index("routes/_index.tsx"),
    route("about", "routes/about.tsx"),
    route("users", "routes/users/_layout.tsx", [
      index("routes/users/_index.tsx"),
      route(":id", "routes/users/$id.tsx"),
    ]),
  ]),
] satisfies RouteConfig;
This creates the route structure:
  • / β†’ _layout β†’ _index
  • /about β†’ _layout β†’ about
  • /users β†’ _layout β†’ users/_layout β†’ users/_index
  • /users/:id β†’ _layout β†’ users/_layout β†’ users/$id

Root Component

app/root.tsx

import {
  Links,
  Meta,
  Outlet,
  Scripts,
  ScrollRestoration,
} from "react-router";

import type { Route } from "./+types/root";
import stylesheet from "./styles/app.css?url";

export const links: Route.LinksFunction = () => [
  { rel: "stylesheet", href: stylesheet },
];

export function Layout({ children }: { children: React.ReactNode }) {
  return (
    <html lang="en">
      <head>
        <meta charSet="utf-8" />
        <meta name="viewport" content="width=device-width, initial-scale=1" />
        <Meta />
        <Links />
      </head>
      <body>
        {children}
        <ScrollRestoration />
        <Scripts />
      </body>
    </html>
  );
}

export default function Root() {
  return <Outlet />;
}
Why this structure?
  • Layout wraps all pages (renders once)
  • Meta renders all route meta tags
  • Links renders all route link tags (stylesheets, etc.)
  • Scripts includes React Router scripts
  • ScrollRestoration remembers scroll positions

Data Loading with Loaders

React Router v7’s killer feature is type-safe data loading:

Basic Loader

import { useLoaderData } from "react-router";
import type { Route } from "./+types/_index";

// Loader runs before component renders
export async function loader() {
  const res = await fetch("/api/users");
  const users = await res.json();
  return { users };
}

// Component receives fully typed loader data
export default function Users({ loaderData }: Route.ComponentProps) {
  const { users } = loaderData;  // Typed automatically!

  return (
    <ul>
      {users.map(user => (
        <li key={user.id}>{user.name}</li>
      ))}
    </ul>
  );
}

Loader with Params

import type { Route } from "./+types/$id";

interface User {
  id: string;
  name: string;
  email: string;
}

export async function loader({ params }: Route.LoaderArgs) {
  // params.id is typed as string
  const res = await fetch(`/api/users/${params.id}`);

  if (!res.ok) {
    throw new Response("Not Found", { status: 404 });
  }

  const user: User = await res.json();
  return { user };
}

export default function UserDetail({ loaderData }: Route.ComponentProps) {
  // loaderData.user is typed as User!
  return (
    <div>
      <h1>{loaderData.user.name}</h1>
      <p>{loaderData.user.email}</p>
    </div>
  );
}
Loader benefits:
  • Data loads before rendering (no loading states)
  • Full type safety from loader to component
  • Errors handled by error boundaries
  • Can be cached and revalidated
  • Run in parallel for nested routes

Error Handling

Each route can have its own error boundary:
export function ErrorBoundary({ error }: Route.ErrorBoundaryProps) {
  return (
    <div className="error">
      <h1>Oops!</h1>
      <p>
        {error instanceof Error
          ? error.message
          : "Something went wrong"}
      </p>
      <a href="/users">Back to Users</a>
    </div>
  );
}
Errors from loaders are automatically caught!

Meta Tags and SEO

Define meta tags per route:
export function meta({ data, params }: Route.MetaArgs) {
  return [
    { title: `${data.user.name} - My App` },
    { name: "description", content: `Profile of ${data.user.name}` },
    { property: "og:title", content: data.user.name },
    { property: "og:image", content: data.user.avatar },
  ];
}
Meta function features:
  • Access to loader data (data)
  • Access to URL params (params)
  • Fully typed with Route.MetaArgs
  • Rendered in <head> by <Meta /> component
import { Link } from "react-router";

function Navigation() {
  return (
    <nav>
      <Link to="/">Home</Link>
      <Link to="/about">About</Link>
      <Link to="/users">Users</Link>

      {/* With state */}
      <Link to="/users/1" state={{ from: "nav" }}>
        User 1
      </Link>

      {/* Relative navigation */}
      <Link to="../">Back</Link>

      {/* With className function */}
      <Link
        to="/users"
        className={({ isActive }) => isActive ? "active" : ""}
      >
        Users
      </Link>
    </nav>
  );
}

Programmatic Navigation

import { useNavigate } from "react-router";

function CreateUser() {
  const navigate = useNavigate();

  const handleSubmit = async (data) => {
    const user = await createUser(data);
    navigate(`/users/${user.id}`);  // Navigate to new user
  };

  return <form onSubmit={handleSubmit}>...</form>;
}

// Navigate back
function BackButton() {
  const navigate = useNavigate();
  return <button onClick={() => navigate(-1)}>Back</button>;
}

Forms and Actions

React Router v7 provides type-safe form handling:

Basic Form

import { Form, useActionData } from "react-router";
import type { Route } from "./+types/create";

interface ActionData {
  errors?: {
    name?: string;
    email?: string;
  };
  user?: {
    id: number;
    name: string;
    email: string;
  };
}

export async function action({ request }: Route.ActionArgs) {
  const formData = await request.formData();

  const name = formData.get("name") as string;
  const email = formData.get("email") as string;

  // Validate
  const errors: ActionData["errors"] = {};
  if (!name) errors.name = "Name is required";
  if (!email) errors.email = "Email is required";

  if (Object.keys(errors).length) {
    return { errors };
  }

  // Create user
  const res = await fetch("/api/users", {
    method: "POST",
    headers: { "Content-Type": "application/json" },
    body: JSON.stringify({ name, email }),
  });

  const user = await res.json();
  return { user };
}

export default function CreateUser() {
  const actionData = useActionData<typeof action>();

  return (
    <Form method="post">
      <div>
        <label>Name</label>
        <input name="name" />
        {actionData?.errors?.name && <span>{actionData.errors.name}</span>}
      </div>

      <div>
        <label>Email</label>
        <input name="email" />
        {actionData?.errors?.email && <span>{actionData.errors.email}</span>}
      </div>

      <button type="submit">Create</button>
    </Form>
  );
}
Form benefits:
  • Uses Web Forms API (works without JS)
  • Automatic revalidation after submission
  • Loading states via useNavigation()
  • Progressive enhancement
  • Type-safe action data

Optimistic UI

import { useFetcher } from "react-router";

function TodoItem({ todo }) {
  const fetcher = useFetcher();

  // Optimistic UI: show pending state immediately
  const isCompleted = fetcher.formData
    ? fetcher.formData.get("completed") === "true"
    : todo.completed;

  return (
    <fetcher.Form method="post" action={`/todos/${todo.id}`}>
      <input
        type="checkbox"
        name="completed"
        value="true"
        checked={isCompleted}
        onChange={(e) => e.currentTarget.form?.requestSubmit()}
      />
      <span style={{ textDecoration: isCompleted ? "line-through" : "none" }}>
        {todo.title}
      </span>
    </fetcher.Form>
  );
}

TypeScript Integration

React Router v7 generates types automatically:

Type Generation

Run npm run typecheck to:
  1. Generate route types in app/+types/
  2. Type check your entire app

Using Generated Types

import type { Route } from "./+types/$id";

// Loader args are typed
export async function loader({ params }: Route.LoaderArgs) {
  // params.id exists and is typed as string
}

// Action args are typed
export async function action({ request }: Route.ActionArgs) {
  // request is typed as Request
}

// Component props are typed
export default function MyRoute({ loaderData }: Route.ComponentProps) {
  // loaderData has the exact shape from your loader!
}

// Meta args are typed
export function meta({ data, params }: Route.MetaArgs) {
  // data is your loader return type
  // params are your route params
}

// Error boundary props are typed
export function ErrorBoundary({ error }: Route.ErrorBoundaryProps) {
  // error is typed
}
No manual type annotations needed!

Configuration

react-router.config.ts

import type { Config } from "@react-router/dev/config";

export default {
  // Static export mode (no SSR)
  ssr: false,

  // Build output (relative to frontend/)
  buildDirectory: "../dist",

  // Server build file
  serverBuildFile: "index.js",

  // Vite config
  vite: {
    server: {
      port: 5173,
      strictPort: true,
    },
  },

  // Prerender routes (optional)
  async prerender() {
    return ["/", "/about"];
  },
} satisfies Config;

vite.config.ts

import { reactRouter } from "@react-router/dev/vite";
import { defineConfig } from "vite";

export default defineConfig({
  plugins: [reactRouter()],

  // Customize Vite if needed
  resolve: {
    alias: {
      "~": "/app",
    },
  },
});

Development Workflow

Start Development

# Using Makefile (recommended)
make dev

# Or manually
# Terminal 1: React Router dev
cd frontend && npm run dev

# Terminal 2: Mizu server
go run cmd/server/main.go
Visit http://localhost:3000

Making Changes

Frontend changes:
  1. Edit any .tsx file
  2. Save the file
  3. Browser updates instantly (HMR)
  4. Types regenerate automatically
Backend changes:
  1. Edit .go files
  2. Stop server (Ctrl+C)
  3. Restart with go run cmd/server/main.go
Or use air for auto-reload.

Type Checking

make typecheck

# Or manually
cd frontend && npm run typecheck
This:
  1. Generates route types
  2. Runs TypeScript compiler
  3. Shows any type errors

Building for Production

Build the complete app:
make build
This:
  1. Runs react-router build in frontend/
  2. Generates static HTML for all routes
  3. Outputs to dist/ directory
  4. Ready to embed in Go binary
Run in production:
MIZU_ENV=production ./bin/server

Build Optimizations

Code splitting: React Router automatically splits code by route. Each route loads only when navigated to. Prefetching:
<Link to="/users" prefetch="intent">
  Users
</Link>
Options:
  • intent: Prefetch on hover
  • render: Prefetch on render
  • none: No prefetch (default)
Bundle analysis:
cd frontend
npm run build -- --profile
Check dist/ for bundle sizes.

Advanced Features

Nested Layouts

Create shared layouts for route groups:
routes/
β”œβ”€β”€ admin/
β”‚   β”œβ”€β”€ _layout.tsx        # Admin layout
β”‚   β”œβ”€β”€ dashboard.tsx      # /admin/dashboard
β”‚   └── users.tsx          # /admin/users
// routes/admin/_layout.tsx
export default function AdminLayout() {
  return (
    <div className="admin">
      <aside>Admin Sidebar</aside>
      <main>
        <Outlet />
      </main>
    </div>
  );
}

Route Grouping

Use . for pathless layouts:
routes/
β”œβ”€β”€ auth._index.tsx        # /auth (no layout)
β”œβ”€β”€ auth.login.tsx         # /auth/login (no layout)
└── _layout.tsx            # Main layout (for other routes)

Splat Routes

Catch-all routes:
// routes/$.tsx
export default function NotFound() {
  return <h1>404 - Page Not Found</h1>;
}
Matches: /anything/not/matched

Resource Routes

API-only routes (no UI):
// routes/api.users.ts
export async function loader() {
  const users = await db.users.findMany();
  return Response.json(users);
}

Real-World Example: User Management

Complete example with CRUD operations:

List Users

// routes/users/_index.tsx
import { Link, useLoaderData } from "react-router";
import type { Route } from "./+types/_index";

interface User {
  id: number;
  name: string;
  email: string;
  role: string;
}

export async function loader() {
  const res = await fetch("/api/users");
  const users: User[] = await res.json();
  return { users };
}

export function meta({}: Route.MetaArgs) {
  return [{ title: "Users" }];
}

export default function Users({ loaderData }: Route.ComponentProps) {
  return (
    <div>
      <h1>Users</h1>
      <div className="user-grid">
        {loaderData.users.map(user => (
          <Link key={user.id} to={`/users/${user.id}`} className="user-card">
            <div className="avatar">{user.name[0]}</div>
            <div>
              <h3>{user.name}</h3>
              <p>{user.email}</p>
              <span className={`badge-${user.role}`}>{user.role}</span>
            </div>
          </Link>
        ))}
      </div>
    </div>
  );
}

View User Detail

// routes/users/$id.tsx
import { useLoaderData, useNavigate } from "react-router";
import type { Route } from "./+types/$id";

export async function loader({ params }: Route.LoaderArgs) {
  const res = await fetch(`/api/users/${params.id}`);

  if (!res.ok) {
    throw new Response("User not found", { status: 404 });
  }

  const user = await res.json();
  return { user };
}

export function meta({ data }: Route.MetaArgs) {
  return [{ title: `${data?.user.name || "User"}` }];
}

export default function UserDetail({ loaderData }: Route.ComponentProps) {
  const navigate = useNavigate();

  return (
    <div>
      <button onClick={() => navigate(-1)}>← Back</button>

      <div className="user-profile">
        <div className="avatar-large">{loaderData.user.name[0]}</div>
        <h2>{loaderData.user.name}</h2>
        <p>{loaderData.user.email}</p>
        <span className="badge">{loaderData.user.role}</span>
      </div>
    </div>
  );
}

export function ErrorBoundary({ error }: Route.ErrorBoundaryProps) {
  return (
    <div>
      <h1>Error Loading User</h1>
      <p>{error instanceof Error ? error.message : "Unknown error"}</p>
      <a href="/users">Back to Users</a>
    </div>
  );
}

Performance Tips

1. Use Loaders for Data

❌ Don’t fetch in components:
export default function Users() {
  const [users, setUsers] = useState([]);

  useEffect(() => {
    fetch("/api/users").then(r => r.json()).then(setUsers);
  }, []);

  return <div>{/* ... */}</div>;
}
βœ… Use loaders:
export async function loader() {
  const res = await fetch("/api/users");
  return { users: await res.json() };
}

export default function Users({ loaderData }: Route.ComponentProps) {
  return <div>{/* loaderData.users ready */}</div>;
}

2. Prefetch Important Routes

<Link to="/dashboard" prefetch="intent">
  Dashboard
</Link>

3. Use React.memo for Static Content

const UserCard = memo(({ user }) => (
  <div className="user-card">
    <h3>{user.name}</h3>
    <p>{user.email}</p>
  </div>
));

4. Code Split Large Components

import { lazy, Suspense } from "react";

const Chart = lazy(() => import("./Chart"));

export default function Dashboard() {
  return (
    <div>
      <h1>Dashboard</h1>
      <Suspense fallback={<div>Loading chart...</div>}>
        <Chart />
      </Suspense>
    </div>
  );
}

Troubleshooting

Types Not Generating

Problem: +types/ folder not updating Solution:
cd frontend
npm run typecheck
This regenerates types from your routes.

404 on Page Refresh

Problem: Direct URLs work in dev, 404 in production Solution: Ensure React Router config has ssr: false:
// react-router.config.ts
export default {
  ssr: false,  // Important for static export
} satisfies Config;

HMR Not Working

Problem: Changes don’t appear in browser Solution: Check Vite dev server is running on correct port:
// react-router.config.ts
export default {
  vite: {
    server: {
      port: 5173,      // Must match
      strictPort: true, // Fail if port taken
    },
  },
} satisfies Config;

Build Errors

Problem: react-router build fails Solution:
  1. Check all imports are valid
  2. Run npm run typecheck to find type errors
  3. Check loader/action return types
  4. Ensure all route files export default component

Loader Data is Undefined

Problem: loaderData is undefined in component Solution:
  1. Ensure loader exports are async functions
  2. Check loader returns an object (not just a value)
  3. Verify loader path in routes.ts is correct

Deployment

Production Build

# Build everything
make build

# Build Go binary
go build -o bin/server ./cmd/server

# Run production server
MIZU_ENV=production ./bin/server

Docker Deployment

# Build stage
FROM node:20 AS frontend
WORKDIR /app
COPY frontend/package*.json ./
RUN npm install
COPY frontend/ ./
RUN npm run build

# Go build stage
FROM golang:1.22 AS backend
WORKDIR /app
COPY go.mod go.sum ./
RUN go mod download
COPY . ./
COPY --from=frontend /app/../dist ./dist
RUN go build -o server ./cmd/server

# Runtime stage
FROM debian:bookworm-slim
COPY --from=backend /app/server /server
EXPOSE 3000
CMD ["/server"]

Migration from React Router v6

Migrating is straightforward:

1. Update Dependencies

npm install react-router@latest
npm install -D @react-router/dev @react-router/node @react-router/serve

2. Move Routes to Files

Before (v6):
<Routes>
  <Route path="/" element={<Home />} />
  <Route path="/about" element={<About />} />
  <Route path="/users/:id" element={<User />} />
</Routes>
After (v7):
app/routes/
β”œβ”€β”€ _index.tsx   # Home
β”œβ”€β”€ about.tsx    # About
└── users/
    └── $id.tsx  # User

3. Convert to Loaders

Before:
function Users() {
  const [users, setUsers] = useState([]);

  useEffect(() => {
    fetch("/api/users").then(r => r.json()).then(setUsers);
  }, []);

  return <div>{users.map(...)}</div>;
}
After:
export async function loader() {
  const res = await fetch("/api/users");
  return { users: await res.json() };
}

export default function Users({ loaderData }: Route.ComponentProps) {
  return <div>{loaderData.users.map(...)}</div>;
}

4. Add Type Generation

Create routes.ts:
import { type RouteConfig, index, route } from "@react-router/dev/routes";

export default [
  index("routes/_index.tsx"),
  route("about", "routes/about.tsx"),
  route("users/:id", "routes/users/$id.tsx"),
] satisfies RouteConfig;

When to Choose React Router v7

Choose React Router v7 When:

βœ… You want modern framework features without SSR complexity βœ… Type-safe routing and data loading is important βœ… You’re familiar with React Router and want the next evolution βœ… File-based routing appeals to you βœ… You need built-in data loading patterns βœ… Static export + Go backend is your deployment model βœ… You might want to add SSR later (easy upgrade path)

Choose Something Else When:

  • Simplest possible setup β†’ Use React + React Router v6
  • Need SSR now β†’ Use full Remix or Next.js
  • Prefer manual routing β†’ Use React + React Router v6
  • Bundle size is critical β†’ Use Preact
  • Team prefers Vue β†’ Use Vue Router or Nuxt

Next Steps