React Hooks useState结合localStorage时数组映射渲染报错求解
React localStorage持久化问题修复方案
问题根因
- 首次加载项目时localStorage不存在对应存储项,
getItem返回null直接赋值给数组类型的dataAll,后续调用数组方法直接抛出空指针错误 useEffect错误添加dataAll作为依赖项,每次dataAll更新都会重新读取localStorage覆盖state,触发无限循环逻辑- React的
setState是异步操作,表单提交后立即读取旧的dataAll写入localStorage,导致存储的永远是上一版数据,刷新后自然丢失 - 变量类型不匹配、逻辑位置错误:比如
price初始值是数组后续赋值为数字、ronin地址截取逻辑放在组件顶层,空地址时直接报错
修正后完整代码
import React, { useState, useEffect } from 'react'; import { FormControl, Form, Button, Container, Card, Row, Col, } from 'react-bootstrap'; import { nanoid } from 'nanoid'; import axios from 'axios'; import * as usertz from 'user-timezone'; import './card.css'; import slp from '../assets/SLP.png'; import ronin from '../assets/ronin.png'; const AxieAPI = () => { const [toggle, setToggle] = useState(false); // 修正price初始值类型为数字 const [price, setPrice] = useState(0); const [addFormData, setAddFormData] = useState({ myName: '', roninAddress: '', manager: '', scholar: '', }); // dataAll初始默认值 const defaultData = [ { id: '', myName: '', roninAddress: '', manager: '', scholar: '', data: { slp: { total: '', claimableTotal: '', lastClaimedItemAt: '', todaySoFar: '', yesterdaySLP: '', }, leaderboard: { winRate: '', winTotal: '', drawTotal: '', loseTotal: '', elo: '', rank: '', name: '', }, }, } ] const [dataAll, setDataAll] = useState(defaultData); const baseURL = 'https://api.coingecko.com/api/v3/coins/markets?vs_currency=usd&ids=smooth-love-potion&order=market_cap_desc&per_page=100&page=1&sparkline=false'; // 仅组件挂载时执行一次初始化逻辑 useEffect(() => { async function fetchData() { // 拉取SLP价格 await axios.get(baseURL).then((response) => { if(response.data && response.data.length > 0) { setPrice(response.data[0].current_price); } }); // 读取localStorage加兜底,不存在则用默认值 const localData = localStorage.getItem('localData'); if(localData) { setDataAll(JSON.parse(localData)); } } fetchData(); }, []); // 监听dataAll变化,自动同步到localStorage useEffect(() => { localStorage.setItem('localData', JSON.stringify(dataAll)); }, [dataAll]) const onChangeHandler = (e) => { try { const fieldName = e.target.getAttribute('name'); const fieldValue = e.target.value; setAddFormData(prev => ({...prev, [fieldName]: fieldValue})); } catch (error) { console.log(error); } }; const onSubmitHandler = async (e) => { e.preventDefault(); // 地址截取逻辑移到提交时执行,避免空地址报错 if(addFormData.roninAddress.length < 6) return; const userAddress = addFormData.roninAddress.slice(6); const options = { method: 'GET', url: `https://axie-infinity.p.rapidapi.com/get-update/0x${userAddress}`, params: { id: `0x${userAddress}` }, headers: { 'x-rapidapi-host': process.env.REACT_APP_HOST, 'x-rapidapi-key': process.env.REACT_APP_KEY, }, }; await axios .request(options) .then(function (response) { const newUser = { id: nanoid(), myName: addFormData.myName, roninAddress: addFormData.roninAddress, manager: addFormData.manager, scholar: addFormData.scholar, data: response.data, }; // 直接更新state,后续由监听useEffect自动同步到localStorage setDataAll(prev => [...prev, newUser]); }) .catch(function (error) { console.error(error); }); e.target.reset(); setAddFormData({ myName: '', roninAddress: '', manager: '', scholar: '', }); }; return ( <div> <Container className="p-4"> <Button onClick={() => setToggle(!toggle)} className="mb-5"> Add Scholar </Button> {toggle && ( <Form onSubmit={onSubmitHandler}> <Row className="mb-3"> <Form.Group as={Col} sm={12} lg={6}> <Form.Label>Scholar Name</Form.Label> <FormControl aria-label="Small" aria-describedby="inputGroup-sizing-sm" name="myName" value={addFormData.myName} onChange={onChangeHandler} required /> </Form.Group> <Form.Group as={Col} sm={12} lg={6}> <Form.Label>Ronin Address</Form.Label> <FormControl aria-label="Small" aria-describedby="inputGroup-sizing-sm" placeholder="ronin:12345-54321" name="roninAddress" value={addFormData.roninAddress} onChange={onChangeHandler} required /> </Form.Group> </Row> <Row> <Form.Group as={Col} sm={12} lg={6}> <Form.Label>Manager %</Form.Label> <FormControl aria-label="Small" aria-describedby="inputGroup-sizing-sm" name="manager" type="number" value={addFormData.manager} onChange={onChangeHandler} required /> </Form.Group> <Form.Group as={Col} sm={12} lg={6}> <Form.Label>Scholar %</Form.Label> <FormControl aria-label="Small" aria-describedby="inputGroup-sizing-sm" name="scholar" type="number" value={addFormData.scholar} onChange={onChangeHandler} required /> </Form.Group> </Row> <div className="d-grid gap-2 pt-3"> <Button type="submit" size="lg"> Add User </Button> </div> </Form> )} <Container> <Row> {/* 先判断dataAll是数组再渲染,过滤掉空id的占位数据 */} {Array.isArray(dataAll) && dataAll.filter(item => item.id).map((singleData) => { const myName = singleData.myName; const manager = singleData.manager; const scholar = singleData.scholar; const roninAddress = singleData.roninAddress; const totalSlp = singleData.data.slp.total || 0; const claimableTotal = singleData.data.slp.claimableTotal || 0; const lastClaimedItemAt = singleData.data.slp.lastClaimedItemAt || 0; const todaySoFar = singleData.data.slp.todaySoFar || 0; const yesterdaySLP = singleData.data.slp.yesterdaySLP || 0; const elo = singleData.data.leaderboard.elo || 0; const rank = singleData.data.leaderboard.rank || 0; const name = singleData.data.leaderboard.name || '-'; const unixTimeStamp = lastClaimedItemAt; const dateTimeFormat = 'MMMM Do, YYYY h:mm ss A'; const datetime = unixTimeStamp ? usertz.datetime(unixTimeStamp, dateTimeFormat) : '-'; const newUnixTimeStamp = lastClaimedItemAt + 1209600; const newdatetime = unixTimeStamp ? usertz.datetime(newUnixTimeStamp, dateTimeFormat) : '-'; const totalSlpPrice = price * totalSlp; const finaltotalSlpPrice = totalSlpPrice.toFixed(2); return ( <Card style={{ width: '22rem', backgroundColor: '#4f4f4f', padding: '20px', marginTop: '30px', marginRight: '10px', }} key={singleData.id} > <Card.Body> <Card.Title className="text-center">{name}</Card.Title> Scholar: <span className="font-weight-bold">{myName}</span> <p className="address"> {' '} <img src={ronin} alt="ronin" height="20px"></img> {' '} {roninAddress} </p> <Card.Subtitle className="mb-2 text-muted"></Card.Subtitle> <Row> <Card.Text as={Col} className="text-center bg-secondary rounded p-2 m-1 d-flex align-items-center justify-content-center" > <div> In-Game SLP <h2> <span style={{ color: '#3cf0cb' }}>{totalSlp}</span> <img height="35px" src={slp} alt="slp"></img> </h2> <p>USD {finaltotalSlpPrice}</p> </div> </Card.Text> <Card.Text as={Col} className="text-center bg-secondary rounded p-2 m-1 d-flex align-items-center justify-content-center" > <div> Claimable SLP <h2> <span style={{ color: '#3cf0cb' }}> {claimableTotal} <img height="35px" src={slp} alt="slp"></img> </span> </h2> </div> </Card.Text> </Row> Previous Claim: {datetime} <br /> Next Claim: {newdatetime} <Card.Text className="bg-dark p-1"> MMR ⚔️ : {elo} <br /> Rank 🏆 : {rank} <br /> SLP Today: {todaySoFar} <br /> SLP Yesterday: {yesterdaySLP} <br /> </Card.Text> <Card.Text className="p-2 bg-dark"> Manager: {manager} % <br /> Scholar: {scholar} % </Card.Text> </Card.Body> </Card> ); })} </Row> </Container> </Container> </div> ); }; export default AxieAPI;
关键改动说明
- 初始化读取localStorage增加兜底逻辑,无存储数据时直接使用默认数组,避免null赋值报错
- 拆分useEffect逻辑:第一个仅挂载时执行初始化拉取价格、读本地存储;第二个单独监听dataAll变化自动同步到localStorage,避免手动写入的异步问题
- 修正变量类型和逻辑位置:ronin地址截取移到提交函数内、price初始值改为数字、表单字段增加value绑定和required校验
- 渲染逻辑增加数组校验+空id数据过滤,避免无效数据渲染报错
- 所有读取字段增加默认值兜底,避免接口返回字段缺失时报错
内容的提问来源于stack exchange,提问作者longtech
相关产品推荐
相关产品推荐

