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.tsxfile.
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.