Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Why is useState not updating state with the keydown handler?

I'm trying to create a simple React image slider, where the right/left arrow keys slides through the images.

Problem

When I press the right arrow ONCE, it works as expected. The id updates from 0 to 1, and re-renders the new image.

When I press the right arrow a SECOND time, I see (through console.log) that it registers the keystroke, but doesn't update the state via setstartId.

Why?

Also, I am printing new StartId: 0 in the component function itself. I see that when you first render the page, it prints it 4 times. Why? Is it: 1 for the initial load, 2 for the two useEffects, and a last one when the promises resolve?

The Code

Here is my sandbox: https://codesandbox.io/s/react-image-carousel-yv7njm?file=/src/App.js

export default function App(props) {
  const [pokemonUrls, setPokemonUrls] = useState([]);
  const [startId, setStartId] = useState(0);
  const [endId, setEndId] = useState(0);

  console.log(`new startId: ${startId}`)

  const handleKeyStroke = (e) => {
    switch (e.keyCode) {
      // GO LEFT
      case 37:
        break;
      // GO RIGHT
      case 39:
        console.log("RIGHT", startId);
        setStartId(startId + 1);
        break;
      default:
        break;
    }
  };

  useEffect(() => {
    async function fetchPokemonById(id) {
      const response = await fetch(`${POKE_API_URL}/${id}`);
      const result = await response.json();
      return result.sprites.front_shiny;
    }

    async function fetchNpokemon(n) {
      let pokemon = [];

      for (let i = 0; i < n; i++) {
        const pokemonUrl = await fetchPokemonById(i + 1);
        pokemon.push(pokemonUrl);
      }
      setPokemonUrls(pokemon);
    }

    fetchNpokemon(5);
  }, []);

  useEffect(() => {
    window.addEventListener("keydown", handleKeyStroke);

    return () => {
      window.removeEventListener("keydown", handleKeyStroke);
    };
  }, []);

  return (
    <div className="App">
      <Carousel pokemonUrls={pokemonUrls} startId={startId} />
      <div id="carousel" onKeyDown={handleKeyStroke}>
        <img alt="pokemon" src={pokemonUrls[startId]} />
      </div>
    </div>
  );
}
like image 336
tbd_ Avatar asked Oct 19 '25 01:10

tbd_


1 Answers

You can solve this by following either approach:

  1. Add handleKeyStroke in the dependency array of useEffect. Wrap handleKeyStroke() with useCallback hook and add startId inside dependency array of useCallback. So, whenever startId changes, useCallback will recreate handleKeyStroke and useEffect will get a new version of handleKeyStroke

  2. Inside handleKeyStroke() call setStartId(prev=>prev+1) (recommended)

Updated handleKeyStroke()

 const handleKeyStroke = (e) => {
    switch (e.keyCode) {
      // GO LEFT
      case 37:
        break;
      // GO RIGHT
      case 39:
        console.log("RIGHT", startId);
        setStartId(prev=>prev + 1);
        break;
      default:
        break;
    }
  };

Explanantion

In the code above, you're adding window.addEventListener inside useEffect with empty dependency array. And inside handleKeyStroke(), you are setting the state this way setStartId(startId + 1);.

Because of the useEffect with empty dependency array, when handleKeyStroke() is initialized, it is initialized with values which are available on mount. It doesn't access the updated state.

So, for example, when you call setStartId(startId + 1);, handleKeyStroke() has the value of startId=0 and it adds 1 to it. But next time when you call setStartId(startId + 1);, the startId value is still 0 inside handleKeyStroke() because it's value has been saved in useEffect because of empty dependency array. But when we use callback syntax it has access to previous state. And we won't need to anything inside useEffect's dependency array.

like image 93
Inder Avatar answered Oct 21 '25 21:10

Inder



Donate For Us

If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!