JulienBlc
JulienBlc

Reputation: 134

React keep old state - new state not updated

In a React project, I have a state gameResults with a array of games, and I have a function to get the list of games based on a query :

useEffect(() => {
    const timeoutId = setTimeout(() => {
      if (gameQuery.length > 0) {
        axios.get(`/api/games/${gameQuery}`).then((response) => {
          const igdbGames: IGDBGame[] = response.data.games;
          const formatedGames = formatGames(igdbGames);
          setGameResults(formatedGames);
        });
      }
    }, 300);
    return () => clearTimeout(timeoutId);
  }, [gameQuery]);

For each game, I don't have the cover, so I get the cover for each game :

const loadGamesImages = async () => {
    for (let i = 0; i < gameResults.length; i++) {
      axios
        .get(`/api/cover/${gameResults[i].id}`)
        .then((response) => {
          const coverUrl: IGDBCover = response.data.covers[0];
          const newGame = {
            ...gameResults[i],
            cover: coverUrl.url.replace("//", "https://"),
          };
          const newGames = gameResults.filter(
            (game: Game) => game.id !== newGame.id
          );
          setGameResults([...newGames, newGame]);
        })
        .catch((error) => {
          console.log("error", error);
        });
      await sleep(300);
    }
    console.log("finish");
  };

  useEffect(() => {
    loadGamesImages();
  }, [gameResults.length]);

Here is my problem : when React update the state, the old state is not there anymore. I explain : for the first cover, it's ok the new state has the first game covered. But when he make a new state for the second game, as you can see i get the gameResults state, but in this one the first game has no cover anymore.

Here is the result : enter image description here

What have I done wrong ?

Upvotes: 0

Views: 48

Answers (1)

CertainPerformance
CertainPerformance

Reputation: 371138

Each one of your looped asynchronous calls closes over the initial binding of the stateful gameResults - and gameResults starts out empty. For example, with the first Promise that resolves, these line:

const newGames = gameResults.filter(
    (game: Game) => game.id !== newGame.id
);
setGameResults([...newGames, newGame]);

have the gameResults refer to the empty array, so setGameResults properly spreads the empty array plus the just-added newGame.

But then on further Promise resolutions, they also close over the initially-empty gameResults - all the async calls happened before the component re-rendered.

Use a callback instead, so that the async calls don't overwrite each other:

setGameResults((gameResults) => {
    const newGames = gameResults.filter(
        (game) => game.id !== newGame.id
    );
    return [...newGames, newGame];
});

(also note that there's no need to explicitly note the type of a parameter that TS can already infer automatically: (game: Game) can be just game)

Once this is working, I'd also suggest tweaking your code so that, when the effect hook runs again, only covers that have not been retrieved yet get requested again. This'll save you from unnecessarily making duplicate requests.

Upvotes: 1

Related Questions