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

React Hooks条件调用报错排查:构建阶段出现调用顺序问题

问题描述

我给项目列表实现了键盘按键和按钮导航功能,本地运行完全正常,但构建阶段抛出以下错误:

Error: React Hook "useRef" is called conditionally. React Hooks must be called in the exact same order in every component render. Did you accidentally call a React Hook after an early return? react-hooks/rules-of-hooks
Error: React Hook "useState" is called conditionally. React Hooks must be called in the exact same order in every component render. Did you accidentally call a React Hook after an early return? react-hooks/rules-of-hooks
Error: React Hook "useEffect" is called conditionally. React Hooks must be called in the exact same order in every component render. Did you accidentally call a React Hook after an early return? react-hooks/rules-of-hooks

以下是我的代码:

'use client'
import { FixedSizeList } from 'react-window'
import useIsMobile from '../../../../../hooks/useIsMobile'
import PackagesCard from './packagesCard'
import AutoSizer from 'react-virtualized-auto-sizer'
import { useRef, useState, useEffect } from 'react'
import Image from 'next/image'

const VirtualPackagesCarousel = ({ data }) => {
  const { mobile } = useIsMobile()
  if (!data || data.length === 0) {
    return (
      <div className="h-[350px] flex text-neutrals-50 justify-center align-middle">
        There are no courses here yet!
      </div>
    )
  }
  const listRef = useRef(null)
  const [selection, setSelection] = useState(null)

  const handleKeyDown = (e) => {
    if (e.key === 'ArrowRight') {
      e.preventDefault()
      if (selection === null) {
        setSelection(0)
      } else {
        setSelection(Math.min(data.length - 1, selection + 1))
      }
    }

    if (e.key === 'ArrowLeft') {
      e.preventDefault()
      if (selection === null) {
        setSelection(0)
      } else {
        setSelection(Math.max(0, selection - 1))
      }
    }
  }

  const handleNext = () => {
    if (selection === null) {
      setSelection(0)
    } else {
      setSelection(Math.min(data.length - 1, selection + 1))
    }
  }

  const handleBack = () => {
    if (selection === null) {
      setSelection(0)
    } else {
      setSelection((prev) => Math.max(0, prev - 1))
    }
  }
  const handleBlur = () => {
    setSelection(null)
  }

  useEffect(() => {
    const list = listRef.current
    if (!list) return

    if (selection === null) return

    list.scrollToItem(selection)
  }, [selection])

  return (
    <div
      tabIndex={0}
      onKeyDown={handleKeyDown}
      onBlur={handleBlur}
      className="w-full flex flex-col gap-5 outline-none border-transparent focus:border-transparent focus:ring-0"
    >
      <div className="flex gap-3 justify-end visible mobile:invisible">
        <Image
          src="/images/arrowLeft.svg"
          alt="arrowBack"
          width="10"
          height="10"
          className="cursor-pointer invert"
          onClick={handleBack}
        />
        <Image
          src="/images/arrowRight.svg"
          alt="arrowNext"
          width="10"
          height="10"
          className="cursor-pointer invert "
          onClick={handleNext}
        />
      </div>

      <AutoSizer>
        {({ _, width }) => (
          <FixedSizeList
            height={400}
            itemCount={data.length}
            itemSize={mobile ? 300 : 600}
            width={width}
            layout="horizontal"
            ref={listRef}
            className="outline-none border-transparent focus:border-transparent focus:ring-0"
          >
            {({ index, style }) => (
              <div className="px-2.5 mobile:px-1" style={style}>
                <PackagesCard
                  data={data[index]}
                  key={data[index].id}
                  index={index}
                  setSelection={setSelection}
                  selection={selection}
                />
              </div>
            )}
          </FixedSizeList>
        )}
      </AutoSizer>
    </div>
  )
}

export default VirtualPackagesCarousel

我找不到问题所在,麻烦帮我定位错误位置,还有为什么本地运行没报错但构建阶段会出问题?


问题分析与修复

错误位置

问题出在条件判断之后调用React Hooks:你在组件开头先判断!data || data.length === 0,如果满足条件就提前return,而useRef、useState、useEffect这些Hooks都写在这个return之后。这违反了React Hooks的核心规则:Hooks必须在组件的每次渲染中按完全相同的顺序调用,不能放在条件语句、循环或者提前return之后。

本地无报错但构建报错的原因

本地开发环境的ESLint可能没有启用react-hooks/rules-of-hooks规则,或者规则配置较宽松(比如只警告不报错);而构建阶段通常会开启严格的代码检查,确保代码符合规范,所以会抛出错误。

修复方案

把所有Hooks调用移到组件的最顶部,放在任何条件判断之前。即使data为空,Hooks也会被调用,但不会影响逻辑,因为后续的条件判断依然会提前return。

修复后的代码:

'use client'
import { FixedSizeList } from 'react-window'
import useIsMobile from '../../../../../hooks/useIsMobile'
import PackagesCard from './packagesCard'
import AutoSizer from 'react-virtualized-auto-sizer'
import { useRef, useState, useEffect } from 'react'
import Image from 'next/image'

const VirtualPackagesCarousel = ({ data }) => {
  // 所有Hooks移到组件最顶部,在条件判断之前
  const { mobile } = useIsMobile()
  const listRef = useRef(null)
  const [selection, setSelection] = useState(null)

  if (!data || data.length === 0) {
    return (
      <div className="h-[350px] flex text-neutrals-50 justify-center align-middle">
        There are no courses here yet!
      </div>
    )
  }

  const handleKeyDown = (e) => {
    if (e.key === 'ArrowRight') {
      e.preventDefault()
      if (selection === null) {
        setSelection(0)
      } else {
        setSelection(Math.min(data.length - 1, selection + 1))
      }
    }

    if (e.key === 'ArrowLeft') {
      e.preventDefault()
      if (selection === null) {
        setSelection(0)
      } else {
        setSelection(Math.max(0, selection - 1))
      }
    }
  }

  const handleNext = () => {
    if (selection === null) {
      setSelection(0)
    } else {
      setSelection(Math.min(data.length - 1, selection + 1))
    }
  }

  const handleBack = () => {
    if (selection === null) {
      setSelection(0)
    } else {
      setSelection((prev) => Math.max(0, prev - 1))
    }
  }
  const handleBlur = () => {
    setSelection(null)
  }

  useEffect(() => {
    const list = listRef.current
    if (!list) return

    if (selection === null) return

    list.scrollToItem(selection)
  }, [selection])

  return (
    <div
      tabIndex={0}
      onKeyDown={handleKeyDown}
      onBlur={handleBlur}
      className="w-full flex flex-col gap-5 outline-none border-transparent focus:border-transparent focus:ring-0"
    >
      <div className="flex gap-3 justify-end visible mobile:invisible">
        <Image
          src="/images/arrowLeft.svg"
          alt="arrowBack"
          width="10"
          height="10"
          className="cursor-pointer invert"
          onClick={handleBack}
        />
        <Image
          src="/images/arrowRight.svg"
          alt="arrowNext"
          width="10"
          height="10"
          className="cursor-pointer invert "
          onClick={handleNext}
        />
      </div>

      <AutoSizer>
        {({ _, width }) => (
          <FixedSizeList
            height={400}
            itemCount={data.length}
            itemSize={mobile ? 300 : 600}
            width={width}
            layout="horizontal"
            ref={listRef}
            className="outline-none border-transparent focus:border-transparent focus:ring-0"
          >
            {({ index, style }) => (
              <div className="px-2.5 mobile:px-1" style={style}>
                <PackagesCard
                  data={data[index]}
                  key={data[index].id}
                  index={index}
                  setSelection={setSelection}
                  selection={selection}
                />
              </div>
            )}
          </FixedSizeList>
        )}
      </AutoSizer>
    </div>
  )
}

export default VirtualPackagesCarousel

这样修改后,Hooks的调用顺序在每次渲染中都保持一致,构建阶段的错误就会消失,同时不影响原有功能。


内容的提问来源于stack exchange,提问作者Valentino Fabris Valenti

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.07.28 14:18:16