Reputation: 1248
I'm trying to create a simple React image slider, where the right/left arrow keys slides through the images.
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?
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>
);
}
Upvotes: 4
Views: 2354
Reputation: 2078
You can solve this by following either approach:
Add handleKeyStroke in the dependency array of useEffect. Wrap handleKeyStroke()
with useCallback
hook and add startId
inside dependency array of useCallback. So, whenever startId
changes, useCallback will recreate handleKeyStroke and useEffect will get a new version of handleKeyStroke
Inside handleKeyStroke()
call setStartId(prev=>prev+1)
(recommended)
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;
}
};
Explanantion
In the code above, you're adding window.addEventListener
inside useEffect with empty dependency array. And inside handleKeyStroke()
, you are setting the state this way setStartId(startId + 1);
.
Because of the useEffect with empty dependency array, when handleKeyStroke()
is initialized, it is initialized with values which are available on mount. It doesn't access the updated state.
So, for example, when you call setStartId(startId + 1);
, handleKeyStroke()
has the value of startId=0 and it adds 1 to it.
But next time when you call setStartId(startId + 1);
, the startId
value is still 0 inside handleKeyStroke()
because it's value has been saved in useEffect because of empty dependency array. But when we use callback syntax it has access to previous state. And we won't need to anything inside useEffect's dependency array.
Upvotes: 4
Reputation: 1248
Got it working with @Inder's answer.
I made a few mistakes:
setStartId(id=>id+1)
instead of setStartId(startId+1)
)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 add tabIndex=0
to it, it allows you to focus on the div, and therefore enables the onKeyDown
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>
);
}
Upvotes: 0