带useEffect的自定义React Hook无法在非组件函数中使用的解决方案咨询
问题本质
你遇到的问题核心是违反了React的Hook调用规则:所有Hook必须在React组件的顶层渲染作用域中调用,不能在事件回调、条件分支、循环或者其他嵌套函数中调用。你原来的useAxios设计是调用即自动通过useEffect触发请求,只能适配页面加载时自动发起请求的场景,无法支持手动触发(比如表单提交时发起请求)的需求。
解决方案:改造useAxios为可手动触发的版本
调整Hook的设计,移除自动执行请求的逻辑,改为返回请求触发函数,同时保留loading、data等状态和取消请求的能力,同时兼容自动请求和手动请求两种场景:
import { useState, useRef, useCallback, useEffect } from 'react'; import axios from 'axios'; export default function useAxios({ url, defaultData = {}, method = "GET", autoRequest = false} ) { const [loading, setLoading] = useState(false); const [data, setData] = useState(null); const [error, setError] = useState(null); // 存储取消请求的令牌,处理竞态和卸载清理 const cancelTokenSource = useRef(null); const triggerRequest = useCallback(async (requestData = defaultData) => { // 取消之前未完成的重复请求,避免状态混乱 if (cancelTokenSource.current) { cancelTokenSource.current.cancel('重复请求,已取消上一次'); } cancelTokenSource.current = axios.CancelToken.source(); setLoading(true); setError(null); try { const response = await axios({ url, method, // 根据请求类型自动适配参数位置 data: method.toUpperCase() !== 'GET' ? requestData : undefined, params: method.toUpperCase() === 'GET' ? requestData : undefined, cancelToken: cancelTokenSource.current.token }); const resData = response.data; setData(resData); return resData; } catch (err) { // 过滤主动取消的请求报错 if (!axios.isCancel(err)) { setError(err); throw err; } } finally { setLoading(false); } }, [url, method, defaultData]); // 如果开启自动请求,组件挂载时自动执行 useEffect(() => { if (autoRequest) triggerRequest(); }, [autoRequest, triggerRequest]); // 组件卸载时清理未完成的请求 useEffect(() => { return () => { if (cancelTokenSource.current) { cancelTokenSource.current.cancel('组件卸载,取消未完成请求'); } }; }, []); return { loading, data, error, triggerRequest }; }
事件回调中使用的示例
export default function MyComponent() { // 组件顶层调用Hook,拿到触发函数和状态 const { data, loading, triggerRequest } = useAxios({ url: "/my-end-point", method: "POST" }); async function handleSubmit(e) { e.preventDefault(); // 事件回调中仅调用触发函数,传入动态的请求参数 const submitResult = await triggerRequest({ testInput: e.target.testInput.value }); // 可直接在这里拿到请求结果做后续逻辑处理 console.log('提交返回结果:', submitResult); } return ( <form onSubmit={handleSubmit}> <input type="text" name="testInput" /> <button type="submit" disabled={loading}> {loading ? '提交中' : '提交'} </button> {data && <div>返回结果:{JSON.stringify(data)}</div>} </form> ); }
方案优势
- 完全符合React Hook调用规则,不会再有Hook调用位置的报错
- 同时兼容两种使用场景:如果需要页面加载时自动发请求,只需要在调用
useAxios时传入autoRequest: true即可,不用修改原有自动请求的业务代码 - 自带重复请求拦截、竞态处理、卸载时请求清理的能力,稳定性更强
内容的提问来源于stack exchange,提问作者Brian Emilius
相关产品推荐
相关产品推荐

