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

