每个路由都可以定义一个“加载器”函数,在渲染路由元素之前为路由组件提供数据。
仅当使用数据路由时,此功能才有效。
createBrowserRouter([
{
path: '/',
element: <App/>,
loader: async () => {
return fetch("https://httpbin.org/get")
},
children: [
{
path: 'home',
element: <Home/>,
loader: async () => {
return fetch("https://httpbin.org/get")
},
}
]
}
])
当用户在应用程序中进行导航时,将并行调用下一个匹配路由分支的加载器,并通过useLoaderData
将其数据提供给组件。
const router = createBrowserRouter([
{
path: "/",
element: <App/>,
loader: () => ({username: "zs", age: 20}),
children: [
{
path: "hoem",
element: <Home/>,
loader: () => ({username: "ls", age: 21}),
}
]
}
])
function Home() {
const loaderData = useLoaderData()
return <div>name: {loaderData.username}</div>
}
params
params
是从动态路由段中解析出来的参数,并传递给加载器:
createBrowserRouter([
{
path: '/',
element: <App/>,
loader: async () => {
return fetch("https://httpbin.org/get")
},
children: [
{
path: 'team/:id',
element: <Team/>,
loader: async ({params}) => {
return fetch(`https://httpbin.org/get?id=${params.id}`)
},
}
]
}
])
路径中的:id
被解析为相同名称的params.id
request
最常见的用例是创建 URL,并从中读取URLSearchParams
。
export async function loader({request, context}: LoaderFunctionArgs) {
const url = new URL(request.url)
const params = url.searchParams.get('id')
return { id: params }
}
返回响应
从loader
中你可以返回任何你想要返回的数据,并从useLoaderData
中得到它,甚至可以在loader
中进行网络请求,例如:
export async function loader({request, context}: LoaderFunctionArgs) {
return fetch("https://httpbin.org/get?name=zs&age=20")
}
React Router 会自动调用 response.json()
,因此组件在渲染时不需要解析它:
function SomeRoute() {
const loaderData = useLoaderData();
console.log(loaderData.args) // {age: '20', name: 'zs'}
return <div>{loaderData.args.name}---{loaderData.args.age}</div>
}
您也可以自己构建响应:
function loader({ request, params }) {
const data = { some: "thing" };
return new Response(JSON.stringify(data), {
status: 200,
headers: {
"Content-Type": "application/json; utf-8",
},
});
}
抛出异常
你可以在加载器中使用throw
来跳出当前堆栈的,React Router 将渲染异常组件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();
}
延迟数据❓
function sleep(ms: number): Promise<IUser> {
return new Promise(resolve => setTimeout(() => resolve({
name: "zs", age: 20
}), ms));
}
export async function loader({params, request, context}: LoaderFunctionArgs) {
return {
user: await sleep(1000)
}
}
export const Component = () => {
const data = useLoaderData() as { user: IUser }
return (
<div>
<h1>welcome</h1>
<p>
Your name is {data.user.name} and you are {data.user.age} years old.
</p>
</div>
);
};
上述事例中,我们通过sleep
函数来模拟网络请求,我们会发现在慢速网络中,loader
中的网络请求会导致导航的延迟跳转和视图的渲染。在网络非常慢的情况下,loader
会直到请求得到响应后,才进行路由的跳转和视图的渲染,这将会导致页面的加载或跳转到该路由所需的时间被拉长。为了改善这一点:
如果这些方法不起作用,我们不得不将网络请求从loader
中移除,转而在组件中获取数据。
但是在大多数情况下仍然是不太理想的,特别是当你正在对路由组件进行代码拆分,原因有两个:
客户端将数据请求放在瀑布流上:文档 -> JavaScript -> 懒加载路由 -> 数据获取。
解决方案✅
React Router 利用 React18 的Suspense
通过defer
响应工具和<Await>
组件或useAsyncValue
钩子来进行数据获取。通过使用这些API,你可以解决以下两个问题:
你的网络请求不再在瀑布流上:文档 -> JavaScript -> 路由懒加载和获取数据(并行)。
使用Suspense
和Await
包裹延迟渲染的视图,以便在渲染回退UI时候使用,使用defer
包裹延迟的数据。
或者你也可以使用useAsyncValue
钩子,但你需要将代码分离到另一个组件中:
现在,我们不必等得到响应数据,在进行导航的跳转,而是在用户跳转新路由时立即获取数据。在网速较慢的情况下,此处会展示你所提供的loading
状态。如果请求失败它会显示errorElement
,请求成功则显示你所提供的视图。
此外,你还可以根据是否包含await
关键字来切换某些内容是否被延迟。
return defer({
// not deferred
user1: await sleep(1000),
// deferred
user2: sleep(1000)
})
为什么加载器返回的响应对象不再起作用?
当你使用defer
时,你告诉了 React Router 立即加载页面,而不是延迟加载。页面在 Response 对象返回之前已经加载,因此响应不会像使用return fetch()
一样自动处理。
你需要处理自己的 Response,并解析延迟的 Promise,而不是 Promise 实例。
async function loader() {
return defer({
data: fetch(url).then(res => res.json())
})
}
甚至,我们可以嵌套多层网络请求。
function sleep(ms: number): Promise<IUser> {
return new Promise(resolve => setTimeout(() => resolve({
name: "zs", age: 20
}), ms));
}
function sleep1(ms: number): Promise<IUser> {
return new Promise(resolve => setTimeout(() => resolve({
name: "ls", age: 21
}), ms));
}
export async function loader({params, request, context}: LoaderFunctionArgs) {
const user = sleep(1000).then(async res => {
return await sleep1(1000)
})
return defer({
user
})
}