React状态与数组:重复渲染导致元素重复问题求解
问题描述
我正在开发基于React、Ethers和Solidity的全栈区块链NFT DApp,已实现路由、带钱包连接和铸造按钮的铸造页面,铸造区下方的个人藏品模块负责从合约获取资产与元数据信息。
以下是藏品组件代码:
import { useEffect, useState } from "react"; import Container from "react-bootstrap/Container"; import Row from "react-bootstrap/Row"; import Dino from "./Dino"; import { Contract, providers } from "ethers"; import { truncateAddress } from "./utils"; import { useWeb3React } from "@web3-react/core"; import { abi } from './abi'; export default function MyDinos() { const { library, account} = useWeb3React(); const [dinosUri, setDinosUri] = useState([]); const dinosTD = dinosUri.map((dino) => { return ( <Dino key={dino} uriMetadata={dino} /> ) }); useEffect(() => { if (!account) return; if (!library) return; const getDinosUri = async () => { try { const provider = await library.provider; const web3Provider = new providers.Web3Provider(provider); const signer = web3Provider.getSigner(); const contract = new Contract(process.env.REACT_APP_CONTRACT_ADDRESS, abi, signer); const idArray = await contract.tokensOfWallet(account); const idArrayFormatted = idArray.map(id => id.toNumber()).sort(); const uri = await contract.tokenURI(1); const uriInPieces = uri.split("/"); const tmpDinos = []; idArrayFormatted.forEach(id => { const uriFormatted = `https://ipfs.io/ipfs/${uriInPieces[2]}/${id}`; tmpDinos.push(uriFormatted); //setDinosUri(prevArray => [...prevArray, uriFormatted]) }); setDinosUri(tmpDinos); } catch (err) { console.log(err); } } getDinosUri(); return () => { setDinosUri([]); } }, [library, account]); return ( <> {dinosUri.length > 0 && <div className='late-wow appear'> <div className='svg-border-container bottom-border-light'></div> <Container fluid className='sfondo-light py-4'> <Container className='wow-container'> <h2 className='wow appear mb-4 text-center'>Account: {truncateAddress(account)}</h2> <h3 className='wow appear mb-4 text-center'>Dinos owned: {dinosUri.length} Dinos</h3> <h4 className='wow appear mb-4 text-center'>Races won: COMING SOON</h4> </Container> </Container> <div className='svg-border-container'></div> <Container fluid className='sfondo-dark py-4'> <Container> <h2 className='mb-4'>My {dinosUri.length} Dinos</h2> <Row className='my-5'> {[...dinosTD]} </Row> </Container> </Container> </div> } </> ) }
我使用临时变量tmpDinos存储URI数组能得到预期结果,但如果采用注释中的setDinosUri(prevArray => [...prevArray, uriFormatted])方式,首次渲染显示正常,切换路由后返回铸造页面时藏品会重复显示。我认为临时变量的方式并非React的正确处理方式,想请教如何让注释中的代码正常工作,是否是useEffect依赖项的问题?
问题原因与解决办法
核心问题
用函数式更新逐个push元素时,每次useEffect触发前的清理函数虽会将数组置空,但路由切换返回时,组件可能未完全卸载(比如React Router的缓存机制),或清理函数与重新渲染的时机存在间隙,导致新元素被追加到未完全清空的旧数组中,最终造成重复显示。
另外,tmpDinos的批量更新方式其实是合理的——一次性收集所有URI再更新状态,仅触发一次重渲染,比逐个调用setState更高效,完全符合React的状态更新逻辑,不存在“非正确处理方式”的问题。
若要使用注释中的函数式更新方式,需做以下修改:
- 在获取数据前强制清空数组:确保每次执行数据请求时,状态数组初始为空,避免基于旧数组追加元素。
- 优先批量更新状态:即使使用函数式更新,也应先收集所有URI,再一次性更新,而非循环中多次调用
setState。
修改后的getDinosUri函数示例:
const getDinosUri = async () => { try { const provider = await library.provider; const web3Provider = new providers.Web3Provider(provider); const signer = web3Provider.getSigner(); const contract = new Contract(process.env.REACT_APP_CONTRACT_ADDRESS, abi, signer); const idArray = await contract.tokensOfWallet(account); const idArrayFormatted = idArray.map(id => id.toNumber()).sort(); const uri = await contract.tokenURI(1); const uriInPieces = uri.split("/"); // 先清空数组,确保初始状态干净 setDinosUri([]); // 收集所有URI后批量更新 const newDinoUris = idArrayFormatted.map(id => `https://ipfs.io/ipfs/${uriInPieces[2]}/${id}` ); setDinosUri(prev => [...prev, ...newDinoUris]); // 若执意要在循环中逐个更新,需确保初始数组为空(已通过上方setDinosUri([])保证) // idArrayFormatted.forEach(id => { // const uriFormatted = `https://ipfs.io/ipfs/${uriInPieces[2]}/${id}`; // setDinosUri(prevArray => [...prevArray, uriFormatted]) // }); } catch (err) { console.log(err); } }
额外优化建议
- 用
useMemo缓存藏品列表,避免每次渲染都重新生成DOM元素:
import { useEffect, useState, useMemo } from "react"; // ... 其他代码不变 const dinosTD = useMemo(() => { return dinosUri.map((dino) => ( <Dino key={dino} uriMetadata={dino} /> )); }, [dinosUri]);
仅当dinosUri变化时才重新生成列表,提升组件性能。
- 当前useEffect的依赖项
[library, account]是正确的,仅当钱包账户或Web3实例变化时才重新获取藏品数据,无需调整。
内容的提问来源于stack exchange,提问作者Andrea Borin
相关产品推荐
相关产品推荐

