React useEffect无限循环问题:默认Props值与useRef解决方案的矛盾
问题场景
我写了一个Dropdown组件,核心代码如下:
import { useEffect, useState } from "react" type PropsType = { options?: any[] } export const DropDown = (props: PropsType) => { const { options = [] } = props; const [query, setQuery] = useState(''); const [filteredOptions, setFilteredOptions] = useState([]); useEffect(() => { setFilteredOptions(()=>[]); // 后续会在这里写过滤逻辑 }, [query, options]) return ( <div>DropDown</div> ) }
我给options设了默认空数组,但如果传入undefined:
<DropDown options={undefined} />
会直接触发无限循环。试过用useRef解决循环,但又出现新问题:父组件动态更新options(比如调用API后更新状态)时,Dropdown里的useEffect完全不触发。
附父组件调用示例:
import { useEffect, useState } from "react"; import { DropDown } from "./dropdown"; function App() { const [options,setOptions] = useState<string[]>([]); useEffect(()=>{ setOptions(()=>['a','b']) },[]) return ( <div> <DropDown options={options}/> </div> ); }
先搞懂为什么会无限循环
当传入options={undefined}时,组件里的const { options = [] } = props会每次渲染都创建一个新的空数组。而React的useEffect依赖项是按引用比较的——数组是引用类型,每次新创建的数组引用都不一样,所以useEffect会反复执行;useEffect里调用setFilteredOptions又会触发组件重新渲染,最终形成无限循环。
如果用useRef存options,虽然能避免依赖变化触发循环,但useRef的.current值变化不会触发组件渲染或useEffect执行,父组件更新options时Dropdown根本感知不到。
正确解决方案:兼顾稳定引用与动态更新
方案1:用useMemo缓存处理后的options
import { useEffect, useState, useMemo } from "react" type PropsType = { options?: any[] } export const DropDown = (props: PropsType) => { // 用useMemo缓存处理后的数组,保证引用稳定 const processedOptions = useMemo(() => { return props.options ?? []; }, [props.options]) const [query, setQuery] = useState(''); const [filteredOptions, setFilteredOptions] = useState([]); useEffect(() => { // 实际过滤逻辑 const filtered = processedOptions.filter(opt => opt.toString().includes(query) ); setFilteredOptions(filtered); }, [query, processedOptions]) return ( <div> <input value={query} onChange={(e) => setQuery(e.target.value)} placeholder="搜索选项" /> <ul> {filteredOptions.map((opt, idx) => ( <li key={idx}>{opt}</li> ))} </ul> </div> ) }
原理:useMemo会根据props.options的变化缓存结果。当props.options是undefined时,只会在第一次渲染创建空数组,后续复用同一个引用,不会触发useEffect;当父组件传递新的options(比如API返回的数组),useMemo会更新缓存,触发useEffect执行过滤逻辑。
方案2:用静态空数组作为默认值
import { useEffect, useState } from "react" // 定义一个静态空数组,引用永远稳定 const EMPTY_ARRAY: any[] = []; type PropsType = { options?: any[] } export const DropDown = (props: PropsType) => { // 用静态数组替代每次创建的新数组 const options = props.options ?? EMPTY_ARRAY; const [query, setQuery] = useState(''); const [filteredOptions, setFilteredOptions] = useState([]); useEffect(() => { const filtered = options.filter(opt => opt.toString().includes(query) ); setFilteredOptions(filtered); }, [query, options]) return ( <div> <input value={query} onChange={(e) => setQuery(e.target.value)} placeholder="搜索选项" /> <ul> {filteredOptions.map((opt, idx) => ( <li key={idx}>{opt}</li> ))} </ul> </div> ) }
原理:EMPTY_ARRAY是组件外部的静态变量,每次渲染时如果props.options是undefined,都会指向同一个引用,不会触发useEffect依赖变化;父组件用useState管理的options更新时,引用变化会正常触发useEffect。
底层原理总结
- React的
useEffect依赖项采用引用比较:对象/数组只要引用不同,就会被判定为变化,触发useEffect执行。 - 无限循环的根源是:传入
undefined时,每次渲染都会创建新的空数组,导致useEffect反复执行、组件反复渲染。 - 解决思路的核心是保证相同逻辑下的数组引用稳定,同时在实际数据变化时能正确触发依赖更新,
useMemo和静态数组都是实现这个目标的手段。
内容的提问来源于stack exchange,提问作者Omid Poorali

