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
- (please refer to the image) The blue lines represent the data fetched by every click of the button (the first one is the API fetched on the first load - question 1). However, it seems kinda wrong. For example, on the first click, it fetches pokemon with the Id 342 but also fetches the pokemon image with the Id of 1. There's a "mismatch". What I expected was on a button click, it will fetch (for example) a pokemon with the Id of 55 and also a image of the pokemon with the Id of 55 (like the blue lines I've drawn)
-> 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>
)
CodePudding user response:
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]);
// ...
}
CodePudding user response:
React.useEffect(() => {
fetch(`https://pokeapi.co/api/v2/pokemon/${pokemonIndex}/`)
.then(res => res.json())
.then(data => {
setSpecifics({
pokemonName: data.name,
pokemonId: data.id,
pokemonType: data.types ? data.types.map(type => type.type.name) : [],
pokemonAbility: data.abilities ? data.abilities.map(ability => ability.ability.name) : [],
pokemonImage: data.sprites ? data.sprites.other[`official-artwork`].front_default : []
});
})
}, [pokemonIndex])
Try this. That will work for you.