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",
};
links
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.
NavLink
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 ที่มาพร้อมกับสถานะ บน className, style, และ 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
<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>
);