React Router

React Router is a popular routing library for React single-page applications (SPAs).

The v7 release has merged features from Remix, introducing incrementally adoptable enhancements like code splitting, data loading, actions, server rendering, static pre-rendering, pending states, optimistic UI, and React Server Components (RSC).

npx create-react-router@latest my-app
cd my-app

You will see the following project structure:

├── README.md
├── app
│   ├── app.css
│   ├── root.tsx
│   ├── routes
│   │   └── home.tsx
│   └── routes.ts
├── "package-lock.json"
├── package.json
├── postcss.config.js
├── public
│   ├── favicon.ico
│   ├── "logo-dark.svg"
│   └── "logo-light.svg"
├── tailwind.config.ts
├── tsconfig.json
└── vite.config.ts

Install dependencies and start the development server:

npm i
npm run dev

Routing

Routing rules are defined in app/routes.ts:

import {
  type RouteConfig,
  route,
  index,
} from "@react-router/dev/routes"

export default [
  index("routes/home.tsx"),
  route("about", "routes/about.tsx"),
] satisfies RouteConfig;

It's pretty straight forward:

  • index(file) defines the default page.
  • route(path, file) maps a path to a .tsx file.

Maybe we can simply use route('', 'routes/home.tsx') for the index, make it even more simpler.

Nested Routes

Nested routes are defined with child routes rendered through <Outlet /> in the parent route:

route("dashboard", "dashboard.tsx", [
  index("home.tsx"),
  route("settings", "settings.tsx"),
])

In dashboard.tsx:

import { Outlet } from "react-router"

export default function Dashboard() {
  return (
    <div>
      <h1>Dashboard</h1>
      <Outlet />
    </div>
  )
}

Layouts

Layouts are similar to nested routes, except they have nothing to do with the URL. You can also group together mutiple routing rules under a layout to share the same parent template.

layout("./auth/layout.tsx", [
  route("login", "./auth/login.tsx"),
  route("register", "./auth/register.tsx"),
])

Prefixes

[
  ...prefix("projects", [
    index("./projects/home.tsx"),
    route(":pid", "./projects/project.tsx"),
    route(":pid/edit", "./projects/edit-project.tsx"),
  ]),
]

It's just a syntax sugar for:

[
  route("projects", "./projects/home.tsx"),
  route("projects/:pid", "./projects/project.tsx"),
  route("projects/:pid/edit", "./projects/edit-project.tsx"),
]

I'm not satisfied with this ..., it should be removed, and let the router handle it.

Type-safe Parameters

In the above example, there is a :pid in the path:

route("projects/:pid", "./projects/project.tsx")

The paramters can be accessed in the component as following:

import type { LoaderArgs, ComponentProps } from "./+types.project"

export async function loader({ params }: LoaderArgs) {
  console.log(params.pid)
}

export default function Component({ params }: ComponentProps) {
  return <div>{params.pid}</div>
}

To make the import statement work, run npx react-router typegen, which generates type definitions in .react-router/types/app/routes/+types.project.d.ts.

Linking

Edit routes/home.tsx:

import type { MetaFunction } from "react-router"
import { Link } from "react-router"

export const meta: MetaFunction = () => {
  return [
    { title: "New React Router App" },
    { name: "description", content: "Welcome to React Router!" },
  ]
}

export default function Index() {
  return <Link to="/projects/99">Project 99</Link>
}

Visit http://localhost:5173 and click the link to navigate to /projects/99.

Server Actions

Server actions are functions defined using the name action.

The action function runs on the server and are removed from client bundles.

import type { Route } from "./+types/home"
import { Form } from "react-router"

export async function action({ request }: Route.ActionArgs) {
  const formData = await request.formData()
  const input = await formData.get("input") as string
  return {
    output: input?.toUpperCase()
  }
}

export default function Page({ actionData }: Route.ComponentProps) {
  return <Form method="post">
    <input type="text" name="input" />
    <button type="submit">Run on Serverside</button>
    {actionData && <p>{actionData.output}</p>}
  </Form>
}

If you need more flexibility, there is fetcher:

import type { Route } from "./+types/home"
import { useFetcher } from "react-router"
import { useState } from "react"

export async function action({ request }: Route.ActionArgs) {
  const formData = await request.formData()
  const input = await formData.get("input") as string
  return {
    output: input?.toUpperCase()
  }
}

export default function Page() {
  const fetcher = useFetcher()
  const [val, setVal] = useState('')

  const cb = () => {
    fetcher.submit({ input: val }, { action: "/about", method: "post" })
  }

  return <div>
    <input value={val} onChange={(e) => setVal(e.target.value)} />
    <button disabled={fetcher.state !== 'idle'} onClick={cb}>Run on Server</button>
    <div>{fetcher.data?.output}</div>
  </div>
}

Why Server Action

Server action is a language level feature, it is implemented via code transformation and framework integration.

From the developer's perspective, it reduces complexity and provides better developer experience.