reactjsreact-hooksjsxcode-cleanup

What's the order of operations of a cleanup function inside a useEffect to clean stale effects?


I’m currently learning about the cleanup function in useEffect but I’m stuck on how it works in this particular example. I’m specifically stuck in the order of operations.

Looking at the last three logs (at the bottom) after clicking the button, how does it go from running the effect for id = 3 where ignore = false --> back to ignore = true for id = 2 --> then into id = 3 and pokemon is set in state? 


import * as React from "react"
import { fetchPokemon } from "./api"
import Carousel from "./Carousel"
import PokemonCard from "./PokemonCard"

export default function App () {
  const [id, setId] = React.useState(1)
  const [pokemon, setPokemon] = React.useState(null)
  const [loading, setLoading] = React.useState(true)
  const [error, setError] = React.useState(null)

  const handlePrevious = () => {
    if (id > 1) {
      setId(id - 1)
    }
  }

  const handleNext = () => setId(id + 1)

  React.useEffect(() => {
    let ignore = false

    console.log(`Effect ran | id: [${id}]`)

    const handleFetchPokemon = async () => {
      setLoading(true)
      setError(null)

      const { error, response } = await fetchPokemon(id)

      if (ignore) {
        console.log(`ignore is: [${ignore}] | id: [${id}]`)
        return
      } else if (error) {
        setError(error.message)
      } else {
        console.log(`Pokemon was set in state | id: [${id}]`)
        setPokemon(response)
      }

      setLoading(false)
    }

    handleFetchPokemon()

    return () => {
      console.log(`cleanup function ran | id: [${id}]`)
      ignore = true
    }
  }, [id])

  return (
    <Carousel onPrevious={handlePrevious} onNext={handleNext}>
      <PokemonCard
        loading={loading}
        error={error}
        data={pokemon}
      />
    </Carousel>
  )
}

LOGS:

On initial render:

Effect ran | id: [1]
Pokemon was set in state | id: [1]

After click the forward arrow twice fast (triggers side effect):

cleanup function ran | id: [1]
Effect ran | id: [2]
cleanup function ran | id: [2]
Effect ran | id: [3]
ignore is: [true] | id: [2]
Pokemon was set in state | id: [3]

I tried writing notes going line by line in the code. I expected to encounter a bit of an "aha!" moment but nothing! I'm still confused.


Solution

  • cleanup function ran | id: [1]
    Effect ran | id: [2]

    These two logs are from the following: due to the first button press, effect 2 is about to run. First it cleans up effect 1, and then it runs effect 2. As part of executing effect 2, it creates a local variable ignore (set to false), and when it reaches the await, execution pauses.

    cleanup function ran | id: [2]
    Effect ran | id: [3]

    Due to the second button press effect 3 is about to run, so it first cleans up effect 2. This sets effect 2's ignore variable to true. It then starts executing effect 3. As before, this creates a local ignore variable, then executes until the await.

    Note that while the variable has the same name "ignore", it's a different spot in memory and in a different closure. Effect 3's code can only interact with effect 3's ignore variable, and Effect 2 only interacts with Effect 2's ignore variable.

    ignore is: [true] | id: [2]

    Eventually the promise from effect 2 finishes resolving, so effect 2 resumes execution. It checks its ignore variable and sees that it has been updated to true. As a result, it does not try to set state.

    Pokemon was set in state | id: [3]

    Eventually the promise from effect 3 finishes resolving, so effect 3 resumes execution. It checks its ignore variable and sees that it's still false, so it sets the state.