组件初始渲染时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;
问题分析与解决方案
核心问题
- 初始值语义错误:App中
selectedExercise初始值设为数组[],但实际保存的是单个运动对象。组件挂载时,依赖selectedExercise的useEffect会立即执行postExercise,此时selectedExercise.name为undefined,触发不必要的请求。 - 闭包数据过期:
postExercise中使用的savedFetchedName未加入useEffect依赖数组,当fetchedData更新时,判断逻辑仍使用旧的已保存列表。 - 无过滤触发:只要
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
相关产品推荐
相关产品推荐

