反应 + Api+ 上下文

React + Api+ Context

我有一个简单的 React 应用程序。在 'home' 页面上,您可以从 API 中搜索电影并将电影添加到收藏列表。我正在使用 Context 来存储列表中的电影,并将其传递到呈现这些项目的 'favorites' 页面。它在一定程度上运作良好。

进入 'favorites' 页面后,当我删除电影时,我希望页面显示更新后的元素。相反,我有我已经有的元素加上更新列表中的元素。

假设我最喜欢的电影是 'spiderman'、'batman' 和 'dracula'。当我从列表中删除 'dracula' 时,我突然有了 'spiderman'、'batman、'dracula'、'spiderman'(again) 和 'batman'(again) .

当我重新加载 'favorites' 页面时,它一切正常。我只想在删除电影后正确更新它。 有什么建议吗?

这是主页、收藏夹页面、DataContext 和卡片组件的代码

import React, { createContext, useState, useEffect } from "react";

export const DataContext = createContext();

function DataContextProvider({ children }) {
  const [favorited, setFavorited] = useState([]);

  useEffect(() => {
    const savedMovies = localStorage.getItem("movies");
    if (savedMovies) {
      setFavorited(JSON.parse(savedMovies));
    }
  }, []);

  useEffect(() => {
    localStorage.setItem("movies", JSON.stringify(favorited));
  }, [favorited]);

  function addToFavorites(id) {
    setFavorited((prev) => [...prev, id]);
  }

  function removeFromFavorited(id) {
    const filtered = favorited.filter(el => el != id)
    setFavorited(filtered)
    
  }

 
  return (
    <DataContext.Provider value={{ favorited, addToFavorites, removeFromFavorited}}>
     
      {children}
    </DataContext.Provider>
  );
}

export default DataContextProvider;

function Favorites(props) {
  const ctx = useContext(DataContext);
  const [favoriteMovies, setFavoriteMovies] = useState([]);

  useEffect(() => {
    const key = process.env.REACT_APP_API_KEY;

    const savedMovies = ctx.favorited;

    for (let i = 0; i < savedMovies.length; i++) {
      axios
        .get(
          `https://api.themoviedb.org/3/movie/${savedMovies[i]}?api_key=${key}&language=en-US`
        )
        .then((res) => {
          setFavoriteMovies((prev) => [...prev, res.data]);
        });
    }
  }, [ctx.favorited]);

  return (
    <>
      <Navbar />
      <main>
        <div className="favorites-container">
          {favoriteMovies.map((movie) => {
            return <Card key={movie.id} movie={movie} />;
          })}
        </div>
      </main>
    </>
  );
}
function Home(props) {
  const [moviesData, setMoviesData] = useState([]);
  const [numOfMovies, setNumOfMovies] = useState(10);
  const [search, setSearch] = useState(getDayOfWeek());
  const [spinner, setSpinner] = useState(true);
  const [goodToBad, setGoodToBad] = useState(null);

  function getDayOfWeek() {
    const date = new Date().getDay();
    let day = "";
    switch (date) {
      case 0:
        day = "Sunday";
        break;
      case 1:
        day = "Monday";
        break;
      case 2:
        day = "Tuesday";
        break;
      case 3:
        day = "Wednesday";
        break;
      case 4:
        day = "Thursday";
        break;
      case 5:
        day = "Friday";
        break;
      case 6:
        day = "Saturday";
        break;
    }
    return day;
  }

  function bestToWorst() {
    setGoodToBad(true);
  }

  function worstToBest() {
    setGoodToBad(false);
  }

  useEffect(() => {
    const key = process.env.REACT_APP_API_KEY;
    axios
      .get(
        `https://api.themoviedb.org/3/search/movie?api_key=${key}&query=${search}`
      )
      .then((res) => {
        setMoviesData(res.data.results);
        //console.log(res.data.results)
        setSpinner(false);
        setGoodToBad(null);
      });
  }, [search]);


  return (
    <>
      <Navbar />
      <main>
        <form>
          <input
            type="text"
            placeholder="Search here"
            id="search-input"
            onChange={(e) => {
              setSearch(e.target.value);
              setNumOfMovies(10);
            }}
          />
          {/* <input type="submit" value="Search" /> */}
        </form>
        <div className="sorting-btns">
          <button id="top" onClick={bestToWorst}>
            <BsArrowUp />
          </button>
          <button id="bottom" onClick={worstToBest}>
            <BsArrowDown />
          </button>
        </div>

        {spinner ? <Loader /> : ""}

        <div>
          <div className="results">
            {!moviesData.length && <p>No results found</p>}
            {moviesData
              .slice(0, numOfMovies)
              .sort((a,b) => {
                if(goodToBad) {
                  return b.vote_average - a.vote_average
                } else if (goodToBad === false){
                  return a.vote_average - b.vote_average
                }
              })
              .map((movie) => (
                <Card key={movie.id}  movie={movie} />
              ))}
          </div>
        </div>
          {numOfMovies < moviesData.length && (
            <button className="more-btn" onClick={() => setNumOfMovies((prevNum) => prevNum + 6)}>
              Show More
            </button>
          )}
      </main>
    </>
  );
}

export default Home;
function Card(props) {
  const ctx = useContext(DataContext);

  return (
    <div
      className={
        ctx.favorited.includes(props.movie.id)
          ? "favorited movie-card"
          : "movie-card"
      }
    >
      <div className="movie-img">
        <img
          alt="movie poster"
          src={
            props.movie.poster_path
              ? `https://image.tmdb.org/t/p/w200/${props.movie.poster_path}`
              : "./generic-title.png"
          }
        />
      </div>
      <h2>{props.movie.original_title}</h2>
      <p>{props.movie.vote_average}/10</p>
      <button
        className="add-btn"
        onClick={() => ctx.addToFavorites(props.movie.id)}
      >
        Add
      </button>
      <button
        className="remove-btn"
        onClick={() => ctx.removeFromFavorited(props.movie.id)}
      >
        Remove
      </button>
    </div>
  );
}

export default Card;

首先,您应该回顾一下您管理最喜欢的电影的方式以及您希望在您的应用程序中对它们执行的操作。如果你需要制作一个页面来显示收藏夹列表,我宁愿在本地存储中保存列表的必要信息(封面、标题、年份、id 等),而不必保存整部电影 object。这将避免您必须为每部电影调用 API,这对您的应用程序的性能非常不利。此外,它会阻止您在收藏夹页面上创建另一个状态,因此它会自动解决您的问题(我认为您的问题来自您拥有的重复状态)。

如前所述,很多事情都没有得到改进(您可能想查看一些与最佳实践相关的 React 初学者教程)。

无论如何,您的应用程序的主要问题似乎是您收到 API 的响应后的回调(所以这部分):

  useEffect(() => {
    const key = process.env.REACT_APP_API_KEY;

    const savedMovies = ctx.favorited;

    for (let i = 0; i < savedMovies.length; i++) {
      axios
        .get(
          `https://api.themoviedb.org/3/movie/${savedMovies[i]}?api_key=${key}&language=en-US`
        )
        .then((res) => {
          setFavoriteMovies((prev) => [...prev, res.data]);
        });
    }

你在这里打电话 setFavoriteMovies((prev) => [...prev, res.data]); 但你实际上从未重置你的 favoriteMovies 列表。

所以在你的例子中 favoriteMovies['spiderman', 'batman', 'dracula']。然后 useEffect 回调在数组不变的情况下执行。

所以你只是为 'spiderman''batman' 发出请求,但是你的 favoriteMovies 数组在输入回调时是 ['spiderman', 'batman', 'dracula'] (这就是你结束的原因将这些值附加到现有值,最后你的 favoriteMovies == ['spiderman', 'batman', 'dracula', 'spiderman', 'batman'] 在你的例子中)。

如何解决?

快速修复可能很明显,就是在 useEffect 开始时重置 favoriteMovies。但这将是一个非常糟糕的想法,因为出于性能原因(每个 setState 回调都会触发 re-render)以及可重复性,多次设置状态是很糟糕的。所以请不要考虑这个。

不过我的建议是获取 useEffect 回调中的所有值,将所有新的最喜欢的电影数据放入一个变量中,并在函数结束时使用完整的更新列表在一次调用中更改状态.

有多种方法可以实现这一点(异步等待是最好的 imo),但尝试尽可能少地更改代码,例如 this 也应该有效:

useEffect(() => {
  const key = process.env.REACT_APP_API_KEY;

  const savedMovies = ctx.favorited;
  const favoriteMoviesPromises = [];

  for (let i = 0; i < savedMovies.length; i++) {
    favoriteMoviesPromises.push(
      axios
        .get(`https://api.themoviedb.org/3/movie/${savedMovies[i]}?api_key=${key}&language=en-US`)
        .then((res) => res.data)
    );
  }
  Promise.all(favoriteMoviesPromises).then((newFavoriteMovies) =>
    setFavoriteMovies(newFavoriteMovies)
  );
});

请注意,我无法测试此代码,因为我无法准确重现错误(因此可能需要进行一些小的调整)。此代码示例是解决您的问题的方向:)


编辑评论:

尽管存在状态问题,但我真的会建议在代码清洁度、效率和可读性方面进行工作。

示例(我在代码片段中放了几个示例以避免太长的注释):

1. `function getDayOfWeek`: 
First thing is that you don't need the `day` variable and all the break statement. 
You could just return the value on the spot (this would also stop the execution of the function). 

So instead of


  case 0:
    day = "Sunday";
    break;

you could have 

  case 0:
    return "Sunday";

Going even further you don't need a switch case at all. You could just create an array 
`const daysOfWeek = ['Sunday', 'Monday', 'Tuesday', 'Wednesday', 'Thursday', 'Friday', "Saturday"]` 
and just return daysOfWeek[date]. 

This would result in shorter and easier to read code.

2. Also this code is not really consistent. For example you have 

        onChange={(e) => {
          setSearch(e.target.value);
          setNumOfMovies(10);
        }}

but also `onClick={bestToWorst}` which is just `function bestToWorst() { setGoodToBad(true) }`. 
If this is not reusable you could just use `onClick={() => setGoodToBad(true)}`.

But even if you really want to keep the bestToWorst callback for whatever reason you could at least write and inline function 
(something like `const bestToWorst = () => setGoodToBad(true)` and use it the same). 

Anyway... From thoose 2 cases (bestToWorst and `Search here` onChange function), 
the second one make more sense to be defined outside.

3. The next part is really hard to read and maintain:

        {!moviesData.length && <p>No results found</p>}
        {moviesData
          .slice(0, numOfMovies)
          .sort((a,b) => {
            if(goodToBad) {
              return b.vote_average - a.vote_average
            } else if (goodToBad === false){
              return a.vote_average - b.vote_average
            }
          })
          .map((movie) => (
            <Card key={movie.id}  movie={movie} />
          ))}

Also this code doesn't belong in html. 
You should at least put the slice and sort parts in a function. 

Going further  `if(goodToBad)` and `else if (goodToBad === false)` are also not ideal. 

It would be best to use a separate function an example would be something like: 

const getFormattedMoviesData = () => {
  let formattedMoviesData = moviesData.slice(0, numOfMovies)
  if(!goodToBad && goodToBad !== false) return formattedMoviesData;
  const getMoviesDifference = (m1, m2) => m1.vote_average - m2.vote_average  
  return formattedMoviesData.sort((a,b) => goodToBad ? getMoviesDIfference(b,a) : getMoviesDIfference(a,b)
            

4. DataContext name doesn't suggest anything. 
I would propose something more meaningfull (especially for contexts) like `FavoriteMoviesContext`.
In this way people can get an ideea of what it represents when they come across it in the code. 

Additionally the context only contains `favorited, addToFavorites, removeFromFavorited`. 

So rather than using 
`const ctx = useContext(DataContext);` 
you could just use 
`const {favorited, addToFavorites, removeFromFavorited}  = useContext(DataContext);` 
and get rid of the ctx variable in your code

Regarding the api: 
If the search api returns all the movie data you need you can take it from there and use it in the favorites.

Alternatively it would be great to have an endpoint to return a list of multiple movies 
(so send an array of id's in the request and receive all of them). 
But this is only possible if the backend supports it.
 
But otherwise, since the api might contain hundreds of thousands or even millions, having them all stored on the frontside state would be an overkill 
(you can in some cases have this type lists stored in a redux state or a react context and filter them on frontend side.
But it won't be efficient for such a big volume of data).

小结论: 忽略状态部分,代码中没有大问题(对于个人项目或学习可能还不错)。但是,如果其他人必须在其中工作,或者您必须在一个月后返回此代码,则可能会成为一场噩梦。 (特别是因为看起来代码库不是很小)

尝试理解您的代码的人也可能会觉得很难理解(包括当您将代码发布到堆栈溢出时)。我只强调了一些,但我希望它应该指向正确的方向。