Skip to main content

React Router

Installation

โปรเจคส่วนใหญ่เริ่มด้วย template เราลองมาใช้ template กับ React Router กันเถอะ

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

ต่อมา เราเข้าไปที่ Directory ของเรา แล้วเริ่มรัน Application ของเรากัน

cd my-react-router-app
npm i
npm run dev

Routing

Configuring Routes

เราจะสามารถ Config Route ได้ที่ app/routes.ts . route แต่ละตัว จะต้องมี 2 อย่าง: URL pattern ที่จับคู่กับ URL, file path ที่บอกว่าจะให้เราแสดงหน้าไหน

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

export default [
route("some/path", "./some/file.tsx"),
// pattern ^ ^ module file
] satisfies RouteConfig;

ตัวอย่างในการ Config ที่ใหญ่ขึ้น

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

export default [
index("./home.tsx"),
route("about", "./about.tsx"),

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

...prefix("concerts", [
index("./concerts/home.tsx"),
route(":city", "./concerts/city.tsx"),
route("trending", "./concerts/trending.tsx"),
]),
] satisfies RouteConfig;

ถ้าเราต้องการที่จะประกาศการใช้งาน route ผ่านการตั้งชื่อ file แทนที่จะมานั่ง Config เอง, @react-router/fs-routes สามารถช่วยเราได้  file system routing convention.

Route Modules

file ที่อ้างอิงใน routes.ts จะกำหนดพฤติกรรมของแต่ละ route:

route("teams/:teamId", "./team.tsx"),
// route module ^^^^^^^^

นี่เป็นตัวอย่างของ route module

// provides type safety/inference
import type { Route } from "./+types/team";

// provides `loaderData` to the component
export async function loader({ params }: Route.LoaderArgs) {
let team = await fetchTeam(params.teamId);
return { name: team.name };
}

// renders after the loader is done
export default function Component({
loaderData,
}: Route.ComponentProps) {
return <h1>{loaderData.name}</h1>;
}

Route modules มี features อื่น ๆ อีก เช่น actions, headers, และ error boundaries, แต่เราจะพูดถึงมัน ในบทต่อ ๆ ไป:  Route Modules

Nested Routes

Routes สามารถซ้อนกันลงไปได้

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

export default [
// parent route
route("dashboard", "./dashboard.tsx", [
// child routes
index("./home.tsx"),
route("settings", "./settings.tsx"),
]),
] satisfies RouteConfig;

path ของ parent จะถูกเพิ่มเข้าไปที่ child โดยอัตโนมัติ, ดังนั้น config จะสร้างทั้ง "/dashboard" และ "/dashboard/settings" URLs.

Child routes จะ render ผ่าน <Outlet/> ใน Parent Route

import { Outlet } from "react-router";

export default function Dashboard() {
return (
<div>
<h1>Dashboard</h1>
{/* will either be home.tsx or settings.tsx */}
<Outlet />
</div>
);
}

Root Route

route ทุกตัวใน routes.ts เป็น route ที่อยู่ใน app/root.tsx module.

Layout Routes

route layout จะสร้างการซ้อนกันใหม่สำหรับ children ของมัน, แต่มันจะไม่เพิ่มส่วนอื่น ๆ ลงใน URL มันคล้ายกับ root route แต่สามารถเพิ่มได้ทุกที่ของ route

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

export default [
layout("./marketing/layout.tsx", [
index("./marketing/home.tsx"),
route("contact", "./marketing/contact.tsx"),
]),
...prefix("projects", [
index("./projects/home.tsx"),
layout("./projects/project-layout.tsx", [
route(":pid", "./projects/project.tsx"),
route(":pid/edit", "./projects/edit-project.tsx"),
]),
]),
] satisfies RouteConfig;

Index Routes

index(componentFile),

Index routes render เข้าไปที่ parent's Outlet ที่ parent's URL ของมัน

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

export default [
// renders into the root.tsx Outlet at /
index("./home.tsx"),
route("dashboard", "./dashboard.tsx", [
// renders into the dashboard.tsx Outlet at /dashboard
index("./dashboard-home.tsx"),
route("settings", "./dashboard-settings.tsx"),
]),
] satisfies RouteConfig;

index routes ไม่สามารถมี Children ได้

Route Prefixes

ใช้ prefix , เราสามารถเพิ่ม path prefix เพื่อตั้งคำนำหน้าให้ route ได้โดยไม่จำเป็นต้องสร้าง file parent route เพิ่มเติม

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

export default [
layout("./marketing/layout.tsx", [
index("./marketing/home.tsx"),
route("contact", "./marketing/contact.tsx"),
]),
...prefix("projects", [
index("./projects/home.tsx"),
layout("./projects/project-layout.tsx", [
route(":pid", "./projects/project.tsx"),
route(":pid/edit", "./projects/edit-project.tsx"),
]),
]),
] satisfies RouteConfig;

Dynamic Segments

ถ้า path เริ่มต้นด้วย : ดังนั้นมันจะมาเป็น "dynamic segment". เมื่อ route จับคู่กับ URL, dynamic segment จะถูกส่งมาจาก URL และ จะถูกเข้าถึงได้โดยใช้ params

route("teams/:teamId", "./team.tsx"),
import type { Route } from "./+types/team";

export async function loader({ params }: Route.LoaderArgs) {
// ^? { teamId: string }
}

export default function Component({
params,
}: Route.ComponentProps) {
params.teamId;
// ^ string
}

เราสามารถมี dynamic segments หลาย ๆ ตัวได้

route("c/:categoryId/p/:productId", "./product.tsx"),
import type { Route } from "./+types/product";

async function loader({ params }: LoaderArgs) {
// ^? { categoryId: string; productId: string }
}

Optional Segments

เราสามารถสร้าง route segment ให้มันเป็น optional ได้ โดยเพิ่ม ? ไปที่ท้ายของ segment ได้

route(":lang?/categories", "./categories.tsx"),

เราสามารถมี optional static segments ได้ด้วย

route("users/:userId/edit?", "./user.tsx");

Splats

เป็นที่รู้จักกันดีในชื่อ "catchall" และ "star" segments. ถ้า route path pattern จบด้วย /* ดังนั้นมันจะจับตัวอักษรทุกตัวที่ตามหลัง /, รวมถึง / ของตัวอื่นด้วย.

route("files/*", "./files.tsx"),
export async function loader({ params }: Route.LoaderArgs) {
// params["*"] will contain the remaining URL after files/
}

เราสามารถ destructure ตัว *, เราสามารถตั้งชื่อให้มันใหม่ได้. แต่ชื่อโดยปกติขของมันคือ splat

Component Routes

คุณสามารถใช้ Component ที่จับคู่ URL กับ elements ในตำแหน่งใดก็ได้ภายใน component tree:

import { Routes, Route } from "react-router";

function Wizard() {
return (
<div>
<h1>Some Wizard with Steps</h1>
<Routes>
<Route index element={<StepOne />} />
<Route path="step-2" element={<StepTwo />} />
<Route path="step-3" element={<StepThree />}>
</Routes>
</div>
);
}


Route Module

file ที่ถูกอ้างใน routes.ts ถูกเรียกโดย Route Modules

route("teams/:teamId", "./team.tsx"),
// route module ^^^^^^^^

Route modules เป็นพื้นฐานของ React Router's framework features พวกมันสามารถใช้

  • automatic code-splitting
  • data loading
  • actions
  • revalidation
  • error boundaries
  • และอื่น ๆ

Component (default)

สร้าง Component ที่จะถูกแสดงเมื่อ route ตรงกัน

export default function MyRouteComponent() {
return (
<div>
<h1>Look ma!</h1>
<p>
I'm still using React Router after like 10 years.
</p>
</div>
);
}

loader

Route loaders จะให้ข้อมูลกับ route component ก่อนที่มันจะถูกโหลด. มันจะถูกเรียกใช้ที่ฝั่ง Server เมื่อ server กำลัง render หรือเมื่อขณะที่สร้าง pre-rendering.

export async function loader() {
return { message: "Hello, world!" };
}

export default function MyRoute({ loaderData }) {
return <h1>{loaderData.message}</h1>;
}

ดูเพิ่มเติม:

clientLoader

เรียกใช้แค่ภายใน browser, route client loaders จะให้ข้อมูลกับ route components

export async function clientLoader({ serverLoader }) {
// call the server loader
const serverData = await serverLoader();
// And/or fetch data on the client
const data = getDataFromClient();
// Return the data to expose through useLoaderData()
return data;
}

ตัว client loaders สามารถมีส่วนร่วมในการโหลดข้อมูลเริ่มต้น (initial page load hydration) ของหน้าเว็บที่ render จากเซิร์ฟเวอร์ได้โดยการตั้งค่า hydrate ใน function:

export async function clientLoader() {
// ...
}
clientLoader.hydrate = true as const;

action

Route actions อนุญาต ข้อมูล server-side สามารถดัดแปลงได้ พร้อมกับ สามารถตรวจสอบข้อมูลและโหลดข้อมูลมาใหม่ได้ (revalidation) โดยอัตโนมัติ สำหรับ loader data ที่มีทั้งหมดในหน้า Page เมื่อมีการถูกเรียกใช้งานผ่าน <Form>, useFetcher, และ useSubmit

// route("/list", "./list.tsx")
import { Form } from "react-router";
import { TodoList } from "~/components/TodoList";

// this data will be loaded after the action completes...
export async function loader() {
const items = await fakeDb.getItems();
return { items };
}

// ...so that the list here is updated automatically
export default function Items({ loaderData }) {
return (
<div>
<List items={loaderData.items} />
<Form method="post" navigate={false} action="/list">
<input type="text" name="title" />
<button type="submit">Create Todo</button>
</Form>
</div>
);
}

export async function action({ request }) {
const data = await request.formData();
const todo = await fakeDb.addItem({
title: data.get("title"),
});
return { ok: true };
}

clientAction

เหมือนกันกับ route actions แต่จะเรียกใช้ใน Browser อย่างเดียว

export async function clientAction({ serverAction }) {
fakeInvalidateClientSideCache();
// can still call the server action if needed
const data = await serverAction();
return data;
}

ErrorBoundary

เมื่อ API ของ route module อื่น ๆ โยน Error มา, route module ErrorBoundary จะ render แทนที่ component ปัจจุบัน

import {
isRouteErrorResponse,
useRouteError,
} from "react-router";

export function ErrorBoundary() {
const error = useRouteError();

if (isRouteErrorResponse(error)) {
return (
<div>
<h1>
{error.status} {error.statusText}
</h1>
<p>{error.data}</p>
</div>
);
} else if (error instanceof Error) {
return (
<div>
<h1>Error</h1>
<p>{error.message}</p>
<p>The stack trace is:</p>
<pre>{error.stack}</pre>
</div>
);
} else {
return <h1>Unknown Error</h1>;
}
}

HydrateFallback

ในหน้า load, route component renders จะ render หลังจาก client โหลดเสร็จแล้ว, ถ้า exported HydrateFallback สามารถ render ทันที แทนที่ route component.

export async function clientLoader() {
const data = await fakeLoadLocalGameData();
return data;
}

export function HydrateFallback() {
return <p>Loading Game...</p>;
}

export default function Component({ loaderData }) {
return <Game data={loaderData} />;
}

headers

Route headers จะประกาศ HTTP headers เพื่อจะส่ง response เมื่อ server กำลัง render อยู่.

export function headers() {
return {
"X-Stretchy-Pants": "its for fun",
"Cache-Control": "max-age=300, s-maxage=3600",
};
}

handle

Route handle อนุญาตให้ Application เพิ่มอะไรก็ได้ เข้าไปที่ route โดยใช้ useMatches ในการสร้าง abstractions เช่น แสดงเส้นทางนำทาง (breadcrumbs) หรือข้อมูลอื่น ๆ

export const handle = {
its: "all yours",
};

Route links ประกาศ <link> element เพื่อจะ render ใน <head>.

export function links() {
return [
{
rel: "icon",
href: "/favicon.png",
type: "image/png",
},
{
rel: "stylesheet",
href: "https://example.com/some/styles.css",
},
{
rel: "preload",
href: "/images/banner.jpg",
as: "image",
},
];
}

routes links ทั้งหมดจะถูกรวมและ rendered ผ่าน <Links /> component

import { Links } from "react-router";

export default function Root() {
return (
<html>
<head>
<Links />
</head>

<body />
</html>
);
}

meta

Route meta ประกาศ meta tag เพื่อจะ render ใน <head>

export function meta() {
return [
{ title: "Very cool app" },
{
property: "og:title",
content: "Very cool app",
},
{
name: "description",
content: "This app is the best",
},
];
}

routes' meta ทั้งหมดจะถูกรวมและ render ใน <Meta /> Component

import { Meta } from "react-router";

export default function Root() {
return (
<html>
<head>
<Meta />
</head>

<body />
</html>
);
}

ดูเพิ่มเติมใน

shouldRevalidate

โดยปกติแล้ว route ทั้งหมด จะได้รับการตรวจสอบข้อมูลใหม่, หลังจากการทำงานของ action, function นี้ช่วยให้ route สามารถยกเลิกการตรวจสอบข้อมูลใหม่ได้หากการทำงานของ action นั้นไม่ส่งผลกระทบต่อข้อมูลของ route นั้น

import type { Route } from "./+types/my-route";

export function shouldRevalidate(
arg: Route.ShouldRevalidateArg
) {
return true;
}

Rendering Strategies

มันมี 3 Strategies ใน ****React Router:

  • Client Side Rendering
  • Server Side Rendering
  • Static Pre-rendering

Client Side Rendering

Routes จะ render ที่ฝั่ง Client เสมอ. ถ้าคุณสร้าง Single Page App, คุณต้องปิด server rendering

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

export default {
ssr: false,
} satisfies Config;

Server Side Rendering

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

export default {
ssr: true,
} satisfies Config;

Server side rendering ต้องการ การ deployment ที่รองรับฟีเจอร์นี้, แม้ว่าจะเป็นการตั้งค่าทั่วไป แต่ route แต่ละ route ยังสามารถ render แบบ static ได้, นอกจากนี้ route ยังสามารถใช้การโหลดข้อมูลจากฝั่ง Client ด้วย clientLoader เพื่อหลีกเลี่ยงการ render หรือการดึงข้อมูลจากเซิร์ฟเวอร์สำหรับส่วนของ UI ของตน

Static Pre-rendering

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

export default {
// return a list of URLs to prerender at build time
async prerender() {
return ["/", "/about", "/contact"];
},
} satisfies Config;

Pre-rendering จะเกิดขณะที่กำลังจะสร้าง Application ซึ่งจะสร้าง HTML แบบ Static และ การนำเข้าข้อมูลของ Client. สำหรับรายการของ URLs การ Pre-render นี้มีประโยชน์สำหรับ SEO และประสิทธิภาพการทำงาน โดยเฉพาะสำหรับการปรับใช้ที่ไม่รองรับการ server rendering เมื่อทำการ pre-render, ตัว route module loaders จะถูกใช้เพื่อดึงข้อมูลในขณะสร้าง Application


Data Loading

ข้อมูลจะถูกส่งให้ Component ผ่าน loader และ clientLoader.

ข้อมูลจาก loader จะถูกแปลงเป็น String โดยอัตโนมัติจากตัว loaders และจะถูกแปลงกลับใน Component นอกจากค่าพื้นฐานเช่น String และ Number แล้ว, Loader ยังสามารถ return ค่าเป็น promises, maps, sets, dates และอื่น ๆ ได้

Client Data Loading

clientLoader ถูกใช้เพื่อดึงข้อมูลจากฝั่ง client ซึ่งมีประโยชน์สำหรับเว็บหรือ Project ทั้งหมดที่คุณต้องการให้ดึงข้อมูลจาก Browser เท่านั้น

// route("products/:pid", "./product.tsx");
import type { Route } from "./+types/product";

export async function clientLoader({
params,
}: Route.ClientLoaderArgs) {
const res = await fetch(`/api/products/${params.pid}`);
const product = await res.json();
return product;
}

export default function Product({
loaderData,
}: Route.ComponentProps) {
const { name, description } = loaderData;
return (
<div>
<h1>{name}</h1>
<p>{description}</p>
</div>
);
}

Server Data Loading

เมื่อ Server กำลัง render, loader จะถูกใช้ทั้งการโหลดหน้าเว็บ และ client navigations, Client navigations จะเรียก loader ผ่าน automatic fetch โดย React Router จาก browser ไป server ของคุณ

// route("products/:pid", "./product.tsx");
import type { Route } from "./+types/product";
import { fakeDb } from "../db";

export async function loader({ params }: Route.LoaderArgs) {
const product = await fakeDb.getProduct(params.pid);
return product;
}

export default function Product({
loaderData,
}: Route.ComponentProps) {
const { name, description } = loaderData;
return (
<div>
<h1>{name}</h1>
<p>{description}</p>
</div>
);
}

loader function ถูกลบออกจาก client bundles ดังนั้นเราจะสามารถใช้มันได้จากแค่ทาง Server เท่านั้น

Static Data Loading

เมื่อเกิด pre-rendering, ตัว loader จะใช้เพื่อ fetch ข้อมูลระหว่าง กำลังสร้าง production

// route("products/:pid", "./product.tsx");
import type { Route } from "./+types/product";

export async function loader({ params }: Route.LoaderArgs) {
let product = await getProductFromCSVFile(params.pid);
return product;
}

export default function Product({
loaderData,
}: Route.ComponentProps) {
const { name, description } = loaderData;
return (
<div>
<h1>{name}</h1>
<p>{description}</p>
</div>
);
}

URLs ที่จะทำการ Pre-render จะถูกกำหนดในไฟล์ react-router.config.ts

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

export default {
async prerender() {
let products = await readProductsFromCSVFile();
return products.map(
(product) => `/products/${product.id}`
);
},
} satisfies Config;

จำไว้ว่า เมื่อ Server กำลัง Render, URLs ที่ไม่ได้เป็น Pre-render ตัว Server จะ render มันตามปกติ

Using Both Loaders

loader และ clientLoader สามารถใช้ร่วมกันได้. loader จะใช้ในการโหลดข้อมูลจากฝั่ง Server และ clientLoader จะใช้ในการ client navigation

// route("products/:pid", "./product.tsx");
import type { Route } from "./+types/product";
import { fakeDb } from "../db";

export async function loader({ params }: Route.LoaderArgs) {
return fakeDb.getProduct(params.pid);
}

export async function clientLoader({
params,
}: Route.ClientLoader) {
const res = await fetch(`/api/products/${params.pid}`);
return res.json();
}

export default function Product({
loaderData,
}: Route.ComponentProps) {
const { name, description } = loaderData;

return (
<div>
<h1>{name}</h1>
<p>{description}</p>
</div>
);
}


Actions

การเปลี่ยนแปลงข้อมูล จะทำผ่าน Route actions เมื่อการทำงานของ action เสร็จสิ้น ข้อมูลทั้งหมดจากตัว loader บนหน้าเว็บจะได้รับการตรวจสอบใหม่ เพื่อให้ UI ของคุณตรงกับข้อมูลโดยไม่ต้องเขียนโค้ดเพื่อทำสิ่งนี้

Route actions ที่กำหนดด้วย action จะถูกเรียกใช้เฉพาะบนฝั่ง Server เท่านั้น ในขณะที่ actions ที่กำหนดด้วย clientAction จะถูกเรียกใช้ใน browser

Client Actions

Client actions จะถูกรันใน Browser เราควรเรียงลำดับในการทำงานดี ๆ เมื่อเราเรียกใช้ทั้งคู่

// route('/projects/:projectId', './project.tsx')
import type { Route } from "./+types/project";
import { Form } from "react-router";
import { someApi } from "./api";

export async function clientAction({
request,
}: Route.ClientActionArgs) {
let formData = await request.formData();
let title = await formData.get("title");
let project = await someApi.updateProject({ title });
return project;
}

export default function Project({
actionData,
}: Route.ComponentProps) {
return (
<div>
<h1>Project</h1>
<Form method="post">
<input type="text" name="title" />
<button type="submit">Submit</button>
</Form>
{actionData ? (
<p>{actionData.title} updated</p>
) : null}
</div>
);
}

Server Actions

Server actions จะรันแค่บน Server เท่านั้น และจะถูกลบออกจาก Client bundles

// route('/projects/:projectId', './project.tsx')
import type { Route } from "./+types/project";
import { Form } from "react-router";
import { fakeDb } from "../db";

export async function action({
request,
}: Route.ActionArgs) {
let formData = await request.formData();
let title = await formData.get("title");
let project = await fakeDb.updateProject({ title });
return project;
}

export default function Project({
actionData,
}: Route.ComponentProps) {
return (
<div>
<h1>Project</h1>
<Form method="post">
<input type="text" name="title" />
<button type="submit">Submit</button>
</Form>
{actionData ? (
<p>{actionData.title} updated</p>
) : null}
</div>
);
}

Calling Actions

Actions จะถูกเรียกใช้ผ่าน <Form> และ ใช้เป็นคำสั่งผ่าน useSubmit (หรือ <fetcher.Form> และ fetcher.submit) โดยการ อ้างอิงไปที่ route's path และใช้ method "post"

Calling actions with a Form

import { Form } from "react-router";

function SomeComponent() {
return (
<Form action="/projects/123" method="post">
<input type="text" name="title" />
<button type="submit">Submit</button>
</Form>
);
}

นี่จะทำให้เกิดการ navigation และถูกบันทึกเข้าไปที่ browser history.

Calling actions with useSubmit

เราสามารถส่งข้อมูลจาก form โดยใชเคำสั่ง  useSubmit ได้

import { useCallback } from "react";
import { useSubmit } from "react-router";
import { useFakeTimer } from "fake-lib";

function useQuizTimer() {
let submit = useSubmit();

let cb = useCallback(() => {
submit(
{ quizTimedOut: true },
{ action: "/end-quiz", method: "post" }
);
}, []);

let tenMinutes = 10 * 60 * 1000;
useFakeTimer(tenMinutes, cb);
}

นี่จะทำให้เกิดการ navigation และถูกบันทึกเข้าไปที่ browser history.

Calling actions with a fetcher

Fetchers อนุญาตให้คุณ submit ข้อมูลไปที่ action (หรือ loader) โดยที่ไม่ทำให้เกิดการ navigation ได้

import { useFetcher } from "react-router";

function Task() {
let fetcher = useFetcher();
let busy = fetcher.state !== "idle";

return (
<fetcher.Form method="post" action="/update-task/123">
<input type="text" name="title" />
<button type="submit">
{busy ? "Saving..." : "Save"}
</button>
</fetcher.Form>
);
}

พวกมันก็มี submit method ให้ใช้งานด้วย

fetcher.submit(
{ title: "New Title" },
{ action: "/update-task/123", method: "post" }
);

เราสามารถดู Using Fetchers เพิ่มเติมได้


Navigating

Users จะ navigate application ของเราด้วย <Link><NavLink><Form>redirect, และ useNavigate.

component นี้ ใช้สำหรับการ navigate link ที่เราต้องการจะ render ในหน้าใหม่

import { NavLink } from "react-router";

export function MyAppNav() {
return (
<nav>
<NavLink to="/" end>
Home
</NavLink>
<NavLink to="/trending" end>
Trending Concerts
</NavLink>
<NavLink to="/concerts">All Concerts</NavLink>
<NavLink to="/account">Account</NavLink>
</nav>
);
}

NavLink renders จะมี Class name ที่เป็นสถานะของมัน ให้เราใช้ ในขณะที่กำลังจะ navigate สำหรับถ้าเราต้องการที่จะตกแต่ง element ด้วย CSS

a.active {
color: red;
}

a.pending {
animate: pulse 1s infinite;
}

a.transitioning {
/* css transition is running */
}

มันมี callBack prop ที่มาพร้อมกับสถานะ บน classNamestyle, และ children เพื่อการ inline styling หรือ conditional rendering:

// className
<NavLink
to="/messages"
className={({ isActive, isPending, isTransitioning }) =>
[
isPending ? "pending" : "",
isActive ? "active" : "",
isTransitioning ? "transitioning" : "",
].join(" ")
}
>
Messages
</NavLink>

// style
<NavLink
to="/messages"
style={({ isActive, isPending, isTransitioning }) => {
return {
fontWeight: isActive ? "bold" : "",
color: isPending ? "red" : "black",
viewTransitionName: isTransitioning ? "slide" : "",
};
}}
>
Messages
</NavLink>

// children
<NavLink to="/tasks">
{({ isActive, isPending, isTransitioning }) => (
<span className={isActive ? "active" : ""}>Tasks</span>
)}
</NavLink>

<Link> ใช้เมื่อเราไม่ต้องการที่จะใช้ state พวกนั้น

import { Link } from "react-router";

export function LoggedOutMessage() {
return (
<p>
You've been logged out.{" "}
<Link to="/login">Login again</Link>
</p>
);
}

Form

form component สามารถใช้ navigate กับ URLSearchParams ที่ใช้โดย User ได้

<Form action="/search">
<input type="text" name="q" />
</Form>

ถ้า User ใส่ "journey" เข้าไปที่ Input และ submits มัน มันจะ navigate ไปที่

/search?q=journey

redirect

ภายใน route loaders และ actions เราสามารถ redirect ไปที่ URL ที่อื่นได้

import { redirect } from "react-router";

export async function loader({ request }) {
let user = await getUser(request);
if (!user) {
return redirect("/login");
}
return { userName: user.name };
}

เป็นเรื่องปกติที่จะทำการ redirect ไปยัง record ใหม่หลังจากที่มันถูกสร้างขึ้น

import { redirect } from "react-router";

export async function action({ request }) {
let formData = await request.formData();
let project = await createProject(formData);
return redirect(`/projects/${project.id}`);
}

useNavigate

hook นี้ จะทำให้ programmer ในการ navigate User ไปที่หน้าใหม่ได้ โดยที่ User ไม่ได้ทำอะไรเลย การใช้งานของ Hook นี้ ไม่ค่อยได้ใช้งานมากนัก. เราแนะนำให้ใช้ API ตัวอื่นดีกว่า

การใช้ useNavigate คือเมื่อ User ไม่ได้ interact แต่คุณต้องการให้เกิดการ navigate

ตัวอย่างเช่น

  • ออกจากระบบ เมื่อ User ไม่ได้ใช้งาน
  • การจับเวลา UIs เช่น quizzes, และ อื่น ๆ
import { useNavigate } from "react-router";

export function useLogoutAfterInactivity() {
let navigate = useNavigate();

useFakeInactivityHook(() => {
navigate("/logout");
});
}

Pending UI

เมื่อ user navigates ไปที่ route อันใหม่ หรือว่า submit ข้อมูล ไปที่ action, UI ควรที่จะ respond กลับไปที่ user ทันที. Application code จะต้องรับผิดชอบในเรื่องนี้

Global Pending Navigation

เมื่อ User navigates ไปที่ URL ใหม่, loader สำหรับหน้าต่อไป จะรอจนกว่าหน้าต่อไปจะ render เสร็จ. เราสามารถรับสถานะได้จาก useNavigation

import { useNavigation } from "react-router";

export default function Root() {
const navigation = useNavigation();
const isNavigating = Boolean(navigation.location);

return (
<html>
<body>
{isNavigating && <GlobalSpinner />}
<Outlet />
</body>
</html>
);
}

Local Pending Navigation

Pending indicators สามารถกำหนดให้เฉพาะเจาะจงกับ link ได้เช่นกัน children, className, และ style ของ NavLink สามารถเป็น function ที่รับ pending state เป็น Argument ได้

import { NavLink } from "react-router";

function Navbar() {
return (
<nav>
<NavLink to="/home">
{({ isPending }) => (
<span>Home {isPending && <Spinner />}</span>
)}
</NavLink>
<NavLink
to="/about"
style={({ isPending }) => ({
color: isPending ? "gray" : "black",
})}
>
About
</NavLink>
</nav>
);
}

Pending Form Submission

เมื่อ form ถูก submit แล้ว UI ควรจะแสดงหน้าจอ respond กลับไปที่ User ทันที ด้วย pending state. ทางที่ง่ายที่สุดคือการใช้ fetcher form เพราะว่ามันมี state ของมันเอง

import { useFetcher } from "react-router";

function NewProjectForm() {
const fetcher = useFetcher();

return (
<fetcher.Form method="post">
<input type="text" name="title" />
<button type="submit">
{fetcher.state !== "idle"
? "Submitting..."
: "Submit"}
</button>
</fetcher.Form>
);
}

สำหรับ การ submit ที่เป็น non-fetcher form, เราสามารถใช้ pending states ได้จาก useNavigation

import { useNavigation, Form } from "react-router";

function NewProjectForm() {
const navigation = useNavigation();

return (
<Form method="post" action="/projects/new">
<input type="text" name="title" />
<button type="submit">
{navigation.formAction === "/projects/new"
? "Submitting..."
: "Submit"}
</button>
</Form>
);
}

Optimistic UI

เมื่อสถานะของ UI สามารถดึงค่าได้จาก form submission, optimistic UI ก็สามารถทำได้ เพื่อ UX ที่ดีขึ้น

function Task({ task }) {
const fetcher = useFetcher();

let isComplete = task.status === "complete";
if (fetcher.formData) {
isComplete = fetcher.formData.get("status");
}

return (
<div>
<div>{task.title}</div>
<fetcher.Form method="post">
<button
name="status"
value={isComplete ? "incomplete" : "complete"}
>
{isComplete ? "Mark Incomplete" : "Mark Complete"}
</button>
</fetcher.Form>
</div>
);
}

Testing

เมื่อ Component ใช้ useLoaderData<Link>, และอื่น ๆ เราต้อง render มันภายใน React Router app. createRoutesStub function ถูกสร้างมาเพื่อกรณีนั้น ในการทดสอบ Component นั้น ๆ

พิจารณาการ Login form component ที่ใช้ useActionData

import { useActionData } from "react-router";

export function LoginForm() {
const errors = useActionData();
return (
<Form method="post">
<label>
<input type="text" name="username" />
{errors?.username && <div>{errors.username}</div>}
</label>

<label>
<input type="password" name="password" />
{errors?.password && <div>{errors.password}</div>}
</label>

<button type="submit">Login</button>
</Form>
);
}

เราสามารถทดสอบ Component ด้วย createRoutesStub ได้. ซึ่งรับ Array ของ Object ที่คล้ายกับ route modules โดยมี loaders, actions, และ components

import { createRoutesStub } from "react-router";
import * as Test from "@testing-library/react";
import { LoginForm } from "./LoginForm";

test("LoginForm renders error messages", async () => {
const USER_MESSAGE = "Username is required";
const PASSWORD_MESSAGE = "Password is required";

const Stub = createRoutesStub([
{
path: "/login",
Component: LoginForm,
action() {
return {
errors: {
username: USER_MESSAGE,
password: PASSWORD_MESSAGE,
},
};
},
}),
]);

// render the app stub at "/login"
Test.render(<Stub initialEntries={["/login"]} />);

// simulate interactions
Test.user.click(screen.getByText("Login"));
await Test.waitFor(() => screen.findByText(USER_MESSAGE));
await Test.waitFor(() =>
screen.findByText(PASSWORD_MESSAGE)
);
});


Custom Framework

แทนที่จะใช้ @react-router/dev, เราสามารถรวม React Router's framework features (เช่น loaders, actions, fetchers, และอื่น ๆ) เข้าไปที่ bundler ของเราและ server abstractions ได้

Client Rendering

1. Create a Router

browser runtime API ที่สามารถใช้งานกับ route module APIs ได้ (loaders, actions, etc.) คือ createBrowserRouter.

มันจะนำ array ของ route Object ที่สนับสนุน loaders, actions, error boundaries และ อื่น ๆ. React Router Vite plugin สร้างหนึ่งในนี้จาก routes.ts แต่เราสามารถสร้างโดยตัวเราเองได้ และ ใช้ bundler ของเราเองได้

import { createBrowserRouter } from "react-router";

let router = createBrowserRouter([
{
path: "/",
Component: Root,
children: [
{
path: "shows/:showId",
Component: Show,
loader: ({ request, params }) =>
fetch(`/api/show/${params.id}.json`, {
signal: request.signal,
}),
},
],
},
]);

2. Render the Router

ในการ render router ใน Browser, เราจะใช้ <RouterProvider>.

import {
createBrowserRouter,
RouterProvider,
} from "react-router";
import { createRoot } from "react-dom/client";

createRoot(document.getElementById("root")).render(
<RouterProvider router={router} />
);

3. Lazy Loading

Route สามารถกำหนด lazily ด้วย lazy property ได้

createBrowserRouter([
{
path: "/show/:showId",
lazy: () => {
let [loader, action, Component] = await Promise.all([
import("./show.action.js"),
import("./show.loader.js"),
import("./show.component.js"),
]);
return { loader, action, Component };
},
},
]);

Server Rendering

ในการที่ให้ Server render custom setup ขึ้นมา, เราสามารถใช้งาน server APIs สำหรับ rendering ข้อมูลได้

guide นี้ จะแสดงแค่การใช้งาน ถ้าต้องการจะเรียนรู้ลึกขึ้น เราสามารถดู https://github.com/remix-run/custom-react-router-framework-example ได้

1. Define Your Routes

Routes เป็น Object ประเภทเดียวกัน ทั้ง Server และ Client

export default [
{
path: "/",
Component: Root,
children: [
{
path: "shows/:showId",
Component: Show,
loader: ({ params }) => {
return db.loadShow(params.id);
},
},
],
},
];

2. Create a static handler

เปลี่ยน routes ไปเป็น request handler ด้วย createStaticHandler

import { createStaticHandler } from "react-router";
import routes from "./some-routes";

let { query, dataRoutes } = createStaticHandler(routes);

3. Get Routing Context and Render

React Router ทำงานกับ web fetch Requests, ดังนั้นถ้า Server ของคุณไม่ยอมรับ คุณจะต้องเปลี่ยน Object ที่ใช้อยู่ให้เป็น Object web fetch Request

import { renderToString } from "react-dom/server";
import {
createStaticHandler,
createStaticRouter,
StaticRouterProvider,
} from "react-router";

import routes from "./some-routes.js";

let { query, dataRoutes } = createStaticHandler(routes);

export async function handler(request: Request) {
// 1. run actions/loaders to get the routing context with `query`
let context = await query(request);

// If `query` returns a Response, send it raw (a route probably a redirected)
if (context instanceof Response) {
return context;
}

// 2. Create a static router for SSR
let router = createStaticRouter(dataRoutes, context);

// 3. Render everything with StaticRouterProvider
let html = renderToString(
<StaticRouterProvider
router={router}
context={context}
/>
);

// Setup headers from action and loaders from deepest match
let leaf = context.matches[context.matches.length - 1];
let actionHeaders = context.actionHeaders[leaf.route.id];
let loaderHeaders = context.loaderHeaders[leaf.route.id];
let headers = new Headers(actionHeaders);
if (loaderHeaders) {
for (let [key, value] of loaderHeaders.entries()) {
headers.append(key, value);
}
}

headers.set("Content-Type", "text/html; charset=utf-8");

// 4. send a response
return new Response(`<!DOCTYPE html>${html}`, {
status: context.statusCode,
headers,
});
}

4. Hydrate in the browser

ข้อมูล Hydration จะอยู่ใน window.__staticRouterHydrationData, ใช้ข้อมูลพวกนี้เพื่อเริ่มการใช้งาน Router ฝั่ง Server และ render <RouterProvider>.

import { StrictMode } from "react";
import { hydrateRoot } from "react-dom/client";
import { RouterProvider } from "react-router/dom";
import routes from "./app/routes.js";
import { createBrowserRouter } from "react-router";

let router = createBrowserRouter(routes, {
hydrationData: window.__staticRouterHydrationData,
});

hydrateRoot(
document,
<StrictMode>
<RouterProvider router={router} />
</StrictMode>
);