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

React应用导航特定链接时Firefox报Invalid Hook Call错误求解

问题描述

在React应用中导航至某特定链接时,Firefox控制台会抛出Invalid Hook Call错误,但Chrome/Edge中访问同一链接无此问题。已检查报错组件,确认Hooks使用符合规范。

错误截图:
Firefox Invalid Hook Call错误截图

报错组件代码:

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 / >
    < /&gt;
  )
}

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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.08.01 05:55:16