Home > OS >  React Api Context
React Api Context

Time:04-17

I have a simple React app. On the 'home' page you can search movies from an API and add a movie to a list of favorited. I'm using Context to store which movies are on the list and pass it to the 'favorites' page where those items are rendered. It works well up to a point.

Once on the 'favorites' page, when I remove a movie, I would like the page to then show the updated elements. Instead, I have the elements I already had there plus the elements from the updated list.

So let's say my favorited movies were 'spiderman', 'batman' and 'dracula'. when I remove 'dracula' from the list, I suddenly have the cards of 'spiderman', 'batman, 'dracula', 'spiderman'(again) and 'batman'(again).

When I reload the 'favorites' page, it all works as intended. I just would like for it to be updated correctly upon removing the movie. Any advice?

Here is the code for the Home page, Favorite page, DataContext and the Card component

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;

CodePudding user response:

First of all, you should review the way you manage the favorite movies and that of what you want to do with them in your app. If you need to make a page to display the list of favorites, I would rather save in localstorage the necessary information for the list (cover, title, year, id, etc) without having to save the whole movie object. This will prevent you from having to call the API for each movie which will be very bad in terms of performance on your application. Also, it will prevent you from having to create another state on the Favorites page so it will solve your problem automatically (I think your problem came from the duplicate state you have).

CodePudding user response:

As mentioned before a lot of things cold be improved (you might want to check some react tutorial beginners related to best practices).

Anyway the main issue your app seems to be your callback after you get the response from the API (so this part):

  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]);
        });
    }

here you are calling setFavoriteMovies((prev) => [...prev, res.data]); but you actually never reset your favoriteMovies list.

So in your example favoriteMovies is ['spiderman', 'batman', 'dracula']. Then the useEffect callback executes with the array unchanged.

So you are making the requests just for 'spiderman' and 'batman' but your favoriteMovies array is ['spiderman', 'batman', 'dracula'] when the callback is entered (and this is why you end up appending those values to the existing ones and in the end your favoriteMovies == ['spiderman', 'batman', 'dracula', 'spiderman', 'batman'] in your example).

How to fix?

Quick fix would that might be obvious would be to reset the favoriteMovies at the beggining of useEffect. But that would be a extremly bad ideea since setting the state many times is terrible for performance reasons (each setState callback triggers a re-render) as well as for redability. So please don't consider this.

What I would suggest though would be to get all the values in the useEffect callback, put all the new favorite movies data in a variable and at the end of the function change the state in one call with the full updated list.

There are multiple ways to achieve this (async await is the best imo), but trying to alter the code as little as possible something like this should also work:

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)
  );
});

Please note I wasn't able to test this code since I don't have an exact reproduction of the error (so it might need some small adjustments). This code sample is rather a direction for your problem :)


Edit regarding the comment:

Despite the state issue, I would really recommend working on code cleanliness, efficiency and readability.

Examples (I put a few examples in code snippets to avoid a really long comment):

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

Small conclusion: ignoring the state part there aren't big issues in the code (and for a personal project or for learning might be decent). But if someone else has to work on in or you have to come back on this code after a month might become a nightmare. (especially since it seems like the codebase is not very small)

And people trying to understand your code might find it hard as well (including when you are posting it on stack overflow). I highlighted just a few, but it should point in the right direction, I hope.

  • Related