React-router 6.4的新变化
"深入分析React Router 6.4的变更,特别是在数据获取逻辑方面的耦合性。文章讨论了新增的API(createBrowserRouter、createMemoryRouter、createHashRouter)以及<Route>属性的变化,介绍了defer函数和<Await>组件的作用。最后给出了对版本选择的建议。"
引言
React Router 6.4的更新引起了开发者的广泛关注。这个版本的更新主要集中在Data API的引入,以及一些API的变化。下面,我们将一起探讨这些变化,并对其进行深入的分析。
主要更新
1. 新增API:createBrowserRouter
,createMemoryRouter
,createHashRouter
在React Router 6.4中,新增了createBrowserRouter
,createMemoryRouter
,createHashRouter
这三个API,它们的主要作用是支持Data API。需要注意的是,如果你没有使用这三个API,而是像v6.0
-v6.3
版本一样,直接使用<BrowserRouter>
等API,那么你将无法使用Data API。
1.1 使用方法
新的API需要结合<RouterProvider>
一起使用。下面是一个例子:
1import * as React from "react";
2import * as ReactDOM from "react-dom";
3import {
4 createBrowserRouter,
5 RouterProvider,
6} from "react-router-dom";
7
8
9const router = createBrowserRouter([
10 {
11 path: "/",
12 element: <Root />,
13 children: [
14 {
15 path: "team",
16 element: <Team />,
17 },
18 ],
19 },
20]);
21
22ReactDOM.createRoot(document.getElementById("root")).render(
23 <RouterProvider router={router} />
24);
1.2 也可以使用JSX定义路由
如果你更喜欢使用JSX语法定义路由,React Router 6.4也提供了JSX配置。例如:
1const router = createBrowserRouter(
2 createRoutesFromElements(
3 <Route path="/" element={<Root />}>
4 <Route path="dashboard" element={<Dashboard />} />
5 <Route path="about" element={<About />} />
6 </Route>
7 )
8);
2. <Route>
的变化
在React Router 6.4中,<Route>
组件也有了一些重要的变化。这些变化主要集中在三个新的属性:loader
,action
,errorElement
。
2.1 什么是Data API?
Data API允许你将数据获取逻辑写入路由定义中。每当路由切换到对应的位置时,会自动获取数据。这一功能通过<Route>
的新属性实现。
2.2 loader
属性
loader
属性接受一个函数(可以是异步函数)。每次渲染对应路由的element
之前,都会执行这个函数。在element
内部,你可以使用useLoaderData
这个hook来获取函数的返回值。
1<Route
2 loader={async ({ request }) => {
3 const res = await fetch("/api/user.json", {
4 signal: request.signal,
5 });
6 const user = await res.json();
7 return user;
8 }}
9 element={<Xxxxxx />}
10/>
loader
函数可以接收两个参数:params
(如果Route
中包含参数)和request
(一个Fetch API的Request
对象,代表一个请求)。你可以通过request
获取当前页面的参数:
1<Route
2 loader={async ({ request }) => {
3 const url = new URL(request.url);
4 const searchTerm = url.searchParams.get("q");
5 return searchProducts(searchTerm);
6 }}
7/>
loader
函数的返回值可以在element
中通过useLoaderData
钩子获取。React Router官方建议返回一个Fetch API的Response
对象。你可以直接return fetch(url, config);
,也可以自己构造一个假的Response
:
1function loader({ request, params }) {
2 const data = { some: "thing" };
3 return new Response(JSON.stringify(data), {
4 status: 200,
5 headers: {
6 "Content-Type": "application/json; utf-8",
7 },
8 });
9}
10//...
11<Route loader={loader} />
如果需要重定向,可以在loader
中return redirect
:
1import { redirect } from "react-router-dom";
2
3const loader = async () => {
4 const user = await getUser();
5 if (!user) {
6 return redirect("/login");
7 }
8};
如果数据获取失败,或者由于其他原因不能让Route
对应的element
正常渲染,可以在loader
中抛出异常。这时,<Route>
的errorElement
会被渲染。
1function loader({ request, params }) {
2 const res = await fetch(`/api/properties/${params.id}`);
3 if (res.status === 404) {
4 throw new Response("Not Found", { status: 404 });
5 }
6 return res.json();
7}
8//...
9<Route loader={loader} />
2.2 errorElement
属性
当loader
内抛出异常时,<Route>
会渲染errorElement
而不是element
。异常可以冒泡,每一层<Route>
都可以定义errorElement
。在errorElement
内部,可以使用useRouteError
钩子获取异常。
1function RootBoundary() {
2 const error = useRouteError();
3 if (isRouteErrorResponse(error)) {
4 if (error.status === 404) {
5 return <div>This page doesn't exist!</div>;
6 }
7 if (error.status === 503) {
8 return <div>Looks like our API is down</div>;
9 }
10 }
11 return <div>Something went wrong</div>;
12}
2.3 action
属性
action
属性类似于loader
,也接收一个函数,也有两个参数:params
和request
。但它们的执行时机不同:loader
是在用户通过GET导航至某路由时执行的,而action
是在用户提交表单时执行的。
1<Route
2 path="/properties/:id"
3 element={<PropertyForSale />}
4 errorElement={<PropertyError />}
5 action={async ({ params }) => {
6 const res = await fetch(`/api/properties/${params.id}`);
7 if (res.status === 404) {
8 throw new Response("Not Found", { status: 404 });
9 }
10 const home = res.json();
11 return { home };
12 }}
13/>
在element
内部,可以使用useActionData
钩子获取action
的返回值。
这些新属性使得<Route>
组件在处理数据加载和异常处理方面更加强大和灵活。
3. 处理页面加载状态:defer
函数与<Await>
组件
由于引入了loader
,内部有API请求,必然导致路由切换时,页面需要时间去加载。加载时间长了怎么办?需要展示Loading态。React Router 6.4为此提供了两种解决方案:一种是在<Route>
对应的element
里发请求并展示Loading态,另一种是针对loader
,提供一种配置方案,允许开发者定义Loading态。下面我们来详细介绍这两种方案。
3.1 使用useFetcher
在element
内发请求
useFetcher
是React Router 6.4提供的一个新的hook,它可以在<Route>
对应的element
内部发起API请求,并展示Loading态。这样,即使API请求需要花费一些时间,用户也可以看到Loading态,而不是一个空白的页面。
1function Book() {
2 const fetcher = useFetcher();
3 const [book, setBook] = useState(null);
4
5 useEffect(() => {
6 fetcher(fetch("/api/book.json")).then((book) => {
7 setBook(book);
8 });
9 }, [fetcher]);
10
11 if (book === null) {
12 return <Loading />;
13 }
14
15 return <BookDetails book={book} />;
16}
3.2 使用defer
函数和<Await>
组件定义Loading态
除了在element
内部发起API请求,React Router 6.4还提供了defer
函数和<Await>
组件,让开发者可以自定义loader
的Loading态。
defer
函数用于标记一个loader需要展示加载状态。如果loader返回了defer
,那么会直接渲染<Route>
的element
。例如:
1<Route
2 loader={async () => {
3 let book = await getBook(); // 这个不会展示 Loading 态,因为它被 await 了,会等它执行完并拿到数据
4 let reviews = getReviews(); // 这个会展示 Loading 态
5 return defer({
6 book, // 这是数据
7 reviews, // 这是 promise
8 });
9 }}
10 element={<Book />}
11/>
<Await>
组件用于在<Route>
的element
中展示加载状态。它需要和<Suspense>
一起使用,加载状态会展示在<Suspense>
的fallback
中。例如:
1function Book() {
2 const { book, reviews } = useLoaderData();
3 return (
4 <div>
5 <h1>{book.title}</h1>
6 <p>{book.description}</p>
7 <React.Suspense fallback={<ReviewsSkeleton />}>
8 <Await resolve={reviews}>
9 <Reviews />
10 </Await>
11 </React.Suspense>
12 </div>
13 );
14}
在loader加载完成后,<Await>
的children
将会被渲染。
这两种方案使得React Router 6.4在处理页面加载状态上更加灵活和强大。
3. 个人观点
虽然React Router 6.4引入了Data API,但我认为这可能会导致一些问题。首先,如果一个项目的一部分数据获取逻辑在Router中,而另一部分在内部组件中,这将不利于项目的维护。其次,为了加入Data API,React Router 6.4增加了大量的代码,这使得它的体积大幅增加。
结论
考虑到上述的问题,我个人更倾向于使用react-router-dom=~6.3.0
版本,而不是升级到6.4。