Search code examples
reactjsapimaterial-ui

Why is state updating on initial component render?


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;

Solution

  • 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.