React-router 6.4的新变化
引言 #
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>
一起使用。下面是一个例子:
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>
组件也有了一些重要的变化。这些变化主要集中在三个新的属性:loader
,action
,errorElement
。
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} />
如果需要重定向,可以在loader
中return 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
,也接收一个函数,也有两个参数:params
和request
。但它们的执行时机不同: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 使用useFetcher
在element
内发请求 #
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。
版权属于: vincent
转载时须注明出处及本声明