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

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的状态更新逻辑,不存在“非正确处理方式”的问题。

若要使用注释中的函数式更新方式,需做以下修改:

  1. 在获取数据前强制清空数组:确保每次执行数据请求时,状态数组初始为空,避免基于旧数组追加元素。
  2. 优先批量更新状态:即使使用函数式更新,也应先收集所有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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.08.15 05:55:17