如何在React Router Loader中使用React Query并处理加载与错误状态?
问题描述
我正在开发一个React应用,使用react-router进行路由管理、react-query处理数据。在react-router的loader函数中获取数据时遇到以下问题:
- 在loader中调用自定义React Query Hook(
useFetchDefaultValues)时,出现Cannot read properties of null (reading 'useContext')错误,因为loader运行在非React组件上下文环境中。 - 直接在loader中调用API接口(通过
getDefaultValues函数)虽能成功获取数据,但无法利用react-query提供的isLoading、isError等状态管理UI。
相关代码
API查询与Hook定义
import { useQuery, UseQueryResult } from '@tanstack/react-query' import { getDefaultValues } from './apiClient' import { AxiosError } from 'axios' export const useFetchDefaultValues = () => { return useQuery({ queryKey: ['defaultValues'], queryFn: () => getDefaultValues(), }) } export const getDefaultValues = async () => { try { const { data } = await axios.get(`${baseUrl}/pulse/getDefaultValues`) return data } catch (error) { console.log(error) throw new Error('Error fetching default values') } }
React Router Loader函数(报错写法)
export const loader: LoaderFunction = async () => { const { data: defaultData } = await useFetchDefaultValues() return defaultData ? defaultData.results : [] }
React Router Loader函数(可行但无状态写法)
import { defer, LoaderFunction } from 'react-router-dom' import { getDefaultValues } from '@/api/apiClient' export const loader: LoaderFunction = async () => { const defaultDataPromise = getDefaultValues() return defer({ defaultData: defaultDataPromise }) }
页面组件代码
import BreadCrumb from '@/components/BreadCrumb' import DataTable from '@/components/DataGrid/DataTable' import { fetchFulfilled } from '@/features/defaultValues/defaultValuesSlice' import { useEffect } from 'react' import { useAppDispatch } from '@/Hooks/hooks' import { useLoaderData } from 'react-router-dom' const ProgramOverviewPage = () => { const defaultData = useLoaderData() as Parameter[] const dispatch = useAppDispatch() useEffect(() => { if (defaultData) { dispatch(fetchFulfilled(defaultData)) } }, [defaultData, dispatch]) return ( <div className='flex flex-col gap-4 h-full'> <div className=''> <BreadCrumb page='T_Program_Overview' /> </div> <div className='flex-1 p-6 overflow-hidden bg-background rounded-xl'> <DataTable /> </div> </div> ) } export default ProgramOverviewPage
解决方案
1. 在react-router loader中正确使用react-query的方式
Loader函数无法直接调用React Hooks(包括React Query的useQuery),因为它不在React组件上下文里。要复用React Query的缓存能力,需直接调用QueryClient实例的fetchQuery或prefetchQuery方法:
import { LoaderFunction } from 'react-router-dom' import { queryClient } from '@/path/to/your/query-client-instance' // 项目中已创建的QueryClient实例 import { getDefaultValues } from '@/api/apiClient' export const loader: LoaderFunction = async () => { // fetchQuery会自动将数据存入React Query缓存,后续组件用useQuery可直接读取缓存 const defaultData = await queryClient.fetchQuery({ queryKey: ['defaultValues'], queryFn: getDefaultValues, }) return defaultData ? defaultData.results : [] }
2. Loader中执行API查询的加载/错误状态处理方式
加载状态
React Router会自动在loader请求数据时触发路由的pending状态,通过<Suspense>组件配合路由配置即可实现加载UI:
import { Suspense } from 'react' import { RouterProvider, createBrowserRouter } from 'react-router-dom' import ProgramOverviewPage from './pages/ProgramOverviewPage' import LoadingSpinner from './components/LoadingSpinner' const router = createBrowserRouter([ { path: '/overview', element: ( <Suspense fallback={<LoadingSpinner />}> <ProgramOverviewPage /> </Suspense> ), loader: yourLoaderFunction, }, ]) function App() { return <RouterProvider router={router} /> }
错误状态
Loader中抛出的错误会被React Router捕获,通过路由的errorElement配置统一错误页面:
import { useRouteError } from 'react-router-dom' function ErrorPage() { const error = useRouteError() as Error return ( <div className='error-page'> <h2>Oops!</h2> <p>{error.message || 'Something went wrong'}</p> </div> ) } // 更新路由配置 const router = createBrowserRouter([ { path: '/overview', element: ( <Suspense fallback={<LoadingSpinner />}> <ProgramOverviewPage /> </Suspense> ), loader: yourLoaderFunction, errorElement: <ErrorPage />, }, ])
3. 直接API请求的加载与错误状态处理(不使用React Query)
如果选择在loader中直接调用API,可通过以下两种方式管理状态:
方式一:用defer配合组件内状态钩子
在loader中返回延迟的Promise:
import { defer, LoaderFunction } from 'react-router-dom' import { getDefaultValues } from '@/api/apiClient' export const loader: LoaderFunction = async () => { return defer({ defaultData: getDefaultValues(), }) }
组件中使用React Router的useAsyncValue和useAsyncError获取状态:
import { useAsyncValue, useAsyncError } from 'react-router-dom' import { Suspense } from 'react' import LoadingSpinner from './components/LoadingSpinner' import ErrorMessage from './components/ErrorMessage' const ProgramOverviewPage = () => { const defaultData = useAsyncValue() as Parameter[] const error = useAsyncError() as Error const dispatch = useAppDispatch() useEffect(() => { if (defaultData) { dispatch(fetchFulfilled(defaultData)) } }, [defaultData, dispatch]) if (error) { return <ErrorMessage message={error.message} /> } return ( <div className='flex flex-col gap-4 h-full'> {/* 原有页面内容 */} </div> ) } // 路由仍需用Suspense包裹处理加载状态 const router = createBrowserRouter([ { path: '/overview', element: ( <Suspense fallback={<LoadingSpinner />}> <ProgramOverviewPage /> </Suspense> ), loader: yourLoaderFunction, }, ])
方式二:手动在loader中处理状态并返回标记
import { LoaderFunction } from 'react-router-dom' import { getDefaultValues } from '@/api/apiClient' export const loader: LoaderFunction = async () => { try { const defaultData = await getDefaultValues() return { data: defaultData.results, error: null } } catch (err) { return { data: [], error: (err as Error).message } } }
组件中读取返回值并处理状态:
const ProgramOverviewPage = () => { const { data, error } = useLoaderData() as { data: Parameter[], error: string | null } const dispatch = useAppDispatch() useEffect(() => { if (data) { dispatch(fetchFulfilled(data)) } }, [data, dispatch]) if (error) { return <ErrorMessage message={error} /> } return ( <div className='flex flex-col gap-4 h-full'> {/* 原有页面内容 */} </div> ) }
内容的提问来源于stack exchange,提问作者said al hendi
相关产品推荐
相关产品推荐

