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

在React中创建无循环依赖的setTimeout循环

React 状态循环问题解决方案

问题场景

想要实现一个包含playing和items两个状态的React组件,当playing设为true时,每秒向items中添加一个依赖当前items已有内容的新项。初步实现代码如下:

function App() {
  const [playing, setPlaying] = useState(false);
  const [items, setItems] = useState([]);
  const addItem = useCallback(
    function () {
      /* adding new item */
    },
    [items]
  );
  useEffect(
    function () {
      let timeout;
      playing &&
        (function loop() {
          timeout = window.setTimeout(loop, 1000);
          addItem();
        })();
      return function () {
        window.clearTimeout(timeout);
      };
    },
    [addItem, playing]
  );
  /* render the items */
}

注:本可以使用setInterval,但后续要添加修改循环间隔的状态,setTimeout更适合此需求。

当前实现的核心问题:effect依赖addItem,而addItem依赖items,当playing切换为true后,每次添加新项导致items变化,addItem会重新生成,进而触发effect重新执行,最终陷入无限循环。

解决方案

方案一:函数式更新 + useReducer(多状态场景首选)

如果addItem需要操作多个状态,且状态间存在依赖,用useReducer统一管理状态是最符合React思维的方案。它能把状态更新逻辑集中,避免addItem依赖具体状态值,从根源上解决循环问题。

示例代码:

function reducer(state, action) {
  switch (action.type) {
    case 'ADD_ITEM':
      return {
        ...state,
        items: [...state.items, `Item ${state.items.length + 1}`],
        // 可同时更新其他关联状态
        totalCount: state.totalCount + 1
      };
    case 'TOGGLE_PLAYING':
      return { ...state, playing: !state.playing };
    default:
      return state;
  }
}

function App() {
  const [state, dispatch] = useReducer(reducer, {
    playing: false,
    items: [],
    totalCount: 0
  });

  // addItem仅触发dispatch,无需依赖任何状态
  const addItem = useCallback(() => {
    dispatch({ type: 'ADD_ITEM' });
  }, []);

  useEffect(function () {
    let timeout;
    if (state.playing) {
      function loop() {
        addItem();
        timeout = window.setTimeout(loop, 1000);
      }
      loop();
    }
    return () => window.clearTimeout(timeout);
  }, [addItem, state.playing]);

  return (
    <div>
      <button onClick={() => dispatch({ type: 'TOGGLE_PLAYING' })}>
        {state.playing ? '暂停' : '开始'}
      </button>
      <ul>
        {state.items.map((item, idx) => <li key={idx}>{item}</li>)}
      </ul>
      <p>总数量:{state.totalCount}</p>
    </div>
  );
}

如果仅操作单个items状态,也可以直接用setItems的函数式更新,让addItem摆脱对items的依赖:

const addItem = useCallback(() => {
  setItems(prevItems => [...prevItems, `Item ${prevItems.length + 1}`]);
}, []);

方案二:用Ref跟踪最新状态(适合特殊场景)

你之前考虑的Ref方案并非不符合React思维,在需要直接读取最新状态且不想触发额外渲染的场景下完全可用。核心是用Ref作为状态的“镜像”,确保addItem能获取到最新值,同时不触发effect的依赖更新。

示例代码:

function App() {
  const [playing, setPlaying] = useState(false);
  const [items, setItems] = useState([]);
  const itemsRef = useRef(items);

  // 同步状态到Ref,确保Ref始终指向最新items
  useEffect(() => {
    itemsRef.current = items;
  }, [items]);

  // addItem读取Ref中的最新值,无需依赖items
  const addItem = useCallback(() => {
    const currentItems = itemsRef.current;
    setItems([...currentItems, `Item ${currentItems.length + 1}`]);
  }, []);

  useEffect(function () {
    let timeout;
    if (playing) {
      function loop() {
        addItem();
        timeout = window.setTimeout(loop, 1000);
      }
      loop();
    }
    return () => window.clearTimeout(timeout);
  }, [addItem, playing]);

  // 渲染部分...
}

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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.08.18 01:05:25