3

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?

General Grievance
  • 4,555
  • 31
  • 31
  • 45
Bob Sacamano
  • 699
  • 15
  • 39
  • You could use a `useEffect`. You put the state A in the dependecy array so that everytime it is updated it triggers the `useEffect` – OneQ Jun 03 '23 at 13:59
  • But that means that the API call is triggered automatically when I change state A, what If I want it to trigger onClick like in the example. – Bob Sacamano Jun 03 '23 at 14:03
  • Then you just need to do like `loadCountryData1` – OneQ Jun 03 '23 at 14:06
  • I'm not sure that's right. is it guaranteed that selected in this case will have the correct value? I think I saw cases where it's outdated. – Bob Sacamano Jun 03 '23 at 15:03
  • Can you clarify what exactly you mean by "is safe"? `loadCountryData1` looks fine to me, so what is the concern with it? `loadCountryData1` is just flat out wrong as the state updater function should be a pure function, making a GET request is a side-effect and doesn't belong in the state updater function. It's unclear what exactly any issue is that you are asking for help with. Please [edit] to clarify the use case and issue, and what you are expecting to be able to accomplish. – Drew Reese Jun 10 '23 at 06:48

4 Answers4

2

You are correct in pointing out that there could be a problem with your loadCountryData1 function, because it could potentially use an outdated value of selected if selected is updated between the time the function is called and the time the API request is made.

In your loadCountryData2 function, you are calling setSelected, and then making the API call, but this does not actually solve the problem because setSelected is asynchronous and does not guarantee that the new value will be used in the API call.

Using the React useEffect hook would watch the selected state and then load the data whenever it changes. This will ensure that you always have the correct, up-to-date value when making the API call:

import { useState, useEffect } from "react";
import axios from "axios";
import "./styles.css";

export default function App() {
  const [selected, setSelected] = useState(null);
  const [countryData, setCountryData] = useState(null);

  const handleSelect = (e) => {
    setSelected(e.currentTarget.value);
  };

  useEffect(() => {
    if (selected !== null) {
      axios
        .get(`https://restcountries.com/v3.1/name/${selected}`)
        .then((response) => setCountryData(response.data));
    }
  }, [selected]);  // re-run this effect whenever `selected` changes

  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>
      </div>
      <div>
        {countryData !== null && (
          <pre>{JSON.stringify(countryData, null, 4)}</pre>
        )}
      </div>
    </div>
  );
}

The useEffect hook would be set up here to run any time selected changes. This means that whenever a new country is selected, the effect will run and fetch the new data. Note that the axios call is inside the if (selected !== null) block to ensure that no API call is made when selected is null.

But that means there is no longer a need for a separate loadCountryData function because the data loading is now automatically tied to the selected state. Which is not what you want.

What if I want it to trigger onClick like in the example?

In this case, you would need to create another piece of state to trigger the API call. This state would be updated within the onClick handler.

import { useState, useEffect } from "react";
import axios from "axios";
import "./styles.css";

export default function App() {
  const [selected, setSelected] = useState(null);
  const [countryData, setCountryData] = useState(null);
  const [shouldFetch, setShouldFetch] = useState(false);

  const handleSelect = (e) => {
    setSelected(e.currentTarget.value);
  };

  useEffect(() => {
    if (selected !== null && shouldFetch) {
      axios
        .get(`https://restcountries.com/v3.1/name/${selected}`)
        .then((response) => {
          setCountryData(response.data);
          setShouldFetch(false);  // reset the fetch trigger
        });
    }
  }, [selected, shouldFetch]);  // re-run this effect whenever `selected` or `shouldFetch` changes

  const loadCountryData = () => {
    setShouldFetch(true);
  };

  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={loadCountryData} disabled={selected === null}>
          Load
        </button>
      </div>
      <div>
        {countryData !== null && (
          <pre>{JSON.stringify(countryData, null, 4)}</pre>
        )}
      </div>
    </div>
  );
}

In this modified version of the code, I have added a shouldFetch piece of state that serves as a trigger for the API call. This trigger is set to true when the "Load" button is clicked, which causes the useEffect hook to run.
After the API call is made, shouldFetch is reset to false to prevent further API calls until the button is clicked again.

This approach gives you control over when the API call is made, while still ensuring that the most recent value of selected is used.

VonC
  • 1,262,500
  • 529
  • 4,410
  • 5,250
2

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:

g0rb
  • 2,234
  • 1
  • 9
  • 13
2

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 :)

Mearaj
  • 1,828
  • 11
  • 14
2

The loadCountryData1 is already a correct implementation and you are guaranteed to have an updated value of select if this function triggers via an event. I'm saying this is already updated because once the handleSelect is executed, it will re-render your component. Once your component rerender all the variables declared inside of it will be recomputed so the selected state inside loadCountryData1 will also be updated. Sample scenario.

// Initial render
const [selected, setSelected] = useState(null)
const loadCountryData1 = () => {
    axios
      .get(`https://restcountries.com/v3.1/name/${selected}`) // selected = null
      .then((response) => setCountryData(response.data));
  };
// handleSelect executed component rerender
// Initial render
const [selected, setSelected] = useState('usa')

// function recomputed
const loadCountryData1 = () => {
    axios
      .get(`https://restcountries.com/v3.1/name/${selected}`) // selected = usa
      .then((response) => setCountryData(response.data));
  };
Marveeen
  • 132
  • 5