React-router 6.4的新变化

引言

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>一起使用。下面是一个例子:

import * as React from "react";
import * as ReactDOM from "react-dom";
import {
  createBrowserRouter,
  RouterProvider,
} from "react-router-dom";


const router = createBrowserRouter([
  {
    path: "/",
    element: <Root />,
    children: [
      {
        path: "team",
        element: <Team />,
      },
    ],
  },
]);

ReactDOM.createRoot(document.getElementById("root")).render(
  <RouterProvider router={router} />
);
import * as React from "react";
import * as ReactDOM from "react-dom";
import {
  createBrowserRouter,
  RouterProvider,
} from "react-router-dom";


const router = createBrowserRouter([
  {
    path: "/",
    element: <Root />,
    children: [
      {
        path: "team",
        element: <Team />,
      },
    ],
  },
]);

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

1.2 也可以使用JSX定义路由

如果你更喜欢使用JSX语法定义路由,React Router 6.4也提供了JSX配置。例如:

const router = createBrowserRouter(
  createRoutesFromElements(
    <Route path="/" element={<Root />}>
      <Route path="dashboard" element={<Dashboard />} />
      <Route path="about" element={<About />} />
    </Route>
  )
);
const router = createBrowserRouter(
  createRoutesFromElements(
    <Route path="/" element={<Root />}>
      <Route path="dashboard" element={<Dashboard />} />
      <Route path="about" element={<About />} />
    </Route>
  )
);

2. <Route>的变化

在React Router 6.4中,<Route>组件也有了一些重要的变化。这些变化主要集中在三个新的属性:loaderactionerrorElement

2.1 什么是Data API?

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

2.2 loader属性

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

<Route
  loader={async ({ request }) => {
    const res = await fetch("/api/user.json", {
      signal: request.signal,
    });
    const user = await res.json();
    return user;
  }}
  element={<Xxxxxx />}
/>
<Route
  loader={async ({ request }) => {
    const res = await fetch("/api/user.json", {
      signal: request.signal,
    });
    const user = await res.json();
    return user;
  }}
  element={<Xxxxxx />}
/>

loader函数可以接收两个参数:params(如果Route中包含参数)和request(一个Fetch API的Request对象,代表一个请求)。你可以通过request获取当前页面的参数:

<Route
  loader={async ({ request }) => {
    const url = new URL(request.url);
    const searchTerm = url.searchParams.get("q");
    return searchProducts(searchTerm);
  }}
/>
<Route
  loader={async ({ request }) => {
    const url = new URL(request.url);
    const searchTerm = url.searchParams.get("q");
    return searchProducts(searchTerm);
  }}
/>

loader函数的返回值可以在element中通过useLoaderData钩子获取。React Router官方建议返回一个Fetch API的Response对象。你可以直接return fetch(url, config);,也可以自己构造一个假的Response

function loader({ request, params }) {
  const data = { some: "thing" };
  return new Response(JSON.stringify(data), {
    status: 200,
    headers: {
      "Content-Type": "application/json; utf-8",
    },
  });
}
//...
<Route loader={loader} />
function loader({ request, params }) {
  const data = { some: "thing" };
  return new Response(JSON.stringify(data), {
    status: 200,
    headers: {
      "Content-Type": "application/json; utf-8",
    },
  });
}
//...
<Route loader={loader} />

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

import { redirect } from "react-router-dom";

const loader = async () => {
  const user = await getUser();
  if (!user) {
    return redirect("/login");
  }
};
import { redirect } from "react-router-dom";

const loader = async () => {
  const user = await getUser();
  if (!user) {
    return redirect("/login");
  }
};

如果数据获取失败,或者由于其他原因不能让Route对应的element正常渲染,可以在loader中抛出异常。这时,<Route>errorElement会被渲染。

function loader({ request, params }) {
  const res = await fetch(`/api/properties/${params.id}`);
  if (res.status === 404) {
    throw new Response("Not Found", { status: 404 });
  }
  return res.json();
}
//...
<Route loader={loader} />
function loader({ request, params }) {
  const res = await fetch(`/api/properties/${params.id}`);
  if (res.status === 404) {
    throw new Response("Not Found", { status: 404 });
  }
  return res.json();
}
//...
<Route loader={loader} />

2.2 errorElement属性

loader内抛出异常时,<Route>会渲染errorElement而不是element。异常可以冒泡,每一层<Route>都可以定义errorElement。在errorElement内部,可以使用useRouteError钩子获取异常。

function RootBoundary() { 
  const error = useRouteError(); 
  if (isRouteErrorResponse(error)) { 
    if (error.status === 404) { 
      return <div>This page doesn't exist!</div>; 
      } 
      if (error.status === 503) { 
        return <div>Looks like our API is down</div>; 
      }
    }
    return <div>Something went wrong</div>;
}
function RootBoundary() { 
  const error = useRouteError(); 
  if (isRouteErrorResponse(error)) { 
    if (error.status === 404) { 
      return <div>This page doesn't exist!</div>; 
      } 
      if (error.status === 503) { 
        return <div>Looks like our API is down</div>; 
      }
    }
    return <div>Something went wrong</div>;
}

2.3 action属性

action属性类似于loader,也接收一个函数,也有两个参数:paramsrequest。但它们的执行时机不同:loader是在用户通过GET导航至某路由时执行的,而action是在用户提交表单时执行的。

<Route
  path="/properties/:id"
  element={<PropertyForSale />}
  errorElement={<PropertyError />}
  action={async ({ params }) => {
    const res = await fetch(`/api/properties/${params.id}`);
    if (res.status === 404) {
      throw new Response("Not Found", { status: 404 });
    }
    const home = res.json();
    return { home };
  }}
/>
<Route
  path="/properties/:id"
  element={<PropertyForSale />}
  errorElement={<PropertyError />}
  action={async ({ params }) => {
    const res = await fetch(`/api/properties/${params.id}`);
    if (res.status === 404) {
      throw new Response("Not Found", { status: 404 });
    }
    const home = res.json();
    return { home };
  }}
/>

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态,而不是一个空白的页面。

function Book() {
  const fetcher = useFetcher();
  const [book, setBook] = useState(null);

  useEffect(() => {
    fetcher(fetch("/api/book.json")).then((book) => {
      setBook(book);
    });
  }, [fetcher]);

  if (book === null) {
    return <Loading />;
  }

  return <BookDetails book={book} />;
}
function Book() {
  const fetcher = useFetcher();
  const [book, setBook] = useState(null);

  useEffect(() => {
    fetcher(fetch("/api/book.json")).then((book) => {
      setBook(book);
    });
  }, [fetcher]);

  if (book === null) {
    return <Loading />;
  }

  return <BookDetails book={book} />;
}

3.2 使用defer函数和<Await>组件定义Loading态

除了在element内部发起API请求,React Router 6.4还提供了defer函数和<Await>组件,让开发者可以自定义loader的Loading态。

defer函数用于标记一个loader需要展示加载状态。如果loader返回了defer,那么会直接渲染<Route>element。例如:

<Route
  loader={async () => {
    let book = await getBook(); // 这个不会展示 Loading 态,因为它被 await 了,会等它执行完并拿到数据
    let reviews = getReviews(); // 这个会展示 Loading 态
    return defer({
      book, // 这是数据
      reviews, // 这是 promise
    });
  }}
  element={<Book />}
/>
<Route
  loader={async () => {
    let book = await getBook(); // 这个不会展示 Loading 态,因为它被 await 了,会等它执行完并拿到数据
    let reviews = getReviews(); // 这个会展示 Loading 态
    return defer({
      book, // 这是数据
      reviews, // 这是 promise
    });
  }}
  element={<Book />}
/>

<Await>组件用于在<Route>element中展示加载状态。它需要和<Suspense>一起使用,加载状态会展示在<Suspense>fallback中。例如:

function Book() {
  const { book, reviews } = useLoaderData();
  return (
    <div>
      <h1>{book.title}</h1>
      <p>{book.description}</p>
      <React.Suspense fallback={<ReviewsSkeleton />}>
        <Await resolve={reviews}>
          <Reviews />
        </Await>
      </React.Suspense>
    </div>
  );
}
function Book() {
  const { book, reviews } = useLoaderData();
  return (
    <div>
      <h1>{book.title}</h1>
      <p>{book.description}</p>
      <React.Suspense fallback={<ReviewsSkeleton />}>
        <Await resolve={reviews}>
          <Reviews />
        </Await>
      </React.Suspense>
    </div>
  );
}

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