React Router V7 学习笔记
开始一个新的RR7项目
前情提要: react router version:V 7.3.0
npx create-react-router@latest
项目结构
├── Dockerfile //docker 部署文件├── README.md├── app│ ├── app.css // 样式文件│ ├── root.tsx // 入口文件│ ├── routes // 路由配置文件夹│ │ └── home.tsx //│ ├── routes.ts│ └── welcome│ ├── logo-dark.svg│ ├── logo-light.svg│ └── welcome.tsx├── package-lock.json├── package.json├── public│ └── favicon.ico├── react-router.config.ts├── tsconfig.json└── vite.config.ts
路由配置文件:routes.ts
import { type RouteConfig, index } from "@react-router/dev/routes";
export default [ index("routes/home.tsx"), // 设置localhost:3000(主页)为此文件展示内容] satisfies RouteConfig;
路由配置: index & layout
index: 指定首页渲染什么UI
layout: 将其子路由页面渲染为固定在框架内 (注意 <Outlet /> )
import { type RouteConfig, index, layout, route,} from "@react-router/dev/routes";
export default [ // index("routes/home.tsx"),// 指定首页打开渲染哪个文件,不被包裹在任何父级布局框架里 layout("routes/dashboard.tsx", [ // 指定布局文件,下面被包裹的都会被固定在这个页面的框架下面 index("routes/home.tsx"), // 指定首页打开渲染哪个文件,且在布局框架里渲染首页 route("dashboard", "routes/home/home.tsx"), // 指定布局文件下的子路由 route("setting", "routes/setting/setting.tsx"), // 指定布局文件下的子路由 ]),] satisfies RouteConfig;
import React from "react";import { Outlet, Link } from "react-router";
function dashboard() { return ( <div className="flex min-h-screen h-full"> <div id="sidebar" className="w-64 bg-gray-100/20 h-full min-h-screen"> <div className="h-full flex flex-col gap-4 p-4"> <Link to="/dashboard">Home</Link> <Link to="/setting">Setting</Link> </div> </div> <div id="detail" className="flex-1 "> <Outlet /> // 注意这个类似插槽的Outlet </div> </div> );}
export default dashboard;
React Router
渲染UI效果
客户端路由(Client side routing)
Client side routing allows our app to update the URL without reloading the entire page.
(客户端路由允许我们的应用程序更新 URL,而无需重新加载整个页面)
数据加载
import React from "react";import type { Route } from "./+types/blog";
// 客户端加载数据
// export async function clientLoader({ params }: Route.ClientLoaderArgs) {// const res = await fetch(`https://jsonplaceholder.typicode.com/posts/1`);// const post = await res.json();// return post;// }
// 服务端加载数据export async function loader({ params, request }: Route.LoaderArgs) { const res = await fetch(`https://jsonplaceholder.typicode.com/posts/1`); const post = await res.json(); return post;}
function blog({ loaderData }: Route.ComponentProps) { console.log("loaderData==>", loaderData); const { title, body, id } = loaderData; return ( <div> <h1>{title}</h1> <p>{body}</p> <p>{id}</p> </div> );}
export default blog;
blog list page
import React from "react";import { Link } from "react-router";import type { Route } from "./+types/blogs";
export async function loader({ request }: Route.LoaderArgs) { const res = await fetch(`https://jsonplaceholder.typicode.com/posts`); const posts = await res.json(); return { posts };}
function blogs({ loaderData }: Route.ComponentProps) { const { posts } = loaderData; console.log("posts==>", posts);
return ( <div> <h1>Blogs</h1> <ul> {posts.map((post: any) => ( <li key={post.id}> <Link to={`/blog/${post.id}`} className="text-blue-300 hover:text-blue-500" > {post.title} </Link> </li> ))} </ul> </div> );}
export default blogs;
在同一组件中既使用server loader 又使用client loader
需要通过
clientLoader.hydrate = true as const;
指定强制在hydration期间运行clientLoader
import React from "react";import type { Route } from "./+types/blog";
export async function loader({ params, request }: Route.LoaderArgs) { const res = await fetch(`https://jsonplaceholder.typicode.com/posts/1`); const post = await res.json(); return post;}
// clientLoader 会等待 serverLoader 完成后再执行export async function clientLoader({ serverLoader, params,}: Route.ClientLoaderArgs) { const serverData = await serverLoader();
const res = await fetch(`https://jsonplaceholder.typicode.com/comments`); const comments = await res.json(); return { ...serverData, comments };}
function blog({ loaderData }: Route.ComponentProps) { console.log("loaderData==>", loaderData); const { title, body, id, comments } = loaderData; return ( <div> <h1 className="text-2xl font-bold">{title}</h1> <p>{body}</p> <p>{id}</p> <div> <span className="text-blue-500">Comments:</span> {comments.map((comment: any) => ( <p key={comment.id}>{comment.body}</p> ))} </div> </div> );}
// export function HydrateFallback() {// return <div>Loading...</div>;// }
// force the client loader to run during hydration// 强制在hydration期间运行clientLoaderclientLoader.hydrate = true as const; // `as const` for type inference
export function HydrateFallback() { return <div>Loading...</div>;}
export default blog;
Actions
Client Actions
客户端action仅在浏览器中运行,并且在定义两者时优先于服务器action。
Pending UI
全局加载UI pending 状态
// root.tsx 全局加载UIexport default function App() { const navigation = useNavigation(); const isNavigating = Boolean(navigation.location);
return ( <> // root.tsx 全局加载UI {isNavigating && <div className="fixed inset-0 bg-white/50" />} <Outlet /> </> );}
Link pendig UI
<NavLink to="/dashboard" className={({ isActive, isPending, isTransitioning }) => [ isPending ? "pending" : "", isActive ? "active" : "", isTransitioning ? "transitioning" : "", ].join(" ") }> Home</NavLink>
表单提交时等待状态
import React from "react";import type { Route } from "./+types/add";
import { Form, redirect } from "react-router";import { addPost } from "../../../lib/data";import { useFetcher } from "react-router";
export async function action({ request }: Route.ClientActionArgs) { const formData = await request.formData();
const { id, title } = await addPost(formData.get("title") as string); return redirect(`/post`);}
function AddPost() { const fetcher = useFetcher(); return ( <div> <fetcher.Form method="post"> <input type="text" name="title" className="border-2 border-gray-300 rounded-md p-2" /> <button type="submit"> {fetcher.state === "submitting" ? "Submitting..." : "Add"} </button> </fetcher.Form> </div> );}
export default AddPost;