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

NextJS/React应用单元格点击后显示更新异常排查

问题:NextJS/React测试应用显示更新异常

运行应用后点击两个单元格,浏览器显示两个红色高亮单元格。预期行为是点击单元格时,先调用cleanFn(即refresh)将所有高亮单元格恢复为黄色,再将当前点击单元格设为红色,但实际结果与预期不符。


相关代码

Board.tsx

'use client'

import './Board.css';
import {useState} from 'react';


export default function Board({count}:{count:number}) {
  const [cellLights,setCellLights] = useState(Array.from({length:0},()=>
                                              Array.from({length:0},()=>false)));
  const refresh = () => {
    let tempLights:boolean[][] = cellLights;
    for (let idx=0; idx<tempLights.length; idx++) {
      for (let idix=0; idix<tempLights[idx].length; idix++) {
        tempLights[idx][idix] = false;
      }
    }
    setCellLights(tempLights)
  } /* End of refresh */


  let rows:number[] = [];
  while (rows.length<count) {rows.push(rows.length)}

  let tempLights:boolean[][] = [];
  rows.forEach(rowElt => {
    let rowLights:boolean[] = [];
    rows.forEach(colElt => {
      rowLights.push(false)
    });
    tempLights.push(rowLights)
  });

  if (tempLights.length!=cellLights.length) setCellLights(tempLights)

  return <div className="board">
    {
      rows.map(item => (
        <div key={item}>
          <Row count={count}
               flags={cellLights[item]}
               cleanFn={refresh}/>
        </div>
      ))
    }
  </div>
} /* End of Board */


function Row({count,flags,cleanFn}:{count:number,flags:boolean[],cleanFn:()=>void}) {
  let cells:number[] = [];
  while (cells.length<count) {cells.push(cells.length)}
    return <div className="row">
  {
    cells.map(item => (
      <div key={item}>
        <Cell flag={flags[item]}
              cleanFn={cleanFn}/>
      </div>
    ))
  }
</div>
} /* End of Row */


function Cell({flag,cleanFn}:{flag:boolean,cleanFn:()=>void}) {
    const [high, setHighFlag] = useState(flag);

  function cellClick() {
    cleanFn();
    setHighFlag(!high)
  }

return <>
    {(!high) && <div className="cell" onClick={cellClick}/>}
    {(high) && <div className="cell high" onClick={cellClick}/>}
  </>
} /* End of Cell */

Board.css

.cell {
  width: 3rem;
  height: 3rem;
  background-color: rgb(255, 255, 0);
  border-width: 2px;
  border-color: rgb(55, 55, 55);
  margin:0.1rem;
}

.high {
  background-color: rgb(255, 0, 0);
}

.row {
  display: flex;
  flex-direction: row;
}

.board {
  display: flex;
  flex-direction: column;
}

app/page.tsx

import Image from 'next/image'
import Board from './components/Board'

export default function Home() {
  return (
    <main className="flex min-h-screen flex-col items-center justify-between p-24">
    {/* // <main> */}
      X4C APP
      <Board count={3}/>
    </main>
  )
}

缺陷分析

  • 直接修改State数组导致React未检测到更新:在refresh函数中,let tempLights:boolean[][] = cellLights;是直接引用原state数组,修改tempLights的元素时实际是在修改原state。React依赖state的引用变化触发重渲染,由于引用未变,setCellLights(tempLights)不会触发组件更新。
  • Cell组件内部state未同步父组件props:Cell组件用useState(flag)初始化high,但useState仅在组件挂载时初始化一次。当父组件的flag变化时,Cell组件的high不会自动更新,无法同步全局重置状态。
  • 状态更新异步性导致逻辑顺序异常:React的状态更新是异步的,cleanFn()中的setCellLights和setHighFlag(!high)会被批量处理,导致当前点击单元格的高亮状态可能在全局重置前就被设置,或重置状态未及时同步到Cell组件。

修复方案

修改后的Board.tsx

'use client'

import './Board.css';
import {useState, useEffect} from 'react';

export default function Board({count}:{count:number}) {
  const [cellLights, setCellLights] = useState<boolean[][]>([]);

  // 初始化或更新网格大小
  useEffect(() => {
    const newLights = Array.from({length: count}, () => 
      Array.from({length: count}, () => false)
    );
    setCellLights(newLights);
  }, [count]);

  const refresh = () => {
    // 创建新数组,避免修改原state
    const newLights = cellLights.map(row => row.map(() => false));
    setCellLights(newLights);
  };

  // 更新指定单元格的高亮状态
  const toggleCell = (rowIdx: number, colIdx: number) => {
    refresh();
    setCellLights(prev => {
      const newLights = prev.map(row => [...row]);
      newLights[rowIdx][colIdx] = true;
      return newLights;
    });
  };

  return (
    <div className="board">
      {Array.from({length: count}, (_, rowIdx) => (
        <div key={rowIdx} className="row">
          {Array.from({length: count}, (_, colIdx) => (
            <Cell 
              key={colIdx} 
              isHigh={cellLights[rowIdx]?.[colIdx] || false}
              onClick={() => toggleCell(rowIdx, colIdx)}
            />
          ))}
        </div>
      ))}
    </div>
  );
}

function Cell({isHigh, onClick}: {isHigh: boolean, onClick: () => void}) {
  // 直接使用父组件传递的props,不需要内部state
  return (
    <div 
      className={`cell ${isHigh ? 'high' : ''}`}
      onClick={onClick}
    />
  );
}

关键修改点

  • 避免直接修改State:refresh函数通过map创建新的二维数组,确保state引用变化触发重渲染。
  • 移除Cell内部state:Cell组件直接使用父组件传递的isHigh props,保证状态完全同步。
  • 统一状态管理:将单元格切换逻辑移到Board组件中,先执行全局重置再设置当前单元格高亮,逻辑清晰且避免异步问题。
  • 用useEffect初始化网格:根据count动态初始化或更新网格,替代原有条件判断逻辑,更符合React生命周期。

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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.07.02 06:34:52