I'm trying to understand what is the correct way to make an API call(say using, axios) that is based on some state. Let's say I have state A that I use in an API call to load state B. What is the correct way to do so if state A is something that is updated using useState. I know that simply using the value directly in the API call is not safe in the sense that I don't have a guarantee that I'll have the correct up to date value. Let's look at an example:
import { useState } from "react";
import axios from "axios";
import "./styles.css";
export default function App() {
const [selected, setSelected] = useState(null);
const handleSelect = (e) => {
setSelected(e.currentTarget.value);
};
const [countryData, setCountryData] = useState(null);
const loadCountryData1 = () => {
axios
.get(`https://restcountries.com/v3.1/name/${selected}`)
.then((response) => setCountryData(response.data));
};
const loadCountryData2 = () => {
setSelected((curr) => {
axios.get(`https://restcountries.com/v3.1/name/${selected}`);
return curr;
});
};
return (
<div className="App">
<div className="container">
<select value={selected} onChange={handleSelect}>
<option value={null}>Select country</option>
<option value="india">India</option>
<option value="usa">USA</option>
<option value="germany">Germany</option>
</select>
<button onClick={loadCountryData1} disabled={selected === null}>
Load1
</button>
<button onClick={loadCountryData2} disabled={selected === null}>
Load2
</button>
</div>
<div>
{countryData !== null && (
<pre>{JSON.stringify(countryData, null, 4)}</pre>
)}
</div>
</div>
);
}
loadCountryData1 is not safe. loadCountryData2 is safe but a hack that I don't think is how the makers intended. So what is the correct way?
Based on your example, you are asking if React guarantees that the state is consistent between different event handlers. Specifically, you want to know if the selected state set in the handleSelect event handler will be guaranteed to be the same state accessed in the loadCountryData1 event handler.
According to some other answers here:
For certain events, including click, React guarantees that the component will be re-rendered before another event can occur. (This isn't the case for other events like mousemove.)
The change event, similar to the click event, is batched and applies state before exiting its own browser event handler. This means state set in the change event handler will be guaranteed in the click event handler. source
You can find all React events and how they are handled here.
In regards to your example, the select state will be guaranteed in the loadCountryData1.
To answer the question, "what is the correct way to make an API call that is based on some state", I would say depends on the scenario. If the API call is caused by a particular interaction (i.e. a click event) keep it in the event handler. If your API call is dependent on a reactive value (i.e. a prop) and should happen regardless of an interaction, put it in an useEffect hook.
The new React docs have a good explanation on how to deal with this. Particularly on when to use useEffect:
The most simple way that I can think of is to create an enum like structure for keeping an eye on api state.
For example,
before making any async call, I would make a sync call that will update the state of api to let's say from idle to fetching.
Now the entire app is aware that my api is fetching, so based on this state,
depending on my needs, I would take different actions.
Here is the modified example of your code.
import { useState } from "react";
import axios from "axios";
import "./styles.css";
const AxiosState = Object.freeze({
Idle: 0,
Fetching: 1,
Completed: 2
});
export default function App() {
const [selected, setSelected] = useState({
country: "",
axiosState:AxiosState.Completed
});
const handleSelect = (e) => {
setSelected({
country: e.currentTarget.value,
axiosState: AxiosState.Completed,
});
};
const [countryData, setCountryData] = useState(null);
const loadCountryData1 = () => {
setSelected({...selected, axiosState: AxiosState.Fetching});
axios
.get(`https://restcountries.com/v3.1/name/${selected.country}`)
.then((response) => {
setSelected({...selected, axiosState:AxiosState.Completed});
setCountryData(response.data)
}).catch(e=> {
setSelected({...selected, axiosState:AxiosState.Completed});
setCountryData(null);
})
};
const loadCountryData2 = () => {
setSelected({...selected, axiosState: AxiosState.Fetching});
setSelected( async (curr) => {
try {
const res = await axios.get(`https://restcountries.com/v3.1/name/${selected.country}`);
setCountryData(res.data);
} catch (e) {
setCountryData(null);
}
return {...curr, axiosState: AxiosState.Completed};
});
};
return (
<div className="App">
<div className="container">
<select value={selected.country} onChange={handleSelect} disabled={selected.axiosState === AxiosState.Fetching}>
<option value={null}>Select country</option>
<option value="india">India</option>
<option value="usa">USA</option>
<option value="germany">Germany</option>
</select>
<button onClick={loadCountryData1} disabled={selected.country === null ||
selected.axiosState === AxiosState.Fetching}>
Load1
</button>
<button onClick={loadCountryData2} disabled={selected.country === null ||
selected.axiosState === AxiosState.Fetching}>
Load2
</button>
</div>
<div>
{countryData !== null && (
<pre>{JSON.stringify(countryData, null, 4)}</pre>
)}
</div>
</div>
);
}
Hope it will at least help, if not completely answer's your question. Thanks :)
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With