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

Next.js中React自定义Hook触发Invalid hook call错误的排查求助

问题原因与解决方案

这个错误的核心原因是你违反了React Hook的调用规则——Hook只能在函数组件的顶层作用域,或者其他自定义Hook的顶层作用域中调用,绝对不能在事件处理函数、条件判断、循环这类嵌套逻辑里间接或直接调用。

具体分析你的代码调用链

来看你的调用流程:

  1. 在PlgButton组件的clickHandler(按钮点击事件的回调函数)里,调用了twigExistsHandler
  2. 而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的核心规则

  1. 只能在函数组件的顶层调用Hook:不能在嵌套函数、条件判断、循环、事件处理函数里调用;
  2. 只能在React函数组件或自定义Hook里调用Hook:普通JavaScript函数里不能调用Hook。

遵循这两条规则,就能避免这类错误啦。

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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.04.29 00:27:37