2

I'm teaching myself React and one of my exercises is using axios to fetch a list of countries from an API

const fetchCountries = () => {
    axios.get("https://restcountries.eu/rest/v2/all").then(response => {
      setCountries(response.data);
    });
  };

  React.useEffect(fetchCountries, []);

Then as a user types into an input the list of countries filters down.

  const handleInputChange = event => {

    const filter = event.target.value; // current input value
    let matchingCountries = query !== ''
        ? countries.filter(country => country.name.toLowerCase().indexOf(query.toLowerCase()) !== -1)
        : countries;
    setQuery(filter);
    setMatches(matchingCountries)
    console.log('matches', matches)
    console.log('query', query)
  };

My goal is that when a single country is matched, a new API request is triggered (to fetch the weather, but the what isn't my problem, the timing is). When a single country is matched, I will then render some data about the country, then fetch and render the weather details for the single country's capital city.

One of the problems I'm having is that when I set the state, the value always seems to be one step behind. For example, in this Codepen when you enter FRA you should get "France". However, I have to enter "FRAN" to get the match. This doesn't happen when I don't use a state variable for the matches (just let matches). This becomes a problem because I need to run the next API call when the number of matches = 1, but the length of the matches state is always wrong.

So I would like to know 1. how to get the correct state of the matched countries. And 2. when I should run the second API call without getting into an infinite loop.

helgatheviking
  • 25,596
  • 11
  • 95
  • 152

3 Answers3

2

useEffect solution using separation of concern

1 function should do 1 thing

  • handleInputChange updates state
  • useEffect updates state

But they are not coupled.
Later you might have a new function called handleDropdownChange which updates state
It that case you don't need to modify useEffect

At the end of the day, we (developers) don't like to rewrite things

  const [countries, setCountries] = React.useState([]);
  const [query, setQuery] = React.useState("");
  const [matches, setMatches] = React.useState([]);

  
  React.useEffect(() => {
    let matchingCountries = query !== ''
        ? countries.filter(country => country.name.toLowerCase().indexOf(query.toLowerCase()) !== -1)
        : countries;
    
    setMatches(matchingCountries)
  }, [query]);                          // called whenever state.query updated
  
  const handleInputChange = event => {
    setQuery(event.target.value);       // update state
  };

  const fetchCountries = () => {
    axios.get("https://restcountries.eu/rest/v2/all").then(response => {
      setCountries(response.data);
    });
  };

  React.useEffect(fetchCountries, []);

And there is also solution (not recommended) by directly using event.target.value provided by @Joseph D.

Community
  • 1
  • 1
Medet Tleukabiluly
  • 11,662
  • 3
  • 34
  • 69
  • Thanks, Medet! What is the advantage of separating out `setMatches()` into another effect versus combining it with `handleInputChange()`? – helgatheviking Oct 23 '19 at 15:30
  • 1
    @helgatheviking its separation of concern, **1 function should do 1 thing**, `handleInputChange` updates state, `useEffect` updates state, but they are not coupled. Later you might have a new function called `handleDropdownChange` which updates state, it that case you don't need to modify `useEffect` – Medet Tleukabiluly Oct 23 '19 at 15:44
  • Makes sense. Thank you for the explanation! – helgatheviking Oct 23 '19 at 15:56
1

The only problem is you are using an old query value in handleInputChange().

Remember setting the state is asynchronous (i.e. doesn't take effect immediately)

Here's an updated version:

const handleInputChange = event => {

  const filter = event.target.value; // current input value
  let matchingCountries = filter ? <code here>
  // ...
  setQuery(filter);
};

UPDATE:

To call the weather api if there's a single country match is to have matches as dependency in useEffect().

useEffect(
  () => {
    async function queryWeatherApi() {
      // const data = await fetch(...)
      // setData(data)
    }

    if (matches.length === 1) {
      queryWeatherApi();
    }
  },
  [matches]
) 
Joseph D.
  • 11,804
  • 3
  • 34
  • 67
  • Yes! Ok, that gets the matching happening. I think I'm getting the countries once (now), but next I'm trying to get the Weather once I find a single country match. When would I fire a new `.get()`? Yesterday I was in an infinite loop and blitzed the weather api. whoops. – helgatheviking Oct 23 '19 at 03:13
  • 1
    @helgatheviking updated answer adding another effect to check for `matches.length` – Joseph D. Oct 23 '19 at 03:17
  • If i simplify my input this _does_ get the weather API call to fire (and not fire infinitely like before). That leaves me with some questions about other things, but I think this solves the question as posted. Thanks Joseph! – helgatheviking Oct 23 '19 at 15:32
  • Will the component re-render when `setWeather()` changes the weather state? – helgatheviking Oct 23 '19 at 15:35
  • 1
    @helgatheviking yes for it's a state. i.e. `const [weather, setWeather] = React.useState({})`. glad to be of help! – Joseph D. Oct 23 '19 at 16:46
1

1) The reason for your problem is in this line:

let matchingCountries = filter !== ''
        ? countries.filter(country => country.name.toLowerCase().indexOf(query.toLowerCase()) !== -1)
        : countries;

you use query instead of filter variable, your handler function should look like this:

const handleInputChange = event => {

    const filter = event.target.value; // current input value
    let matchingCountries = filter !== ''
        ? countries.filter(country => country.name.toLowerCase().indexOf(filter.toLowerCase()) !== -1)
        : countries;

    setQuery(filter);
    setMatches(matchingCountries)
  };

2) Where to run your next API call: For studying purpose I do not want to recommend you using some application state management lib like redux.Just calling it right after setFilter and setQuery. It will run as expected. Because calling an API is asynchronous too so it will be executed after setQuery and setFilter what does not happen with console.log, a synchronous function.

thelonglqd
  • 1,805
  • 16
  • 28