React开发NFT市场时为不同NFT设置独立hashtag的问题排查
问题原因
全局只定义了一个tagList数组存储标签,渲染所有NFT卡片时都共用这同一个数组做遍历,没有建立NFT和自身标签的绑定关系,所以只要数组内容变化,所有卡片都会渲染相同的标签。
修复步骤
- 调整标签存储结构:将单一数组改为对象映射,以NFT的唯一标识(推荐用
合约地址+token_id组合,全局唯一不会重复)为键,对应值为该NFT自身的标签数组 - 拆分编辑态和展示态:弹窗内编辑标签时使用独立的临时状态存储,用户点击保存后再同步到全局标签映射,避免编辑中途取消时错误修改卡片展示内容
- 点击标签按钮打开弹窗时,自动加载当前操作NFT已有的标签到编辑态
- 渲染单张NFT卡片时,只读取当前NFT对应键下的标签数组做遍历,不再读取全局统一数组
- 提交、删除标签操作仅修改当前编辑NFT对应的标签集合,不影响其他NFT的内容
修正后核心代码
状态与逻辑部分
const [visible1, setVisibility1] = useState(false); const [nftToSend1, setNftToSend1] = useState(null); const [loading, setLoading] = useState(false); const [tagItem, setTagItem] = useState('') // 存储所有NFT的标签映射,key为NFT唯一标识,value为对应标签数组 const [nftTagMap, setNftTagMap] = useState({}) // 弹窗内编辑用的临时标签列表 const [editingTagList, setEditingTagList] = useState([]) // 点击标签按钮打开弹窗逻辑 const handleTagClick = (nft) => { setNftToSend1(nft) const nftKey = `${nft.token_address}_${nft.token_id}` // 加载当前NFT已有标签到编辑态 setEditingTagList(nftTagMap[nftKey] || []) setTagItem('') setVisibility1(true) } const onKeyPress = e => { if (e.target.value.trim().length !== 0 && e.key === 'Enter') { submitTagItem() } } const submitTagItem = () => { const updatedTagList = [...editingTagList, tagItem.trim()] setEditingTagList(updatedTagList) setTagItem('') } const deleteTagItem = e => { const deleteTarget = e.target.parentElement.firstChild.innerText const filteredTagList = editingTagList.filter(tag => tag !== deleteTarget) setEditingTagList(filteredTagList) } // 保存标签到全局状态 const handleSaveTag = () => { if (!nftToSend1) return const nftKey = `${nftToSend1.token_address}_${nftToSend1.token_id}` setNftTagMap(prev => ({ ...prev, [nftKey]: editingTagList })) // 如需持久化到Moralis服务端,在此处添加接口调用逻辑即可 setVisibility1(false) }
渲染部分
return ( <> ... <div style={styles.NFTs}> {NFTBalance && NFTBalance.map((nft) => { const currentNftKey = `${nft.token_address}_${nft.token_id}` const currentNftTags = nftTagMap[currentNftKey] || [] return ( <Card hoverable actions={[ <Tooltip title="HashTag"> <TagOutlined onClick={() => handleTagClick(nft)} /> </Tooltip>, ]} cover={ <Image preview={false} src={nft?.image || "error"} fallback="data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAMIAAADDCAYAAADQvc6UAAABRWlDQ1BJQ0MgUHJvZmlsZQAAKJFjYGASSSwoyGFhYGDIzSspCnJ3UoiIjFJgf8LAwSDCIMogwMCcmFxc4BgQ4ANUwgCjUcG3awyMIPqyLsis7PPOq3QdDFcvjV3jOD1boQVTPQrgSkktTgbSf4A4LbmgqISBgTEFyFYuLykAsTuAbJEioKOA7DkgdjqEvQHEToKwj4DVhAQ5A9k3gGyB5IxEoBmML4BsnSQk8XQkNtReEOBxcfXxUQg1Mjc0dyHgXNJBSWpFCYh2zi+oLMpMzyhRcASGUqqCZ16yno6CkYGRAQMDKMwhqj/fAIcloxgHQqxAjIHBEugw5sUIsSQpBobtQPdLciLEVJYzMPBHMDBsayhILEqEO4DxG0txmrERhM29nYGBddr//5/DGRjYNRkY/l7////39v///y4Dmn+LgeHANwDrkl1AuO+pmgAAADhlWElmTU0AKgAAAAgAAYdpAAQAAAABAAAAGgAAAAAAAqACAAQAAAABAAAAwqADAAQAAAABAAAAwwAAAAD9b/HnAAAHlklEQVR4Ae3dP3PTWBSGcbGzM6GCKqlIBRV0dHRJFarQ0eUT8LH4BnRU0NHR0UEFVdIlFRV7TzRksomPY8uykTk/zewQfKw/9znv4yvJynLv4uLiV2dBoDiBf4qP3/ARuCRABEFAoBEgghggQAQZQKAnYEaQBAQaASKIAQJEkAEEegJmBElAoBEgghggQAQZQKAnYEaQBAQaASKIAQJEkAEEegJmBElAoBEgghggQAQZQKAnYEaQBAQaASKIAQJEkAEEegJmBElAoBEgghggQAQZQKAnYEaQBAQaASKIAQJEkAEEegJmBElAoBEgghggQAQZQKAnYEaQBAQaASKIAQJEkAEEegJmBElAoBEgghggQAQZQKAnYEaQBAQaASKIAQJEkAEEegJmBElAoBEgghggQAQZQKAnYEaQBAQaASKIAQJEkAEEegJmBElAoBEgghggQAQZQKAnYEaQBAQaASKIAQJEkAEEegJmBElAoBEgghggQAQZQKAnYEaQBAQaASKIAQJEkAEEegJmBElAoBEgghgg0Aj8i0JO4OzsrPv69Wv+hi2qPHr0qNvf39+iI97soRIh4f3z58/u7du3SXX7Xt7Z2enevHmzfQe+oSN2apSAPj09TSrb+XKI/f379+08+A0cNRE2ANkupk+ACNPvkSPcAAEibACyXUyfABGm3yNHuAECRNgAZLuYPgEirKlHu7u7XdyytGwHAd8jjNyng4OD7vnz51dbPT8/7z58+NB9+/bt6jU/TI+AGWHEnrx48eJ/EsSmHzx40L18+fLyzxF3ZVMjEyDCiEDjMYZZS5wiPXnyZFbJaxMhQIQRGzHvWR7XCyOCXsOmiDAi1HmPMMQjDpbpEiDCiL358eNHurW/5SnWdIBbXiDCiA38/Pnzrce2YyZ4//59F3ePLNMl4PbpiL2J0L979+7yDtHDhw8vtzzvdGnEXdvUigSIsCLAWavHp/+qM0BcXMd/q25n1vF57TYBp0a3mUzilePj4+7k5KSLb6gt6ydAhPUzXnoPR0dHl79WGTNCfBnn1uvSCJdegQhLI1vvCk+fPu2ePXt2tZOYEV6/fn31dz+shwAR1sP1cqvLntbEN9MxA9xcYjsxS1jWR4AIa2Ibzx0tc44fYX/16lV6NDFLXH+YL32jwiACRBiEbf5KcXoTIsQSpzXx4N28Ja4BQoK7rgXiydbHjx/P25TaQAJEGAguWy0+2Q8PD6/Ki4R8EVl+bzBOnZY95fq9rj9zAkTI2SxdidBHqG9+skdw43borCXO/ZcJdraPWdv22uIEiLA4q7nvvCug8WTqzQveOH26fodo7g6uFe/a17W3+nFBAkRYENRdb1vkkz1CH9cPsVy/jrhr27PqMYvENYNlHAIesRiBYwRy0V+8iXP8+/fvX11Mr7L7ECueb/r48eMqm7FuI2BGWDEG8cm+7G3NEOfmdcTQw4h9/55lhm7DekRYKQPZF2ArbXTAyu4kDYB2YxUzwg0gi/41ztHnfQG26HbGel/crVrm7tNY+/1btkOEAZ2M05r4FB7r9GbAIdxaZYrHdOsgJ/wCEQY0J74TmOKnbxxT9n3FgGGWWsVdowHtjt9Nnvf7yQM2aZU/TIAIAxrw6dOnAWtZZcoEnBpNuTuObWMEiLAx1HY0ZQJEmHJ3HNvGCBBhY6jtaMoEiJB0Z29vL6ls58vxPcO8/zfrdo5qvKO+d3Fx8Wu8zf1dW4p/cPzLly/dtv9Ts/EbcvGAHhHyfBIhZ6NSiIBTo0LNNtScABFyNiqFCBChULMNNSdAhJyNSiECRCjUbEPNCRAhZ6NSiAARCjXbUHMCRMjZqBQiQIRCzTbUnAARcjYqhQgQoVCzDTUnQIScjUohAkQo1GxDzQkQIWejUogAEQo121BzAkTI2agUIkCEQs021JwAEXI2KoUIEKFQsw01J0CEnI1KIQJEKNRsQ80JECFno1KIABEKNdtQcwJEyNmoFCJAhELNNtScABFyNiqFCBChULMNNSdAhJyNSiECRCjUbEPNCRAhZ6NSiAARCjXbUHMCRMjZqBQiQIRCzTbUnAARcjYqhQgQoVCzDTUnQIScjUohAkQo1GxDzQkQIWejUogAEQo121BzAkTI2agUIkCEQs021JwAEXI2KoUIEKFQsw01J0CEnI1KIQJEKNRsQ80JECFno1KIABEKNdtQcwJEyNmoFCJAhELNNtScABFyNiqFCBChULMNNSdAhJyNSiECRCjUbEPNCRAhZ6NSiAARCjXbUHMCRMjZqBQiQIRCzTbUnAARcjYqhQgQoVCzDTUnQIScjUohAkQo1GxDzQkQIWejUogAEQo121BzAkTI2agUIkCEQs021JwAEXI2KoUIEKFQsw01J0CEnI1KIQJEKNRsQ80JECFno1KIABEKNdtQcwJEyNmoFCJAhELNNtScABFyNiqFCBChULMNNSdAhJyNSiEC/wGgKKC4YMA4TAAAAABJRU5ErkJggg==" alt="" style={{ height: "240px" }} /> } key={currentNftKey} > <Meta title={nft.name} description={nft.contract_type} /> {/* 仅渲染当前NFT自身的标签 */} {currentNftTags.map((value, idx) => ( <p key={idx}>#{value}</p> ))} </Card> ) })} </div> <Modal title={`HashTag ${nftToSend1?.name} #${nftToSend1?.token_id}`} visible={visible1} onCancel={() => setVisibility1(false)} footer={[ <Button onClick={handleSaveTag}> Save </Button>, ]} > <Spin spinning={loading}> <img src={`${nftToSend1?.image}`} alt="" style={{maxWidth: '100%', marginBottom: 16}} /> <Input autoFocus placeholder="Press Enter to enter a hashtag" tabIndex={2} onChange={(e) => setTagItem(e.target.value)} onKeyPress={onKeyPress} value={tagItem} /> {/* 渲染编辑态的临时标签列表 */} {editingTagList.map((tag, idx) => { return ( <div key={idx} style={{marginTop: 8}}> <span>#{tag}</span> <button onClick={deleteTagItem} style={{marginLeft: 8}}>X</button> </div> ) })} </Spin> </Modal> ...
额外说明
- 不要用数组遍历的index作为Card或列表项的key,NFT列表如果做排序、过滤操作,index作为key会导致状态错位,用合约地址+token_id的组合可以保证全局唯一
- 如果需要标签持久化,在
handleSaveTag方法中调用Moralis存储接口,将对应NFT的标签存入数据表即可,初始化加载NFT列表时顺便拉取对应标签填充nftTagMap
内容的提问来源于stack exchange,提问作者tnrms08
相关产品推荐
相关产品推荐

