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

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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.10.03 03:18:03