为什么 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>
);
}
我正在尝试创建一个简单的 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>
);
}