I have a question about API fetching order using .then and useEffect. I'll do my best to explain to you all to get some helpful advice.
Expected outcome of the code:
On first load(when accessing the site for the first time), I want the pokemon with the index of 1 to appear as default (thus, the reason why I set the default value of the state variable "pokemonIndex" to 1. But nothing shows up..
Every time I click the button which will toggle the toggle() function, it will render the getRandomIndex() function, update the state variable "pokemonIndex" which will run the useEffect.
The useEffect will fetch a new API with the updated ${pokemonIndex}
The rest of the code seems pretty self-explanatory.
These are my questions:
In step 1 from above, why does nothing show up even though I set the default state variable to 1?
Every time I click the button which runs the toggle(), it fetches a new API. However, the network tab looks like this: chrome network tab image
-> Is this normal behavior? Or is something messed up with my code. Thanks for reading such a long explanation. Thanks in advance!
export default function Testing() {
const [randomPokemon, setRandomPokemon] = React.useState({})
const [specifics, setSpecifics] = React.useState({})
const [pokemonIndex, setPokemonIndex] = React.useState(1)
const [prevPokemonIndex, setPrevPokemonIndex] = React.useState(0)
const [disableButton, setDisableButton] = React.useState(false)
function disableBtn() {
setDisableButton(prevState => !prevState);
setTimeout((setDisableButton), 1500)
}
function getRandomIndex(lowerBound, upperBound) {
return Math.floor(Math.random() * (upperBound - lowerBound + 1)) + lowerBound;
};
function toggle(e) {
e.preventDefault()
const randomIndex = getRandomIndex(1, 898);
setPokemonIndex(randomIndex);
disableBtn()
}
React.useEffect(() => {
fetch(`https://pokeapi.co/api/v2/pokemon/${pokemonIndex}/`)
.then(res => res.json())
.then(data => {
setRandomPokemon(data);
setSpecifics({
pokemonName: randomPokemon.name,
pokemonId: randomPokemon.id,
pokemonType: randomPokemon.types ? randomPokemon.types.map(type => type.type.name) : [],
pokemonAbility: randomPokemon.abilities ? randomPokemon.abilities.map(ability => ability.ability.name) : [],
pokemonImage: randomPokemon.sprites ? randomPokemon.sprites.other[`official-artwork`].front_default : []
});
})
}, [pokemonIndex])
return (
<div>
<form>
<button onClick={toggle} disabled={disableButton} type="button">Click me</button>
<img src={specifics.pokemonImage}></img>
<span>{specifics.pokemonName}</span>
<span>{specifics.pokemonId}</span>
<span>{specifics.pokemonType}</span>
<span>{specifics.pokemonAbility}</span>
</form>
</div>
)
The problem here is you're relying on randomPokemon
, the state atom, in the then
handler.
However, setState
in React is asynchronous, so randomPokemon
will not have changed by the time that setSpecifics
is executed just after setRandomPokemon
, so the specifics are always computed from data at the time of the effect's invocation (which, in effect (heh) is one "step" behind the just-loaded data).
You should instead use data
(which is the fresh data you've just loaded) in that function – or, since specifics
is strictly computed based on what randomPokemon
is, it doesn't need to be a state atom at all, but can just be derived using useMemo
, like so:
function getRandomIndex(lowerBound, upperBound) {
return Math.floor(Math.random() * (upperBound - lowerBound + 1)) + lowerBound;
}
export default function Testing() {
const [pokemon, setPokemon] = React.useState(null); // Nothing loaded yet
const [pokemonIndex, setPokemonIndex] = React.useState(1); // Which pokemon to load
const [disableButton, setDisableButton] = React.useState(false);
function disableBtn() {
setDisableButton(true);
setTimeout(() => setDisableButton(false), 1500);
}
function loadRandomPokemon(e) {
e.preventDefault();
const randomIndex = getRandomIndex(1, 898);
setPokemonIndex(randomIndex);
disableBtn();
}
React.useEffect(() => {
fetch(`https://pokeapi.co/api/v2/pokemon/${pokemonIndex}/`)
.then((res) => res.json())
.then(setPokemon);
});
const specifics = React.useMemo(() => {
if (!pokemon) {
return null; // Not loaded, no specifics yet
}
return {
pokemonName: pokemon.name,
pokemonId: pokemon.id,
pokemonType: pokemon.types ? pokemon.types.map((type) => type.type.name) : [],
pokemonAbility: pokemon.abilities ? pokemon.abilities.map((ability) => ability.ability.name) : [],
pokemonImage: pokemon.sprites ? pokemon.sprites.other[`official-artwork`].front_default : [],
};
}, [pokemon]);
// ...
}