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 ofsetStartId(startId 1)) - Used the div's onKeyDown prop instead of
window.addEventListener. That is how react expects you to bind event listeners to elements. onKeyDowndoesn't work on divs by default, because it doesn't expect any inputs (unlike<input />). But if you addtabIndex=0to it, it allows you to focus on the div, and therefore enables theonKeyDownlistener 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>
);
}
