Next.js中React自定义Hook触发Invalid hook call错误的排查求助
问题原因与解决方案
这个错误的核心原因是你违反了React Hook的调用规则——Hook只能在函数组件的顶层作用域,或者其他自定义Hook的顶层作用域中调用,绝对不能在事件处理函数、条件判断、循环这类嵌套逻辑里间接或直接调用。
具体分析你的代码调用链
来看你的调用流程:
- 在
PlgButton组件的clickHandler(按钮点击事件的回调函数)里,调用了twigExistsHandler - 而
twigExistsHandler内部直接调用了自定义HookuseTwigExistsRequest
因为clickHandler是事件触发的回调函数,属于组件的嵌套逻辑,不是组件的顶层作用域,所以在这里间接调用Hook就触发了Invalid hook call错误。
反观你正常运行的useChangeBranchRequest,它应该是在某个函数组件的顶层被调用的(比如组件的最外层,不是在事件或条件里),所以符合Hook规则,能正常工作。
修复方案
针对你的场景,有两种更合适的修复方式:
方案1:将检查逻辑改为普通异步函数(更适合事件触发场景)
既然这个检查是在按钮点击时触发的,不需要依赖React的生命周期或状态订阅,完全可以把它改成普通的异步函数,而不是自定义Hook:
首先修改twigExistsRequest.js为普通函数:
import {webApiUrl} from "@/library/webHelpers"; export async function checkTwigExists({ parentDirSegment, name }) { try { const response = await fetch( webApiUrl() + '/branchName', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({parentDirSegment, name}) } ); const answer = await response.json(); if (undefined !== answer['exists']) { return answer['exists']; } else if (undefined !== answer['err']) { console.log(answer['err']); return 'not'; } } catch (err) { console.log(err); return 'not'; } }
然后修改twigExistsHandler.js为普通函数:
import {checkTwigExists} from "@/library/twigExistsRequest"; export default async function twigExistsHandler({ parentDirSegment, name, action, treeStateDispatch }) { const exists = await checkTwigExists({ parentDirSegment, name }); if (exists === 'exists') { treeStateDispatch({ action, name, dirSegment: parentDirSegment }); } }
最后在PlgButton的clickHandler里调用这个异步函数:
// ... 其他代码不变 async function clickHandler(action) { let name; switch (action) { case ACTIONS.add: name = window.prompt('New name:'); if (name) { // 增加空值判断,避免用户取消输入 await twigExistsHandler({ parentDirSegment: dirSegment, name, action, treeStateDispatch }); } break; case ACTIONS.dup: name = window.prompt('Dup name:'); if (name) { await twigExistsHandler({ parentDirSegment: dirSegment.slice(0,dirSegment.lastIndexOf('/')), name, action, treeStateDispatch }); } break; case ACTIONS.del: window.confirm('Irrevocably delete the whole playground?') && treeStateDispatch({ info: '', dirSegment, action }); break; } }
方案2:将Hook移到组件顶层,用状态触发请求
如果你坚持要用Hook,可以把useTwigExistsRequest移到PlgButton组件的顶层,用状态来控制是否执行检查:
修改useTwigExistsRequest.js,加入依赖项并通过状态触发:
import {useEffect, useRef} from "react"; import {webApiUrl} from "@/library/webHelpers"; export function useTwigExistsRequest({ checkTrigger, parentDirSegment, name, action, treeStateDispatch }) { const nameExists = useRef('not'); useEffect(() => { // 当checkTrigger变化时,执行检查 if (checkTrigger && name) { async function fetchNameValidation() { try { const response = await fetch( webApiUrl() + '/branchName', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({parentDirSegment, name}) } ); const answer = await response.json(); if (undefined !== answer['exists']) { nameExists.current = answer['exists']; if (nameExists.current === 'exists') { treeStateDispatch({ action, name, dirSegment: parentDirSegment }); } } else if (undefined !== answer['err']) { console.log(answer['err']); } } catch (err) { console.log(err); } } fetchNameValidation(); } }, [checkTrigger, parentDirSegment, name, action, treeStateDispatch]); // 加入所有依赖项 }
然后在PlgButton组件里使用这个Hook:
import {ACTIONS} from "@/library/common"; import {useState} from "react"; import {useTwigExistsRequest} from "@/library/twigExistsRequest"; export default function PlgButton({dirSegment, action, text, treeStateDispatch}) { const [info, SetInfo] = useState(''); const [parentDirSegment, SetParentDirSegment] = useState(''); // 新增状态控制检查触发 const [checkTrigger, setCheckTrigger] = useState(0); const [checkParams, setCheckParams] = useState({}); // 在组件顶层调用Hook useTwigExistsRequest({ checkTrigger, ...checkParams, treeStateDispatch }); if (info) SetInfo(''); return ( <button className="btn btn-secondary btn-sm new-plg-btn" onClick={() => { clickHandler(action); }} > {text} </button> ); function clickHandler(action) { let name; switch (action) { case ACTIONS.add: name = window.prompt('New name:'); if (name) { setCheckParams({ parentDirSegment: dirSegment, name, action }); setCheckTrigger(prev => prev + 1); // 通过更新trigger触发检查 } break; case ACTIONS.dup: name = window.prompt('Dup name:'); if (name) { setCheckParams({ parentDirSegment: dirSegment.slice(0,dirSegment.lastIndexOf('/')), name, action }); setCheckTrigger(prev => prev + 1); } break; case ACTIONS.del: window.confirm('Irrevocably delete the whole playground?') && treeStateDispatch({ info: '', dirSegment, action }); break; } } }
最后再强调下React Hook的核心规则
- 只能在函数组件的顶层调用Hook:不能在嵌套函数、条件判断、循环、事件处理函数里调用;
- 只能在React函数组件或自定义Hook里调用Hook:普通JavaScript函数里不能调用Hook。
遵循这两条规则,就能避免这类错误啦。
内容的提问来源于stack exchange,提问作者ejoo
相关产品推荐
相关产品推荐

