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

组件初始渲染时state为何自动更新?React项目问题排查

问题描述

开发了一款调用API搜索并展示运动数据的应用,用户可将运动项目保存至数据库供后续查看。但执行API数据搜索时state会意外更新,期望仅在点击ExerciseCard组件的Save按钮、selectedExercise state变化时才触发postExercise()函数,其余功能均正常。

相关组件代码

Exercises组件

import { Grid, Typography, Container, Pagination, Button } from "@mui/material";
import { Box } from "@mui/system";
import React, { useState, useEffect, useContext } from "react";
import ExerciseCard from "./ExerciseCard";
import { SavedExercisesContext } from "../App.js";


const Exercises = ({ exercises }) => {
  const [currentPage, setCurrentPage] = useState(1);
  const exercisesPerPage = 24;
  const [fetchedData, setFetchedData] = useState([]);

  const [selectedExercise] = useContext(SavedExercisesContext);

 
 // 获取已保存的运动项目
  useEffect(() => {
    fetch("http://localhost:3001/savedexercises")
      .then((res) => {
        return res.json();
      })
      .then((data) => {
        setFetchedData(data);
      });
    console.log("已保存运动项目获取完成");
  }, []);


  // 提取已保存项目的名称列表
  const savedFetchedName = fetchedData.map((fetched) => fetched.name);


  // 点击Add按钮时检查选中项目是否已在数据库中
  useEffect(() => {
    const postExercise = () => {
      if (savedFetchedName.includes(selectedExercise.name)) {
        console.log("该运动项目已添加");
      } else {
        console.log("正在添加新运动项目");
        fetch("http://localhost:3001/savedExercises", {
          method: "POST",
          body: JSON.stringify(selectedExercise),
          headers: { "Content-Type": "application/json" },
        });
      }
    };
    postExercise();
  }, [selectedExercise]);

  
// 分页逻辑
  const lastIndex = currentPage * exercisesPerPage;
  const firstIndex = lastIndex - exercisesPerPage;
  const currentExercises = exercises.slice(firstIndex, lastIndex);

  const handlePagination = (event, value) => {
    setCurrentPage(value);
  };

  // 回到顶部函数(补充原代码缺失部分)
  const handleScroll = () => {
    window.scrollTo({ top: 0, behavior: "smooth" });
  };


  return (
    <Box>
      <Typography variant="h4" color="black" sx={{ p: 3 }}>
        搜索结果
      </Typography>
      <Container maxWidth="xl">
        <Grid container spacing={1}>
          {currentExercises?.map((exercise, id) => (
            <Grid item xs={12} md={4} key={exercise.id}>
              <ExerciseCard
                key={id}
                exercise={exercise}
                savedFetchedName={savedFetchedName}
              />
            </Grid>
          ))}
        </Grid>
      </Container>
      <Box sx={{ display: "flex", justifyContent: "center", p: 3 }}>
        <Pagination
          count={Math.ceil(exercises.length / exercisesPerPage)}
          color="error"
          onChange={handlePagination}
        />
      </Box>
      <Box sx={{ display: "flex", justifyContent: "center", p: 3 }}>
        <Button variant="contained" color="error" onClick={handleScroll}>
          返回顶部
        </Button>
      </Box>
    </Box>
  );
};

export default Exercises;

ExerciseCard组件

import {
  Button,
  Card,
  CardContent,
  CardMedia,
  Container,
  Typography,
} from "@mui/material";
import { Box } from "@mui/system";
import React, { useContext } from "react";

import { SavedExercisesContext } from "../App.js";


const ExerciseCard = ({ exercise }) => {

  const [selectedExercise, setSelectedExercise] = useContext(
    SavedExercisesContext
  );

  
const saveExerciseToDatabase = () => {
    setSelectedExercise({
      apiId: exercise.id,
      name: exercise.name,
      target: exercise.target,
      gifUrl: exercise.gifUrl,
    });

  };


  return (
    <Container maxWidth="xl">
      <Box>
        <Card>
          <CardMedia
            component="img"
            alt={exercise.name}
            image={exercise.gifUrl}
          />
          <CardContent sx={{ pb: 2, height: "75px" }}>
            <Typography variant="h5" sx={{ pb: 1 }}>
              {exercise.name.toUpperCase()}
            </Typography>
            <Typography variant="body2">
              {exercise.target.toUpperCase()}
            </Typography>
          </CardContent>
          <Box>
            <Box>
              <Button
                variant="contained"
                color="error"
                size="medium"
                sx={{ m: 2 }}
                onClick={() => saveExerciseToDatabase()}
              >
                保存
              </Button>
            </Box>
          </Box>
        </Card>
      </Box>
    </Container>
  );
};

export default ExerciseCard;

App.js

import "./App.css";
import { Box } from "@mui/system";
import React, { useState } from "react";
import Navbar from "./components/Navbar";
import { Route, Routes } from "react-router-dom";
import Home from "./pages/Home";
import SavedExercisesPage from "./pages/SavedExercisesPage";


export const SavedExercisesContext = React.createContext();


const App = () => {
  const [selectedExercise, setSelectedExercise] = useState([]);

  
return (
    <SavedExercisesContext.Provider
      value={[selectedExercise, setSelectedExercise]}
    >
      <Box>
        <Navbar />
        <Routes>
          <Route exact path="/" element={<Home />} />
          <Route path="/SavedExercisesPage" element={<SavedExercisesPage />} />
        </Routes>
      </Box>
    </SavedExercisesContext.Provider>
  );
};

export default App;

问题分析与解决方案

核心问题

  1. 初始值语义错误:App中selectedExercise初始值设为数组[],但实际保存的是单个运动对象。组件挂载时,依赖selectedExercise的useEffect会立即执行postExercise,此时selectedExercise.name为undefined,触发不必要的请求。
  2. 闭包数据过期:postExercise中使用的savedFetchedName未加入useEffect依赖数组,当fetchedData更新时,判断逻辑仍使用旧的已保存列表。
  3. 无过滤触发:只要selectedExercise变化就执行postExercise,未判断是否是用户主动触发的有效对象。

修复步骤

1. 修正初始值语义

修改App.js中selectedExercise的初始值为null,匹配单个运动对象的存储逻辑:

const [selectedExercise, setSelectedExercise] = useState(null);

2. 限制postExercise触发条件

修改Exercises组件中的useEffect,仅当selectedExercise为有效对象时执行,同时补充依赖数组:

useEffect(() => {
  // 仅当selectedExercise是包含name属性的有效对象时执行
  if (!selectedExercise || !selectedExercise.name) return;

  const postExercise = () => {
    if (savedFetchedName.includes(selectedExercise.name)) {
      console.log("该运动项目已添加");
    } else {
      console.log("正在添加新运动项目");
      fetch("http://localhost:3001/savedExercises", {
        method: "POST",
        body: JSON.stringify(selectedExercise),
        headers: { "Content-Type": "application/json" },
      })
      .catch(err => console.error("保存失败:", err)); // 新增错误捕获
    }
  };
  postExercise();
}, [selectedExercise, savedFetchedName]); // 加入savedFetchedName确保数据最新

3. 可选:重置状态避免重复触发

若希望用户点击保存后自动重置状态,可在Exercises组件中获取setSelectedExercise,并在postExercise执行后重置:

// 先在组件顶部获取setSelectedExercise
const [selectedExercise, setSelectedExercise] = useContext(SavedExercisesContext);

// 然后在postExercise中添加重置
const postExercise = () => {
  // ...原逻辑
  setSelectedExercise(null);
};

修复后效果

  • 仅用户点击Save按钮、selectedExercise变为有效运动对象时,才会触发数据库保存逻辑。
  • 解决了组件初始挂载时的意外请求问题。
  • 确保判断已保存状态时使用的是最新数据库数据。

内容的提问来源于stack exchange,提问作者postmetric

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.07.29 13:35:15