Home > front end >  Why is useState not updating state with the keydown handler?
Why is useState not updating state with the keydown handler?

Time:05-02

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

*I added a hook called useTraceUpdate to track how state is causing re-renders.

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>
  );
}

CodePudding user response:

Inside handleKeyStroke() call setStartId(prev=>prev 1)

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;
    }
  };

CodePudding user response:

Got it working with @Inder's answer.

I made a few mistakes:

  • Updated the state setter to use a callback instead. Not sure why that works, I will do some research (eg. setStartId(id=>id 1) instead of setStartId(startId 1))
  • Used the div's onKeyDown prop instead of window.addEventListener. That is how react expects you to bind event listeners to elements.
  • onKeyDown doesn't work on divs by default, because it doesn't expect any inputs (unlike <input />). But if you add tabIndex=0 to it, it allows you to focus on the div, and therefore enables the onKeyDown listener on it.

Here is the final code:

const POKE_API_URL = "https://pokeapi.co/api/v2/pokemon";

const Carousel = ({ handleKeyDown, pokemonUrls, startId }) => (
  <div tabIndex="0" onKeyDown={handleKeyDown}>
    <img alt="pokemon" src={pokemonUrls[startId]} />
  </div>
);

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

  const handleKeyDown = (e) => {
    switch (e.keyCode) {
      // GO LEFT
      case 37:
        if (startId - 1 >= 0) {
          setStartId((prev) => prev - 1);
        }
        break;
      // GO RIGHT
      case 39:
        if (startId < pokemonUrls.length - 1) {
          setStartId((prev) => prev   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);
  }, []);

  return (
    <div className="App">
      <Carousel
        handleKeyDown={handleKeyDown}
        pokemonUrls={pokemonUrls}
        startId={startId}
      />
    </div>
  );
}
  • Related