Are you logging the state immediately after updating it? Here's why that doesn't work
Did you ever stumble upon a situation where it seems no matter what you do, your state does not get correctly updated?
import React, { useState, useEffect } from 'react'
const PokemonList = () => {
const [pokemons, setPokemons] = useState([]);
useEffect(async () => {
const data = await fetch(`https://pokeapi.co/api/v2/pokemon?limit=10`).then(response => response.json());
setPokemons(data.results);
console.log('New pokemons list ', pokemons);
}, [])
return <ul>{pokemons.map(pokemon => <li key={pokemon.name}>{pokemon.name}</li>)}</ul>
}
The API returns correctly, you update your state by calling setState
, yet when you log the value, it always comes back empty?
Well, maybe everything works fine just that you're logging at the wrong time 🙈
Why does this happen?
When logging inside useEffect
, you're using the state
value that was captured in the closure at the beginning of the render. So that's why you're always seeing the empty array.
Your code is probably working just fine, and it's just the console.log
looking at an outdated value!
How to fix this
Moving the console.log
right before the return
statement will instead look at the latest state
value and log the updated list of pokemons 💥
Deep dive: Logging inside useEffect
Let's do a step by step walkthrough of what happens when the component is rendered. Notice I added a lot of extra console.log
statements:
import React, { useState, useEffect } from 'react'
const PokemonList = () => {
console.log('======= RENDER START =======')
const [pokemons, setPokemons] = useState([]);
useEffect(async () => {
const data = await fetch(`https://pokeapi.co/api/v2/pokemon?limit=10`).then(response => response.json());
console.log('API Returned:', data.results);
setPokemons(data.results);
console.log('State right after setPokemon: ', pokemons);
}, [])
console.log('State right before render: ', pokemons);
return <ul>{pokemons.map(pokemon => <li key={pokemon.name}>{pokemon.name}</li>)}</ul>
}
Pause for a second - can you guess what the order of the console.log
s is and what they will output?
...
...
Here's what the component will log:
======= RENDER START =======
State right before render: []
API Returned: (10) [{…}, {…}, {…}, {…}, {…}, {…}, {…}, {…}, {…}, {…}]
======= RENDER START =======
State right before render: (10) [{…}, {…}, {…}, {…}, {…}, {…}, {…}, {…}, {…}, {…}]
State right after setPokemon: []
So what's going on?
RENDER 1
state
is[]
- a new function is created to be passed into
useEffect
- --> function creates a closer over the current value of
state
:[]
- effect is called
- --> API is called to load the data
- --> data is successfully retrieved
- -->
setState
is called to update the pokemon list - --> --> RENDER 2 starts
- -->
console.log
statement is called loggingstate
value wrapped in closure -[]
-> the old one!
RENDER 2
state
is now the list of pokemons- a new function is created to be passed into
useEffect
- --> function creates a closure over the current value of
state
: the full list of pokemons - effect is on longer called, since it was defined with empty deps array`, which means it only gets run on the first render
So the console.log
that outputs the empty array is actually a stale value, captured in the first render!
To make sure you really understand the flow, try drawing a diagram of the flow above with pen and paper!
I hope you found this helpful! Let me know in the comments below if you still have any questions 🙏