我有一个应用程序可以搜索 API 调用的结果并显示数据。用户可以选择将练习保存到数据库中以供日后查看。
为什么搜索API数据时状态会更新?我希望 postExercise() 函数仅在 selectedExercise 状态发生变化时通过 useEffect 调用(当在 ExerciseCard 组件中单击保存按钮时)。否则一切正常。
练习部分
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);
// fetches saved exercises
useEffect(() => {
fetch("http://localhost:3001/savedexercises")
.then((res) => {
return res.json();
})
.then((data) => {
setFetchedData(data);
});
console.log("saved exercises fetched");
}, []);
// maps over fetchedData and saves to a variable
const savedFetchedName = fetchedData.map((fetched) => fetched.name);
// checks if selected exercise exists in database when Add button is clicked
useEffect(() => {
const postExercise = () => {
if (savedFetchedName.includes(selectedExercise.name)) {
console.log("already added exercise");
} else {
console.log("adding new exercise");
fetch("http://localhost:3001/savedExercises", {
method: "POST",
body: JSON.stringify(selectedExercise),
headers: { "Content-Type": "application/json" },
});
}
};
postExercise();
}, [selectedExercise]);
// pagination
const lastIndex = currentPage * exercisesPerPage;
const firstIndex = lastIndex - exercisesPerPage;
const currentExercises = exercises.slice(firstIndex, lastIndex);
const handlePagination = (event, value) => {
setCurrentPage(value);
};
return (
<Box>
<Typography variant="h4" color="black" sx={{ p: 3 }}>
Search Results
</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}>
Back to Top
</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()}
>
Save
</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;
我希望仅当 selectedExercise 状态发生变化时才调用 postExercise() 函数
这不是 useEffect 的工作方式。每次更改依赖列表中的值时,它都会调用您对 mount 的影响。如果你想阻止初始调用,你必须首先考虑在挂载上触发的效果。
在执行 API 数据搜索时更新状态的原因是每次渲染组件时,都会执行负责获取保存的练习的效果钩子。这个 effect hook 不依赖于任何状态,所以它在每次渲染时运行,并更新 fetchedData 的状态。由于 savedFetchedName 派生自 fetchedData,它也会在每次渲染时更新。
然后,当ExerciseCard中点击“Save”按钮导致selectedExercise状态改变时,postExercise effect hook再次运行。由于 savedFetchedName 已在每次渲染时更新,因此每次都是一个新对象,并且会调用 postExercise 挂钩。这就是在执行 API 数据搜索时更新状态的原因。
要解决此问题,您可以将负责获取已保存练习的逻辑移至单独的组件,如 SavedExercisesProvider,它将包装您的 App 组件。这样,获取逻辑只会运行一次,并且不会在每次渲染时更新状态。
这是 SavedExercisesProvider 组件的示例:
import React, { useState, useEffect, createContext } from 'react';
export const SavedExercisesContext = createContext([]);
const SavedExercisesProvider = ({ children }) => {
const [fetchedData, setFetchedData] = useState([]);
useEffect(() => {
fetch("http://localhost:3001/savedexercises")
.then((res) => {
return res.json();
})
.then((data) => {
setFetchedData(data);
});
console.log("saved exercises fetched");
}, []);
const savedFetchedName = fetchedData.map((fetched) => fetched.name);
return (
<SavedExercisesContext.Provider value={savedFetchedName}>
{children}
</SavedExercisesContext.Provider>
);
};
export default SavedExercisesProvider;
用法:
import SavedExercisesProvider from './SavedExercisesProvider';
function MainApp() {
return (
<SavedExercisesProvider>
<App />
</SavedExercisesProvider>
);
}
export default MainApp;
savedFetchedName变量依赖于fetchedData状态,这意味着每当fetchedData状态改变时,savedFetchedName也会改变,这将导致组件重新渲染。
为了避免这个问题:
您可以将 savedFetchedName 作为状态。
更新 useEffect 中的 savedFetchedName(仅依赖于 fetchedData)。
const Exercises = ({ exercises }) => {
const [currentPage, setCurrentPage] = useState(1);
const exercisesPerPage = 24;
const [fetchedData, setFetchedData] = useState([]);
const [savedFetchedName, setSavedFetchedName] = useState([]);
//....
useEffect(() => {
setSavedFetchedName(fetchedData.map((fetched) => fetched.name));
}, [fetchedData]);
//...
}