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

React嵌套Provider中跨Context调用authFetch API失败排查

问题:React多Context协作时API调用状态未更新

我在React应用中创建了两个Context:

  • App Provider:包含登录、登出、注册、更新等认证API调用函数,以及用于校验用户权限的authFetch工具
  • Study Provider:处理学习资料相关逻辑,需授权访问

尝试在Study Provider中引入App Context的authFetch实现fetchQuestions功能,但调用后仅返回空数组,状态未更新。将fetchQuestions移至App Context中则能正常发起API调用,请问我遗漏了什么?是否有更优的代码编写或重构方案?


React DOM 代码

<React.StrictMode>
    <AppProvider>
      <StudyProvider>
        <App />
      </StudyProvider>
    </AppProvider>
  </React.StrictMode>

studyContext.js 代码

import { useReducer, useContext, createContext, useEffect } from 'react'
import * as actionTypes from './studyActionTypes'
import { useAppContext } from '../appContext'

const initialState = {
  questions: [],
  error: null
}

const StudyContext = createContext()

const reducer = (state, action) => {
  switch (action.type) {
    case actionTypes.FETCH_QUESTIONS:
      return {
        ...state,
        questions: action.payload,
        isLoading: false,
        error: null
      }
    default:
      throw new Error(`Unknown action type: ${action.type}`)
  }
}

const StudyProvider = ({ children }) => {
  const { authFetch } = useAppContext()
  const [state, dispatch] = useReducer(reducer, initialState)

  const fetchQuestions = async () => {
    try {
      const { data } = await authFetch.get('/questions')
      const questions = data

      dispatch({
        type: actionTypes.FETCH_QUESTIONS,
        payload: questions
      })

      console.log(state.questions)
    } catch (error) {
      dispatch({
        type: actionTypes.FETCH_QUESTIONS,
        payload: [],
        error: error.message
      })
    }
  }

  useEffect(() => {
    fetchQuestions()
  }, [])

  return (
    <StudyContext.Provider
      value={{
        ...state,
        fetchQuestions
      }}
    >
      {children}
    </StudyContext.Provider>
  )
}

const useStudyContext = () => {
  return useContext(StudyContext)
}

export { StudyProvider, useStudyContext }

appContext.js 代码

import { useReducer, useContext, createContext } from 'react'
import axios from 'axios'
import * as actionTypes from './actionTypes'

const token = localStorage.getItem('token')
const user = localStorage.getItem('user')

const initialState = {
  isLoading: false,
  showAlert: false,
  alertText: '',
  alertType: '',
  user: user ? JSON.parse(user) : null,
  token: token
}

const AppContext = createContext()

const reducer = (state, action) => {
  switch (action.type) {
    case actionTypes.DISPLAY_ALERT:
      return {
        ...state,
        showAlert: false,
        alertType: '',
        alertText: ''
      }
    case actionTypes.REGISTER:
    case actionTypes.LOGIN:
      return {
        ...state,
        token: action.payload.token,
        user: action.payload.user
      }
    case actionTypes.LOGOUT:
      return {
        ...initialState,
        user: null,
        token: null
      }
    case actionTypes.UPDATE_USER:
      return {
        ...state,
        token: action.payload.token,
        user: action.payload.user
      }
    default:
      throw new Error(`Unknown action type: ${action.type}`)
  }
}

const AppProvider = ({ children }) => {
  const [state, dispatch] = useReducer(reducer, initialState)

  // Axios
  const authFetch = axios.create({
    baseURL: 'api/v1'
  })

  // Request
  authFetch.interceptors.request.use(
    (config) => {
      config.headers.common['Authorization'] = `Bearer ${state.token}`
      return config
    },
    (error) => {
      return Promise.reject(error)
    }
  )

  // Response
  authFetch.interceptors.response.use(
    (response) => {
      return response
    },
    (error) => {
      if (error.response.status === 401) {
        logout()
      }
      return Promise.reject(error)
    }
  )

  const displayAlert = () => {
    dispatch({ type: actionTypes.DISPLAY_ALERT })
  }

  const addUserToLocalStorage = ({ user, token }) => {
    localStorage.setItem('user', JSON.stringify(user))
    localStorage.setItem('token', token)
  }

  const removeUserFromLocalStorage = () => {
    localStorage.removeItem('user')
    localStorage.removeItem('token')
  }

  const registerUser = async (currentUser) => {
    try {
      const { data } = await axios.post('/api/v1/auth/register', currentUser)
      const { user, token } = data

      dispatch({
        type: actionTypes.REGISTER,
        payload: { user, token }
      })

      addUserToLocalStorage({ user, token })
    } catch (error) {
      throw new Error(error.response.data.msg)
    }
  }

  const login = async (currentUser) => {
    try {
      const { data } = await axios.post('/api/v1/auth/login', currentUser)
      const { user, token } = data

      dispatch({
        type: actionTypes.LOGIN,
        payload: { user, token }
      })

      addUserToLocalStorage({ user, token })
    } catch (error) {
      throw new Error(error.response.data.msg)
    }
  }

  const logout = () => {
    try {
      dispatch({ type: actionTypes.LOGOUT })
      removeUserFromLocalStorage()
    } catch (error) {
      throw new Error(error)
    }
  }

  const updateUser = async (currentUser) => {
    try {
      const { data } = await authFetch.patch('/auth/updateUser', currentUser)
      const { user, token } = data

      dispatch({ type: actionTypes.UPDATE_USER, payload: { user, token } })
      addUserToLocalStorage({ user, token })
    } catch (error) {
      throw new Error(error.response.data.msg)
    }
  }

  return (
    <AppContext.Provider
      value={{
        ...state,
        displayAlert,
        registerUser,
        login,
        logout,
        updateUser
      }}
    >
      {children}
    </AppContext.Provider>
  )
}

const useAppContext = () => {
  return useContext(AppContext)
}

export { AppProvider, useAppContext }

问题原因

  1. authFetch未暴露到App Context:AppProvider的value中没有传递authFetch,导致StudyContext调用useAppContext()时获取不到该工具,自然无法发起授权请求。
  2. 状态更新的异步性:console.log(state.questions)在dispatch后立即执行,此时React还未完成状态更新,打印的是旧的空数组,并非实际状态未更新。
  3. useEffect依赖缺失:StudyProvider的useEffect依赖数组为空,即使后续authFetch可用,也不会重新触发请求。
  4. 错误处理逻辑缺陷:catch中的dispatch没有正确传递error字段,导致错误状态无法被正确更新。

修复步骤

1. 在AppContext中暴露authFetch

修改appContext.js中AppProvider的返回值,将authFetch加入到value中:

return (
  <AppContext.Provider
    value={{
      ...state,
      displayAlert,
      registerUser,
      login,
      logout,
      updateUser,
      authFetch // 新增该行
    }}
  >
    {children}
  </AppContext.Provider>
)

2. 修正StudyProvider的useEffect依赖

添加authFetch作为useEffect的依赖,确保工具可用时触发请求:

useEffect(() => {
  fetchQuestions()
}, [authFetch])

3. 完善错误处理逻辑

首先更新studyContext的reducer,支持错误状态:

const reducer = (state, action) => {
  switch (action.type) {
    case actionTypes.FETCH_QUESTIONS_SUCCESS:
      return {
        ...state,
        questions: action.payload,
        isLoading: false,
        error: null
      }
    case actionTypes.FETCH_QUESTIONS_ERROR:
      return {
        ...state,
        questions: [],
        isLoading: false,
        error: action.payload
      }
    default:
      throw new Error(`Unknown action type: ${action.type}`)
  }
}

然后修改fetchQuestions的catch分支:

catch (error) {
  dispatch({
    type: actionTypes.FETCH_QUESTIONS_ERROR,
    payload: error.message
  })
}

4. 正确查看更新后的状态

不要在dispatch后立即打印state,改用useEffect监听状态变化:

useEffect(() => {
  console.log('更新后的questions:', state.questions)
}, [state.questions])

优化重构方案

1. 拆分API层

将所有API请求逻辑从Context中抽离到单独的api文件,让Context只负责状态管理,职责更清晰:

// api/auth.js
import axios from 'axios'

const authFetch = axios.create({ baseURL: 'api/v1' })

// 配置拦截器...

export const registerUser = async (userData) => {
  const { data } = await axios.post('/api/v1/auth/register', userData)
  return data
}

export const loginUser = async (userData) => {
  const { data } = await axios.post('/api/v1/auth/login', userData)
  return data
}

export default authFetch
// api/study.js
import authFetch from './auth'

export const fetchQuestions = async () => {
  const { data } = await authFetch.get('/questions')
  return data
}

2. 使用自定义Hook封装逻辑

把数据获取逻辑封装成自定义Hook,替代StudyContext中的直接处理:

// hooks/useQuestions.js
import { useState, useEffect } from 'react'
import { fetchQuestions } from '../api/study'

const useQuestions = () => {
  const [questions, setQuestions] = useState([])
  const [error, setError] = useState(null)
  const [isLoading, setIsLoading] = useState(true)

  const getQuestions = async () => {
    try {
      setIsLoading(true)
      const data = await fetchQuestions()
      setQuestions(data)
      setError(null)
    } catch (err) {
      setError(err.message)
      setQuestions([])
    } finally {
      setIsLoading(false)
    }
  }

  useEffect(() => {
    getQuestions()
  }, [])

  return { questions, error, isLoading, getQuestions }
}

export default useQuestions

3. 保持Context单一职责

当前的AuthContext+StudyContext拆分是合理的,AuthContext负责认证状态和工具,StudyContext负责学习资料状态,只要确保依赖传递正确即可。若后续逻辑复杂,可考虑使用Context组合工具,但无需强行合并。

内容的提问来源于stack exchange,提问作者isaaca

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.07.17 15:06:59