·vincent

React-router 6.4的新变化

"深入分析React Router 6.4的变更,特别是在数据获取逻辑方面的耦合性。文章讨论了新增的API(createBrowserRouter、createMemoryRouter、createHashRouter)以及<Route>属性的变化,介绍了defer函数和<Await>组件的作用。最后给出了对版本选择的建议。"

react

引言

React Router 6.4的更新引起了开发者的广泛关注。这个版本的更新主要集中在Data API的引入,以及一些API的变化。下面,我们将一起探讨这些变化,并对其进行深入的分析。

主要更新

1. 新增API:createBrowserRoutercreateMemoryRoutercreateHashRouter

在React Router 6.4中,新增了createBrowserRoutercreateMemoryRoutercreateHashRouter这三个API,它们的主要作用是支持Data API。需要注意的是,如果你没有使用这三个API,而是像v6.0-v6.3版本一样,直接使用<BrowserRouter>等API,那么你将无法使用Data API。

1.1 使用方法

新的API需要结合<RouterProvider>一起使用。下面是一个例子:

jsx
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配置。例如:

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>组件也有了一些重要的变化。这些变化主要集中在三个新的属性:loaderactionerrorElement

2.1 什么是Data API?

Data API允许你将数据获取逻辑写入路由定义中。每当路由切换到对应的位置时,会自动获取数据。这一功能通过<Route>的新属性实现。

2.2 loader属性

loader属性接受一个函数(可以是异步函数)。每次渲染对应路由的element之前,都会执行这个函数。在element内部,你可以使用useLoaderData这个hook来获取函数的返回值。

jsx
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获取当前页面的参数:

jsx
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

jsx
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} />

如果需要重定向,可以在loaderreturn redirect

jsx
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会被渲染。

jsx
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钩子获取异常。

jsx
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,也接收一个函数,也有两个参数:paramsrequest。但它们的执行时机不同:loader是在用户通过GET导航至某路由时执行的,而action是在用户提交表单时执行的。

jsx
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 使用useFetcherelement内发请求

useFetcher是React Router 6.4提供的一个新的hook,它可以在<Route>对应的element内部发起API请求,并展示Loading态。这样,即使API请求需要花费一些时间,用户也可以看到Loading态,而不是一个空白的页面。

jsx
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。例如:

jsx
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中。例如:

jsx
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。