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

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。

底层原理总结

  1. React的useEffect依赖项采用引用比较:对象/数组只要引用不同,就会被判定为变化,触发useEffect执行。
  2. 无限循环的根源是:传入undefined时,每次渲染都会创建新的空数组,导致useEffect反复执行、组件反复渲染。
  3. 解决思路的核心是保证相同逻辑下的数组引用稳定,同时在实际数据变化时能正确触发依赖更新,useMemo和静态数组都是实现这个目标的手段。

内容的提问来源于stack exchange,提问作者Omid Poorali

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.08.16 07:55:27