You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

如何在React Router Loader中使用React Query并处理加载与错误状态?

问题描述

我正在开发一个React应用,使用react-router进行路由管理、react-query处理数据。在react-router的loader函数中获取数据时遇到以下问题:

  1. 在loader中调用自定义React Query Hook(useFetchDefaultValues)时,出现Cannot read properties of null (reading 'useContext')错误,因为loader运行在非React组件上下文环境中。
  2. 直接在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

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.06.18 12:28:10