React logoReactv7INTERMEDIATE

React Router v7

React Router v7 cheat sheet covering Library Mode, Framework Mode, data loading, actions, nested routes, and code examples.

12 min read
react-routerroutingreactspanavigationremixframework

Sign in to mark items as known and track your progress.

Sign in

Installation & Setup

Setting up React Router v7 in Library Mode and Framework Mode

Installation & Basic Setup

Installing React Router v7 and choosing between Library and Framework modes

typescript
# Library Mode (Classic SPA)
npm install react-router

# Framework Mode (Full-stack with Vite)
npm create vite@latest my-app -- --template react
npm install react-router
npm install -D @react-router/dev

// vite.config.ts (Framework Mode)
import { reactRouter } from "@react-router/dev/vite"
import { defineConfig } from "vite"

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

// Library Mode: main.tsx
import { createBrowserRouter, RouterProvider } from 'react-router'

const router = createBrowserRouter([
  {
    path: "/",
    element: <Root />,
    children: [
      { path: "about", element: <About /> },
      { path: "contact", element: <Contact /> }
    ]
  }
])

createRoot(document.getElementById('root')!).render(
  <RouterProvider router={router} />
)
💡 Framework Mode includes SSR, data loading, and actions out of the box
⚡ Library Mode is lighter for SPAs, Framework Mode for full-stack apps
📌 Framework Mode uses file-based routing by default
🟢 Start with Library Mode if migrating from v6
setupinstallationconfiguration

Routing Basics

Core routing concepts and components

Route Configuration

Defining routes and nested routes

typescript
// Library Mode: Object-based routes
const router = createBrowserRouter([
  {
    path: "/",
    element: <Root />,
    loader: rootLoader,
    action: rootAction,
    errorElement: <ErrorBoundary />,
    children: [
      {
        index: true, // Default child route
        element: <Index />,
      },
      {
        path: "teams",
        element: <Teams />,
        loader: teamsLoader,
        children: [
          {
            path: ":teamId",
            element: <Team />,
            loader: teamLoader,
          }
        ]
      },
      {
        path: "*",
        element: <NotFound />
      }
    ]
  }
])

// Framework Mode: File-based routing
// app/routes/_index.tsx → /
// app/routes/about.tsx → /about
// app/routes/blog.$id.tsx → /blog/:id
// app/routes/$.tsx → catch-all
💡 Framework Mode uses file-based routing with naming conventions
⚡ Lazy loading routes reduces initial bundle size
📌 Loaders run in parallel for nested routes
🟢 Use index routes for default child components
routingconfigurationnested

Navigation Components

Link, NavLink, and programmatic navigation

typescript
import { Link, NavLink, useNavigate, Navigate } from 'react-router'

// Basic navigation
<Link to="/about">About</Link>
<Link to="/users/123">User Profile</Link>
<Link to=".." relative="path">Go Up</Link>

// NavLink with active styles
<NavLink
  to="/tasks"
  className={({ isActive, isPending }) =>
    isActive ? "active" : isPending ? "pending" : ""
  }
>
  Tasks
</NavLink>

// Programmatic navigation
function LoginForm() {
  const navigate = useNavigate()

  const handleSubmit = async (e) => {
    e.preventDefault()
    await login()
    navigate("/dashboard", { replace: true })
  }

  return <form onSubmit={handleSubmit}>...</form>
}
💡 NavLink automatically adds active class for current route
⚡ Use navigate with replace: true to avoid back button issues
📌 Pass state through navigation for context
🟢 Framework Mode prefetches via <Link prefetch="intent"> (also render/viewport)
navigationlinknavlink

Data Loading

Loaders, data fetching, and streaming

Loaders & Data Fetching

Loading data before rendering components

typescript
// Basic loader
export async function loader({ params }) {
  const user = await fetchUser(params.userId)
  if (!user) {
    throw new Response("Not Found", { status: 404 })
  }
  return { user }
}

// Using loaded data
import { useLoaderData } from 'react-router'

export default function User() {
  const { user } = useLoaderData<typeof loader>()
  return <h1>{user.name}</h1>
}

// Defer streaming data
import { defer, Await } from 'react-router'
import { Suspense } from 'react'

export async function loader() {
  return defer({
    critical: await fetchCriticalData(),
    slow: fetchSlowData() // Don't await
  })
}

export default function Page() {
  const { critical, slow } = useLoaderData<typeof loader>()

  return (
    <div>
      <h1>{critical.title}</h1>
      <Suspense fallback={<Loading />}>
        <Await resolve={slow}>
          {(data) => <SlowComponent data={data} />}
        </Await>
      </Suspense>
    </div>
  )
}
💡 Use defer() to stream non-critical data for faster initial loads
⚡ Loaders run in parallel for nested routes automatically
📌 Throw responses for errors and redirects
🟢 Always handle loading states with Suspense for deferred data
loadersdatadeferstreaming

Revalidation & Fetchers

Data revalidation and non-navigation data fetching

typescript
import { useFetcher, useFetchers, useRevalidator } from 'react-router'

// Fetcher for non-navigation data loading
function NewsletterSignup() {
  const fetcher = useFetcher()

  return (
    <fetcher.Form method="post" action="/newsletter">
      <input name="email" type="email" />
      <button type="submit">
        {fetcher.state === "submitting" ? "Subscribing..." : "Subscribe"}
      </button>
    </fetcher.Form>
  )
}

// Manual revalidation
function RefreshButton() {
  const revalidator = useRevalidator()

  return (
    <button
      onClick={() => revalidator.revalidate()}
      disabled={revalidator.state === "loading"}
    >
      {revalidator.state === "loading" ? "Refreshing..." : "Refresh"}
    </button>
  )
}
💡 Fetchers are perfect for non-navigation data operations
⚡ Use fetcher.Form for forms that don't navigate
📌 shouldRevalidate gives fine control over data refresh
🟢 Fetchers with keys persist across route changes
fetcherrevalidationdata

Forms & Actions

Form handling, actions, and mutations

Forms & Actions

Handling forms with actions and progressive enhancement

typescript
import { Form, useActionData, useNavigation } from 'react-router'

// Action function
export async function action({ request, params }) {
  const formData = await request.formData()
  const title = formData.get("title")

  try {
    const post = await createPost({ title })
    return redirect(\`/posts/\${post.id}\`)
  } catch (error) {
    return { error: error.message }
  }
}

// Form component
export default function NewPost() {
  const actionData = useActionData<typeof action>()
  const navigation = useNavigation()
  const isSubmitting = navigation.state === "submitting"

  return (
    <Form method="post">
      <input name="title" required />
      {actionData?.error && (
        <p className="error">{actionData.error}</p>
      )}
      <button disabled={isSubmitting}>
        {isSubmitting ? "Creating..." : "Create Post"}
      </button>
    </Form>
  )
}
💡 Forms work without JavaScript - progressive enhancement
⚡ Use fetcher.Form for non-navigation forms
📌 Actions handle POST, PUT, PATCH, DELETE methods
🟢 Return json() with validation errors for form feedback
formsactionsmutations

Error Handling

Error boundaries and error handling patterns

Error Boundaries

Handling errors in loaders, actions, and components

typescript
import { useRouteError, isRouteErrorResponse } from 'react-router'

// Error boundary component
export function ErrorBoundary() {
  const error = useRouteError()

  if (isRouteErrorResponse(error)) {
    return (
      <div>
        <h1>{error.status} {error.statusText}</h1>
        <p>{error.data}</p>
      </div>
    )
  }

  return (
    <div>
      <h1>Oops!</h1>
      <p>{error?.message || "Unknown error"}</p>
    </div>
  )
}

// Throwing errors in loaders
export async function loader({ params }) {
  const post = await fetchPost(params.id)

  if (!post) {
    throw new Response("Not Found", { status: 404 })
  }

  return { post }
}
💡 Error boundaries catch errors in loaders, actions, and rendering
⚡ Use isRouteErrorResponse to handle thrown Responses
📌 Errors bubble up to nearest errorElement
🟢 Throw Response objects for HTTP-style errors
errorserror-boundaryhandling

Framework Mode Features

Server-side rendering, streaming, and framework-specific features

SSR & Streaming

Server-side rendering and HTML streaming in Framework Mode

typescript
// app/root.tsx - Framework Mode root
import {
  Links,
  Meta,
  Outlet,
  Scripts,
  ScrollRestoration,
  useLoaderData
} from "react-router"

export function Layout({ children }) {
  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>
  )
}

// Server-only loader
export async function loader() {
  // This only runs on the server
  const data = await db.query("SELECT * FROM products")
  return { products: data }
}

export default function App() {
  const { products } = useLoaderData<typeof loader>()
  return <Outlet />
}
💡 Framework Mode enables SSR and streaming out of the box
⚡ Use defer() to stream non-critical data
📌 .server files only run on server, never bundled to client
🟢 Route modules run on server and client; use .server/.client file suffixes to control bundling (RSC + "use client" is a separate experimental feature)
ssrstreamingframeworkserver