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

React应用setState问题:isClickable为何自动恢复为true?

问题原因与修复方案

核心问题

你遇到的isClickable自动恢复为true的原因有两个:

  1. 闭包陷阱:setTimeout里引用的是handleClick执行时的旧cardState快照,其中isClickable还是初始的true,1秒后执行setCardState时会把isClickable覆盖回true。
  2. 异步State更新冲突:handleClick中多次调用setCardState,每次都基于当前的cardState快照,后面只更新gameState的操作会覆盖之前设置isClickable: false的状态。

另外还有两个隐性问题:

  • 直接修改了state中的对象(card.isFlipped = !card.isFlipped),违反React不可变更新原则,可能导致组件不触发重新渲染。
  • 调用checkGame时用的是修改前的旧cardState.gameState,逻辑判断会滞后一步。

修复后的代码

const Card = ({ number, flipped, flipHandler, index }) => {
  return (
    <div
      className={flipped ? "card disabled" : "card"}
      data-index={index}
      onClick={flipHandler}
    >
      {flipped ? number : "FLIP ME"}
    </div>
  );
};

const Game = () => {
  const [cardState, setCardState] = React.useState({
    gameState,
    isClickable: true
  });

  function handleClick(e) {
    // 1. 不可变更新gameState,不直接修改原对象
    const newGameState = cardState.gameState.map((card, index) => {
      if (index === +e.target.dataset.index) {
        return { ...card, isFlipped: !card.isFlipped };
      }
      return card;
    });

    // 2. 先更新gameState,用函数式更新确保基于最新状态
    setCardState(prev => ({ ...prev, gameState: newGameState }));

    // 3. 用刚更新的newGameState做判断,而非旧状态
    if (checkGame(newGameState)) {
      // 4. 设置isClickable为false,函数式更新避免覆盖
      setCardState(prev => ({ ...prev, isClickable: false }));

      setTimeout(() => {
        // 5. 用函数式setState获取最新state,解决闭包陷阱
        setCardState(prev => {
          const flippedCards = prev.gameState.filter(card => card.isFlipped);
          let updatedGameState;
          if (flippedCards[0].number === flippedCards[1].number) {
            updatedGameState = prev.gameState.filter(
              card => card.number !== flippedCards[0].number
            );
          } else {
            updatedGameState = prev.gameState.map(card => ({
              ...card,
              isFlipped: false
            }));
          }
          // 6. 同步更新gameState和isClickable状态
          return { ...prev, gameState: updatedGameState, isClickable: true };
        });
      }, 1000);
    }
  }

  const cards = cardState.gameState.map((card, index) => (
    <Card
      key={index}
      number={card.number}
      flipHandler={cardState.isClickable ? handleClick : () => {}}
      flipped={card.isFlipped}
      index={index}
    />
  ));

  return <> {cards} </>;
};

关键修复点

  • 不可变更新:修改卡片状态时返回新对象,不直接修改原state中的数据。
  • 函数式setState:所有状态更新都使用prev => ...的形式,确保每次更新基于最新的state快照,避免异步更新冲突。
  • 闭包问题解决:setTimeout回调中通过函数式setCardState获取最新状态,不再依赖旧的cardState。
  • 正确判断时机:用刚生成的newGameState调用checkGame,保证判断逻辑基于最新的游戏状态。
  • 状态同步更新:在setTimeout回调中同时更新gameState和isClickable,确保两个状态同步变化。

内容的提问来源于stack exchange,提问作者Vuk Trifunović

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.08.22 21:18:18