React useEffect中扩展运算符处理Promise返回值为空无法触发更新
核心问题根因
排查下来两个问题是连锁反应的:
- 你用
[...result]拿不到任何元素,本质是getHiscores()返回的result根本不是实现了迭代协议的标准数组。Firebase的查询返回通常是两种结构:带.docs属性的QuerySnapshot对象、以文档ID为键的普通对象,这两种结构直接用扩展运算符展开,只会返回空数组——因为扩展运算符只对实现了Symbol.iterator的结构生效,普通对象、没有迭代器的类数组对象都会被展开成空内容。 - 直接写
setHiscores(result)不触发重渲染,不是因为React检测不到数组变更——本来存的就不是数组,加上你用来生成表格的useEffect完全没写依赖数组,默认每次组件渲染都会执行,没有明确的触发条件。你之前为了等数据出来去掉依赖数组反复跑的方案,本质是形成了“执行useEffect→setState→触发重渲染→再执行useEffect”的无限循环,才会触发最大更新深度警告。
修复步骤
- 把Firebase返回的非数组结构转成真正的标准数组,再用扩展运算符生成新引用
不要直接把Promise返回的根对象当数组用,在.then回调里先把数据拍平成数组:.then((result) => { // 兼容两种常见Firebase返回结构 let hiscoreList = [] if (result.docs) { // 兼容Firestore QuerySnapshot结构 hiscoreList = result.docs.map(doc => doc.data()) } else if (typeof result === 'object' && !Array.isArray(result)) { // 兼容Realtime Database返回的键值对对象 hiscoreList = Object.values(result) } else if (Array.isArray(result)) { hiscoreList = result } // 这时候hiscoreList是标准数组,扩展运算符完全有效,生成的新引用能被React正确检测到 setHiscores([...hiscoreList]) setLoading(false) }) - 给生成表格的useEffect补上明确的依赖列表,删掉无意义的兜底循环
之前无依赖的useEffect完全不需要反复执行,只要把依赖项设为hiscores和error,就会在数据真正更新的时候自动触发表格渲染:useEffect(() => { // 没有有效数据的时候清空表格 if (error || hiscores.length === 0) { setHiscoresTable(null) return } // 直接遍历hiscores生成表格行,不需要额外的临时变量 const tableRows = hiscores.map((item, index) => ( // 列表渲染加上key避免警告 <tr key={item.id ?? index}> <td>{index + 1}</td> <td>{item.name}</td> <td>{new Date(item.time * 1000).toISOString().substr(11, 8)}</td> </tr> )) setHiscoresTable(tableRows) }, [hiscores, error]) // 明确指定依赖,不会无限制触发执行 - 优化初始拉取的useEffect依赖
原来的拉取逻辑是空依赖数组[],如果用户切换关卡的时候,levelToShow的props变了,组件不会重新拉取对应关卡的高分。把props.levelToShow加到依赖列表里,同时在拉取前重置加载状态,避免旧数据闪现。
调整后的可运行组件代码
function HiscoresDisplay(props) { const [loading, setLoading] = useState(true); const [error, setError] = useState(false); const [hiscores, setHiscores] = useState([]); const [hiscoresTable, setHiscoresTable] = useState(null); useEffect(() => { // 状态重置,避免跨关卡的旧数据展示 setLoading(true) setError(false) getHiscores(props.levelToShow.getLevelName()) .then((result) => { let hiscoreList = [] if (result.docs) { hiscoreList = result.docs.map(doc => doc.data()) } else if (typeof result === 'object' && !Array.isArray(result)) { hiscoreList = Object.values(result) } else if (Array.isArray(result)) { hiscoreList = result } setHiscores([...hiscoreList]) setLoading(false) }) .catch(() => { setError(true) setLoading(false) }) }, [props.levelToShow]) useEffect(() => { if (error || hiscores.length === 0) { setHiscoresTable(null) return } const tableRows = hiscores.map((item, index) => ( <tr key={item.id ?? index}> <td>{index + 1}</td> <td>{item.name}</td> <td>{new Date(item.time * 1000).toISOString().substr(11, 8)}</td> </tr> )) setHiscoresTable(tableRows) }, [hiscores, error]) return ( <> <header> <h1 style={{textAlign: 'center', width: '100%'}}>Where's Wally?</h1> </header> <p className="under-header">{props.levelToShow.getLevelName()} Hiscores</p> <button onClick={() => props.setDisplay(0)} style={{display: 'block', margin: '0 auto'}} > ◀ Back to Main Menu </button> {loading && <h1 className="loading-levels">Loading hiscores...</h1>} {error && <h1 className="loading-levels">Failed to load hiscores</h1>} {!error && !loading && ( <div className="hiscores-container"> <table className="hiscores" style={{backgroundImage: `url(${props.levelToShow.getImgSrc()})`}} > <thead> <tr> <th>#</th> <th>Name</th> <th>Time</th> </tr> </thead> <tbody> {hiscoresTable} </tbody> </table> </div> )} </> ) }
内容的提问来源于stack exchange,提问作者9bp7
相关产品推荐
相关产品推荐

