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组件直接使用父组件传递的
isHighprops,保证状态完全同步。 - 统一状态管理:将单元格切换逻辑移到Board组件中,先执行全局重置再设置当前单元格高亮,逻辑清晰且避免异步问题。
- 用useEffect初始化网格:根据
count动态初始化或更新网格,替代原有条件判断逻辑,更符合React生命周期。
内容的提问来源于stack exchange,提问作者Michel
相关产品推荐
相关产品推荐

