React应用导航特定链接时Firefox报Invalid Hook Call错误求解
问题描述
在React应用中导航至某特定链接时,Firefox控制台会抛出Invalid Hook Call错误,但Chrome/Edge中访问同一链接无此问题。已检查报错组件,确认Hooks使用符合规范。
错误截图:
报错组件代码:
import { useState, useEffect } from 'react' import { useParams, useNavigate, Link } from 'react-router-dom' import { FontAwesomeIcon } from '@fortawesome/react-fontawesome' import { faTrashCan, faEdit } from '@fortawesome/free-solid-svg-icons' import Modal from './modal' import ErrorModal from './errorModal' import { useAuthContext } from '../hooks/useAuth' import { Remarkable } from "remarkable" function SingleBlog() { const params = useParams() const navigate = useNavigate() const [blog, setBlog] = useState(null) const [error, setError] = useState(null) const { user } = useAuthContext() const md = new Remarkable() useEffect(() => { async function getSingleBlog() { if (!user) { setError('You are not logged in.') displayError() return } const response = await fetch(`/api/${params.id}`, { method: 'GET', headers: { accepts: 'application/json', Authorization: `Bearer ${user.token}`, }, }) const json = await response.json() if (response.status !== 200) { setError(json.error) displayError() return } else { setBlog(json) } } getSingleBlog() }, [params.id, user]) function displayError() { if (!document.querySelector('#errorModal')) return document.querySelector('#errorModal').classList.remove('scale-0') setTimeout( () => document.querySelector('#errorModal').classList.add('scale-0'), 2000 ) } if (!user || error) { return <ErrorModal props = { { errorName: '401', errorMessage: error } } /> } if (blog == null && !error) { return ( < div className = "flex grow flex-row justify-center items-center" > < div className = "animate-spin rounded-full h-32 w-32 border-b-2 border-gray-900" > < /div> < / div > ) } async function handleDelete() { const response = await fetch(`/api/${params.id}`, { method: 'DELETE', headers: { accepts: 'application/json', Authorization: `Bearer ${user.token}`, }, }) if (!response.status === '200') { return ( < > < h1 > Opps!!!There was an error while fetching the resource < /h1> < / > ) } else { return navigate('/') } } function handleModal(e) { const modal = document.querySelector('#deleteModal') modal.classList.toggle('scale-0') document.addEventListener('mousedown', x) function x(e) { document.removeEventListener('mousedown', x) if (e.target.closest('div').id === 'trashCan') return if (!modal.contains(e.target)) { modal.classList.add('scale-0') } if (e.target.textContent === 'OK') { handleDelete() } if (e.target.textContent === 'Cancel') { modal.classList.add('scale-0') } } } return ( < > < div className = "flex flex-col grow" > < div className = "flex flex-row justify-between items-center" > < h1 className = "text-xl sm:text-3xl text-red-800 font-bold my-2 sm:my-3" > { blog.title } < /h1> < div className = "flex flex-row space-x-5" > < div id = "trashCan" > < FontAwesomeIcon icon = { faTrashCan } className = "text-xl sm:text-2xl text-red-500 cursor-pointer" onClick = { handleModal } /> < / div > < Link to = { `/${params.id}/edit` } > < div id = "editIcon" > < FontAwesomeIcon icon = { faEdit } className = "text-xl sm:text-2xl text-gray-800 cursor-pointer" / > < /div> < / Link > < /div> < / div > < div className = "flex flex-col grow gap-y-1 p-y-1 flex-wrap" > < div className = "flex flex-row text-xs" > < p className = "font-bold text-gray-600" > Author: < /p> < p className = "px-3" > { blog.author } < /p> < / div > < div className = "flex flex-col sm:flex-row text-xs" > < div className = "flex flex-row" > < p className = "font-bold text-gray-600" > Created at: < /p> < p className = "px-3 italic" > { new Date(blog.createdAt).toLocaleString() } < /p> < / div > < div className = "flex flex-row" > < p className = "font-bold text-gray-600" > Last Updated at: < /p> < p className = "px-3 italic" > { new Date(blog.updatedAt).toLocaleString() } < /p> < / div > < /div> < div className = "font-serif text-xl my-2 sm:my-1" > < p className = "break-all" > { blog.body } < /p> < / div > < /div> < / div > < Modal / > < /> ) } export default SingleBlog
解决方案
针对Firefox独有的Invalid Hook Call错误,结合你的代码,可从以下几个方向排查修复:
1. 替换DOM直接操作为React状态控制
你的代码中多次用document.querySelector直接修改DOM类名,这种做法违反React数据流原则,在Firefox中容易触发渲染时序异常,进而导致Hook调用错误:
- 重构错误弹窗逻辑:
// 新增状态控制弹窗显示 const [showErrorModal, setShowErrorModal] = useState(false) function displayError() { setShowErrorModal(true) setTimeout(() => setShowErrorModal(false), 2000) } // 渲染时传入状态 if (!user) { return <ErrorModal props={{ errorName: '401', errorMessage: 'You are not logged in.' }} show={showErrorModal} /> } if (error) { return <ErrorModal props={{ errorName: '401', errorMessage: error }} show={showErrorModal} /> } - 重构删除弹窗逻辑:
const [showDeleteModal, setShowDeleteModal] = useState(false) function handleModal() { setShowDeleteModal(prev => !prev) } function handleModalConfirm() { handleDelete() setShowDeleteModal(false) } function handleModalCancel() { setShowDeleteModal(false) } // 渲染Modal时传入状态和回调 return ( <> {/* ...其他代码... */} <Modal show={showDeleteModal} onConfirm={handleModalConfirm} onCancel={handleModalCancel} /> </> )
2. 修正代码逻辑错误
- 修复
handleDelete中的判断错误:!response.status === '200'是无效逻辑,改为数字类型的状态码判断:async function handleDelete() { const response = await fetch(`/api/${params.id}`, { method: 'DELETE', headers: { accepts: 'application/json', Authorization: `Bearer ${user.token}`, }, }) if (response.status !== 200) { setError('删除失败,请重试') displayError() return } navigate('/') } - 优化条件渲染顺序:拆分
!user和error的判断,避免因状态变化导致的Hook上下文异常:if (!user) { return <ErrorModal props={{ errorName: '401', errorMessage: 'You are not logged in.' }} /> } if (error) { return <ErrorModal props={{ errorName: '401', errorMessage: error }} /> } if (!blog) { return ( <div className="flex grow flex-row justify-center items-center"> <div className="animate-spin rounded-full h-32 w-32 border-b-2 border-gray-900"></div> </div> ) }
3. 确保React依赖版本一致
Firefox对模块加载的处理和Chrome存在差异,多版本React/React Router会导致Hook调用异常:
- 执行命令检查依赖树:
npm ls react react-dom react-router-dom - 若发现重复依赖,执行
npm dedupe合并,或删除node_modules和package-lock.json后重新安装。
4. 清除Firefox缓存
Firefox缓存的旧编译文件可能引发异常:
- 打开开发者工具,在「网络」面板勾选「禁用缓存」后刷新页面测试;
- 或按
Ctrl+Shift+Delete手动清除缓存。
5. 检查路由配置
确保SingleBlog组件在路由中是直接渲染的函数组件,没有嵌套在类组件内,也没有在循环、条件判断的嵌套函数中调用Hook。
内容的提问来源于stack exchange,提问作者Ali Mustafa
相关产品推荐
相关产品推荐

