如何使用依赖Context的Hook?解决React钩子调用报错问题
问题描述
通过Context获取用户数据的组件,尝试使用该数据发起API调用时触发以下错误:
Uncaught Error: Invalid hook call. Hooks can only be called inside of the body of a function component. This could happen for one of the following reasons:
- You might have mismatching versions of React and the renderer (such as React DOM)
- You might be breaking the Rules of Hooks
- You might have more than one copy of React in the same app
核心矛盾:必须在函数组件顶层调用useAxios Hook,但此时AuthContext的authData尚未加载完成;若在条件判断或useEffect内部调用Hook,则违反React的Hook规则。
相关代码
Portfolio.js
// Portfolio.js import {Newsfeed} from "../components/Newsfeed"; import {PortfolioList} from "../components/PortfolioList"; import React, {useEffect, useState, useContext} from "react"; import AuthContext from "../context/authContext"; import {useNavigate} from "react-router-dom"; import {useAxios} from "../hooks/useAxios"; export const Portfolio = () => { const {authData} = useContext(AuthContext); const navigate = useNavigate(); let coin = null; useEffect(() => { if (!authData.signedIn) { navigate('/login'); } if (authData.signedIn) { const { response, loading , error } = useAxios(`/coins/${authData.user.data.coins[0].name}`); coin = response; } }, []); if (authData.signedIn) { return ( <main className="-mt-24 pb-8 mb-auto"> <div className="max-w-3xl mx-auto px-4 sm:px-6 lg:max-w-7xl lg:px-8"> <div className="sm:flex sm:items-center"> <div className="mb-7 sm:flex-auto"> <h1 className="text-xl font-semibold text-indigo-100">Portfolio</h1> <p className="mt-2 text-sm text-gray-200">Welcome to your portfolio.</p> </div> </div> <div className="grid grid-cols-1 gap-4 items-start lg:grid-cols-3 lg:gap-8"> {authData.signedIn && authData.user && coin && coin.data( <Newsfeed key={authData.user.data.coins[0].id} coin={coin} /> )} <PortfolioList/> </div> </div> </main> ); } }
useAxios.js
// useAxios.js import axios from "axios"; import { useEffect, useState } from "react" export const useAxios = (param) => { const [response, setResponse] = useState(null); const [loading, setLoading] = useState(false); const [error, setError] = useState(''); axios.defaults.baseURL = 'https://api.coingecko.com/api/v3'; const fetchData = async (param) => { try { setLoading(true); const result = await axios(param, { withCredentials: false }); setResponse(result.data); } catch(err) { setError(err); } finally { setLoading(false); } } useEffect(() => { fetchData(param); }, []); return { response, loading, error } }
authContext.js
/// authContext.js import React from "react"; const authData = { signedIn: false, user: null }; export default React.createContext({authData: {...authData}, setAuthData: (val) => {}});
解决方案
1. 改造useAxios Hook,支持动态参数与条件触发
修改Hook逻辑,允许空参数时不发起请求,并监听参数变化自动重新请求:
// 改造后的useAxios.js import axios from "axios"; import { useEffect, useState } from "react" export const useAxios = (param) => { const [response, setResponse] = useState(null); const [loading, setLoading] = useState(false); const [error, setError] = useState(''); axios.defaults.baseURL = 'https://api.coingecko.com/api/v3'; const fetchData = async (currentParam) => { if (!currentParam) return; // 参数为空时跳过请求 try { setLoading(true); const result = await axios(currentParam, { withCredentials: false }); setResponse(result.data); } catch(err) { setError(err); } finally { setLoading(false); } } useEffect(() => { fetchData(param); }, [param]); // 监听参数变化,参数更新时自动重新请求 return { response, loading, error } }
2. 调整Portfolio组件逻辑,遵守Hook规则
将useAxios移至组件顶层,通过动态参数控制请求时机,同时完善状态管理与导航逻辑:
// 改造后的Portfolio.js import {Newsfeed} from "../components/Newsfeed"; import {PortfolioList} from "../components/PortfolioList"; import React, {useEffect, useContext} from "react"; import AuthContext from "../context/authContext"; import {useNavigate} from "react-router-dom"; import {useAxios} from "../hooks/useAxios"; export const Portfolio = () => { const {authData} = useContext(AuthContext); const navigate = useNavigate(); // 动态生成请求参数:仅当登录且有用户硬币数据时,传入有效API路径,否则传null const requestParam = authData.signedIn && authData.user?.data?.coins[0]?.name ? `/coins/${authData.user.data.coins[0].name}` : null; // 顶层调用useAxios,遵守Hook规则 const { response: coin, loading, error } = useAxios(requestParam); // 监听登录状态变化,未登录时跳转登录页 useEffect(() => { if (!authData.signedIn) { navigate('/login'); } }, [authData.signedIn, navigate]); // 未登录时返回空或加载组件 if (!authData.signedIn) { return null; } return ( <main className="-mt-24 pb-8 mb-auto"> <div className="max-w-3xl mx-auto px-4 sm:px-6 lg:max-w-7xl lg:px-8"> <div className="sm:flex sm:items-center"> <div className="mb-7 sm:flex-auto"> <h1 className="text-xl font-semibold text-indigo-100">Portfolio</h1> <p className="mt-2 text-sm text-gray-200">Welcome to your portfolio.</p> </div> </div> <div className="grid grid-cols-1 gap-4 items-start lg:grid-cols-3 lg:gap-8"> {loading && <div>加载中...</div>} {error && <div>加载出错:{error.message}</div>} {coin && ( <Newsfeed key={authData.user.data.coins[0].id} coin={coin} /> )} <PortfolioList/> </div> </div> </main> ); }
关键说明
- 严格遵守Hook规则:所有Hook均在组件顶层调用,确保React能正确跟踪Hook调用顺序
- 动态请求控制:通过
requestParam的空值判断,避免在未获取用户数据时发起无效请求 - 响应式状态管理:直接使用
useAxios返回的response作为硬币数据,确保组件能响应数据变化自动重新渲染 - 依赖项完善:
useEffect添加authData.signedIn和navigate作为依赖,避免过时闭包问题
内容的提问来源于stack exchange,提问作者Shaun
相关产品推荐
相关产品推荐

