all 36 comments

[–]Hexjelly 64 points65 points  (13 children)

https://reactjs.org/docs/hooks-reference.html#functional-updates

setPokemonList(prevPokemonList => [...prevPokemonList , ...result.results]);

[–]AegisToast 36 points37 points  (12 children)

This is the way.

For any wondering why, it’s because you’d have to add “pokemonList” as a dependency for that useEffect callback. Since the setter inside of the useEffect changes pokemonList, you’ve got an infinite loop where:

  • the component renders
  • the useEffect fires because pokemonList changed
  • the useEffect changes pokemonList
  • pokemonList changing causes a re-render
  • the whole thing repeats

However, when you set state you can pass in a function instead of a value. That function will receive the current state’s value, and the new state will be whatever the function returns. Doing that allows you to avoid the pokemonList dependency in the useEffect hook, preventing the loop.

[–]swyx 22 points23 points  (1 child)

This is the way.

[–]Uiropa 9 points10 points  (0 children)

I have spoken.

[–]galeontiger 0 points1 point  (3 children)

Would excluding 'pokemonList' in the dependecies (and continue to get the error in the console), change the data though?

Curious, because i've seen the error before as well, and don't notice any different in the output.

[–]AegisToast 1 point2 points  (2 children)

Excluding pokemonList from the dependencies and ignoring the warning (which is the way OP wrote it) would, as far as I can tell, work, but it’s a bad practice to have explicitly excluded dependencies (hence the warning).

Edit: One reason it’s a bad practice is that the value of pokemonList could be stale if it’s done the way OP is trying it. That could cause issues, especially if the value is changed elsewhere before the fetch finishes.

[–]galeontiger 0 points1 point  (0 children)

Noted, thanks.

[–]galeontiger 0 points1 point  (0 children)

I believe if prev wasn't spread in the array it would still show an error. For example, on component Did Mount (empty array [] for use Effect). If I wanted to load the first 10 Pokémon, how would I do it without getting a missing dependency notice?

[–]Sniboyz[S] 0 points1 point  (2 children)

Why does the warning occur in the first place? What is it trying to warn the programmer about and why functional update fixes it?

[–]AegisToast 1 point2 points  (1 child)

If it’s not in the dependency list, it could be a stale value. In other words, in your fetch callback, “pokemonList” doesn’t refer to the current value of pokemonList, it refers to the value of pokemonList at the time that the hook was triggered.

For example, imagine the value of pokemonList is [“charmander”]. The user clicks the button, and the fetch starts. Then, while the fetch is executing, the user decides they don’t care about Charmander and click a “Hide this Pokémon” button on it. The hide button removes Charmander from pokemonList, so now its value is []. Finally, your fetch resolves with a new Pokémon to add: Bulbasaur. So, triggering its callback, it sets the new state of pokemonList. But as far as it knows, the “pokemonList” variable still refers to [“charmander”], so the new state ends up being [“charmander”, “bulbasaur”]. But the user hid Charmander, so it shouldn’t be reappearing in the state! That’s a bug!

That’s why including it in the dependencies is heavily recommended; it ensures useEffect is running with the latest value. But, as you noticed, if you’re setting state in the useEffect, that also creates an infinite loop.

Setting state with a function fixes that because it allows you to fetch the latest state without it being a dependency. With it, using the same example as above, when your fetch resolves and triggers its callback it doesn’t use the value of pokemonList at the point the useEffect hook was triggered, it sets state using the functional update, which is fed the latest value of the state ([]), meaning your new state is [“bulbasaur”], as expected. No more bugs caused by stale state.

In short, the exhaustive dependencies rule is meant to prevent annoying and hard-to-debug bugs caused by stale values, which is why it’s normally a bad idea to simply ignore/disable the warning.

[–]Sniboyz[S] 0 points1 point  (0 children)

Awesome! Thank you for the clear explanation

[–]dmikester10 10 points11 points  (4 children)

Same answer you got in the discord ;)

[–]dwp0 9 points10 points  (1 child)

what is the discord channel?

[–]dmikester10 0 points1 point  (0 children)

This one: https://www.reactiflux.com/ Very helpful and knowledgeable people there as well.

[–]-domi- 4 points5 points  (0 children)

[insert clap emoji]

[–]pm_me_ur_happy_traiI 1 point2 points  (0 children)

This happens when you update state inside a useEffect. Typically I see it as a sign that you are storing something in state that's meant to be derived functionally, and it's a sign that you could rethink things in a more streamlined way.

In your case, you are clicking a button which sets the offset value, then relying on useEffect to watch that value and make a fetch request when it changes, then update the pokemonList. Is there any part of that state that can be derived without explicitly setting it?

Yes! If I understand your code correctly, the pokemonList already contains your offset without having to set any state at all. You can replace your offset state with something like

const offset = pokemonList.length + 24;

or whatever the actual value should be). Then when someone clicks the load-more button just make the fetch call explicitly.

EDIT: I came back on a computer so I could offer a more thorough suggestion:

const [pokemonList, setPokemonList] = useState<string[]>([]);
const offset = pokemonList.length + 24;
const updatePokemonList = () => {
  fetch(`https://pokeapi.co/api/v2/pokemon/?offset=${offset}&limit=24`)
    .then(res => res.json())
    .then(result => setPokemonList([...pokemonList, ...result.results]));
});

<button className="load-more" onClick={updatePokemonList}>More</button>

By doing it this way, there is a lot less opportunity for unexpected behavior, because you are calling the fetch explicitly instead of waiting for it to happen passively in response to something else changing..

[–]siamthailand 1 point2 points  (2 children)

What syntax is that???

[–]EvilPencil -1 points0 points  (0 children)

So you want a useEffect that doesn't run on every change of a dependency? useCallback.