为什么在初始组件渲染时更新状态?

问题描述 投票:0回答:3

我有一个应用程序可以搜索 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;
reactjs api material-ui
3个回答
0
投票

我希望仅当 selectedExercise 状态发生变化时才调用 postExercise() 函数

这不是 useEffect 的工作方式。每次更改依赖列表中的值时,它都会调用您对 mount 的影响。如果你想阻止初始调用,你必须首先考虑在挂载上触发的效果。


0
投票

在执行 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;

0
投票

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]);
       //...
       }
    
© www.soinside.com 2019 - 2024. All rights reserved.