I have an app that searches the results of an API call and displays the data. The user has the option to save an exercise to the database to view later.
Why is the state being updated when a search is preformed for API data? I expect the postExercise() function to be invoked only when the selectedExercise state changes, via useEffect (when the Save button is clicked in the ExerciseCard component). Everything works fine otherwise.
Exercises component
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 component
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;
I expect the postExercise() function to be invoked only when the selectedExercise state changes
That's not how useEffect works. It will call your effect on mount AND every time values in the dependency list are changed. If you want to prevent the initial call you'll have to account for the effect being triggered on mount first.