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. onKeyDown
doesn't work on divs by default, because it doesn't expect any inputs (unlike<input />
). But if you addtabIndex=0
to it, it allows you to focus on the div, and therefore enables theonKeyDown
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>
);
}