为什么 useState 不使用 keydown 处理程序更新状态?

Why is useState not updating state with the keydown handler?

我正在尝试创建一个简单的 React 图像滑块,其中 right/left 箭头键在图像中滑动。

问题

当我按一次向右箭头时,它按预期工作。 id 从 0 更新为 1,并重新渲染新图像。

当我第二次按向右箭头时,我看到(通过 console.log)它注册了击键,但没有通过 setstartId.

更新状态

为什么?

此外,我在组件函数本身中打印 new StartId: 0。我看到当您第一次渲染页面时,它打印了 4 次。为什么?是:1 个用于初始加载,2 个用于两个 useEffects,最后一个用于承诺解决时?

代码

这是我的沙箱: 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>
  );
}

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

说明

在上面的代码中,您在 useEffect 中添加了 window.addEventListener,依赖数组为空。在 handleKeyStroke() 中,您以这种方式设置状态 setStartId(startId + 1);

由于useEffect,当handleKeyStroke()被初始化时,它被初始化为挂载时可用的值。它不访问更新后的状态。

因此,例如,当您调用 setStartId(startId + 1); 时,handleKeyStroke() 具有 startId=0 的值,并将其加 1。 但是下次调用 setStartId(startId + 1); 时, startId 的值在 handleKeyStroke 中仍然是 0。但是当我们使用回调语法时,它可以访问以前的状态。

得到它与@Inder 的回答一起工作。

我犯了一些错误:

  • 更新了状态 setter 以改为使用回调。不确定为什么会这样,我会做一些研究(例如 setStartId(id=>id+1) 而不是 setStartId(startId+1)
  • 使用 div 的 onKeyDown 道具而不是 window.addEventListener。这就是 React 希望您将事件侦听器绑定到元素的方式。
  • 默认情况下,
  • onKeyDown 不适用于 div,因为它不需要任何输入(与 <input /> 不同)。但是,如果您向其添加 tabIndex=0,它可以让您专注于 div,并因此在其上启用 onKeyDown 侦听器。

这是最终代码:

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