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

