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.logs 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 logging state 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 🙏