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

React中如何在onSubmit事件处理函数中正确调用自定义Hook

问题产生原因

React 对 Hook 的调用有强制规则,仅支持两个合法调用位置:

  • React 函数组件的顶层作用域
  • 自定义 Hook 的顶层作用域

这个规则的核心逻辑是:React 完全依靠 Hook 每次渲染时的固定调用顺序,来把 Hook 内部维护的状态和组件实例做绑定关联。handleSubmit 属于用户交互触发的事件回调,不会在组件每次渲染时按固定顺序执行,在这个位置调用 Hook 会打乱整个 Hook 调用链,React 无法正确追踪 Hook 对应的状态,因此会抛出你看到的 "React hook is called in function that is neither a React function or a custom React Hook function" 报错。

自定义 Hook 本质是对 React 内置 Hook 的逻辑封装,同样必须遵守上述调用规则,和你写的业务逻辑本身没有关系,纯粹是调用位置不合法导致的问题。

正确实现方案

不要在事件处理函数里直接调用自定义 Hook,要先在组件顶层调用 Hook,拿到 Hook 暴露的状态、触发方法,再在事件回调里调用对应的触发方法即可。根据业务场景,分两种常见实现方式:

场景1:提交时手动触发校验/请求

如果你的逻辑是点击提交按钮时才发起用户校验请求,就把自定义Hook里的请求逻辑封装成可调用的函数,通过Hook返回值暴露出来:

// 调整后的自定义Hook
const useLoginHook = () => {
  const [isUser, setIsUser] = useState(null);
  const [loading, setLoading] = useState(false);

  // 把手动触发的逻辑单独抽成函数返回
  const checkUser = async (username) => {
    setLoading(true);
    try {
      // 替换为你的实际API请求逻辑
      const res = await fetch(`/api/check-user?name=${username}`);
      const data = await res.json();
      setIsUser(data.exist);
      return data.exist;
    } catch (err) {
      setIsUser(false);
      return false;
    } finally {
      setLoading(false);
    }
  };

  return { isUser, loading, checkUser };
};

组件里的调用方式:

const Login = () => {
  const userRef = useRef();
  const [user, setUser] = useState("");
  const [validName, setValidName] = useState(false);
  const [userFocus, setUserFocus] = useState(false);
  
  // 顶层调用Hook,拿到状态和触发方法
  const { isUser, loading, checkUser } = useLoginHook();

  const handleSubmit = async (e) => {
    e.preventDefault();
    // 事件回调里只调用Hook返回的业务方法,不直接调用Hook本身
    const userExist = await checkUser(user);
    if (userExist) {
      // 写后续登录跳转等逻辑
    }
  };

  return (
    <>
      <main className="App">
        <form onSubmit={handleSubmit}>
          <input
            type="text"
            id="username"
            ref={userRef}
            autoComplete="off"
            onChange={(e) => setUser(e.target.value)}
            value={user}
            required
            aria-invalid={validName ? "false" : "true"}
            aria-describedby="uidnote"
            onFocus={() => setUserFocus(true)}
            onBlur={() => setUserFocus(false)}
          />
          {loading && <p>用户校验中...</p>}
          <button type="submit" disabled={loading}>登录</button>
        </form>
      </main>
    </>
  );
};

场景2:输入内容变化时自动触发校验

如果你的逻辑是用户输入用户名时就自动实时校验,不需要等提交再触发,就把校验逻辑放到Hook内部的useEffect里,监听入参变化自动执行,组件顶层调用Hook后直接读取返回的状态即可,不需要在提交函数里做额外调用:

// 自动校验的自定义Hook
const useLoginHook = (username) => {
  const [isUser, setIsUser] = useState(null);
  const [loading, setLoading] = useState(false);

  useEffect(() => {
    if (!username) {
      setIsUser(null);
      return;
    }
    const controller = new AbortController();
    const checkUser = async () => {
      setLoading(true);
      try {
        const res = await fetch(`/api/check-user?name=${username}`, {signal: controller.signal});
        const data = await res.json();
        setIsUser(data.exist);
      } catch (err) {
        if (err.name !== 'AbortError') setIsUser(false);
      } finally {
        setLoading(false);
      }
    };
    checkUser();
    // 组件卸载或参数变化时取消上一次请求,避免竞态问题
    return () => controller.abort();
  }, [username]);

  return { isUser, loading };
};

组件中使用时,直接在顶层传入user参数即可:

const { isUser, loading } = useLoginHook(user);
const handleSubmit = (e) => {
  e.preventDefault();
  // 直接读取已经校验好的isUser状态做后续逻辑
  if (isUser) {
    // 登录逻辑
  }
}

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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.08.27 23:36:27