Why is useState not updating state with the keydown handler?

tki*_*m90 5 reactjs react-hooks

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>
  );
}
Run Code Online (Sandbox Code Playgroud)

Ind*_*der 4

您可以通过以下任一方法解决此问题:

  1. 在useEffect的依赖数组中添加handleKeyStroke。handleKeyStroke()用钩子包装useCallback并添加startIduseCallback 的内部依赖数组。因此,每当startId发生变化时,useCallback将重新创建handleKeyStroke,而useEffect将获得新版本的handleKeyStroke

  2. 内部handleKeyStroke()通话setStartId(prev=>prev+1)(推荐)

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

Run Code Online (Sandbox Code Playgroud)

说明

在上面的代码中,您在window.addEventListeneruseEffect 内部添加了空依赖项数组。在内部handleKeyStroke(),您可以通过这种方式设置状态setStartId(startId + 1);

由于 useEffect 具有空的依赖数组,因此在handleKeyStroke()初始化时,它会使用挂载时可用的值进行初始化。它不访问更新的状态。

因此,例如,当您调用 时setStartId(startId + 1);handleKeyStroke()startId 的值为 0,并且它会加 1。但是下次调用时setStartId(startId + 1);startId里面的值仍然是0 handleKeyStroke(),因为它的值已经保存在useEffect中,因为依赖数组是空的。但是当我们使用回调语法时,它可以访问之前的状态。我们不需要 useEffect 的依赖数组中的任何内容。