Search code examples
reactjssortingfetch-api

Sort fetched data


I need to add sorting to fetched data (ascending/descending).

I get all the data from API endpoint. I map every object in that array to be displayed in separate component card. But once I choose to sort data from Descending name I get a quick change of components were they are sorted from Z to A but it just instantly converts back to initial fetched state (from A to Z).

Could you please tell me where the problem is? I don't know why but it feels like sorted array doesn't get saved in state "data" which I use to map all the cards.

import { useState } from 'react';
import { useEffect } from 'react';
import './styles/main.scss';
import Card from './components/Card/Card';
import { v4 as uuidv4 } from 'uuid';

function App() {
  const [data, setData] = useState([]);
  const [sortType, setSortType] = useState('default');

  useEffect(() => {
    fetchData();
    sortData();
  }, [sortType]);

  const fetchData = async () => {
    const response = await fetch(
      'https://restcountries.com/v2/all?fields=name,region,area'
    );
    const data = await response.json();
    setData(data);
  };

  function sortData() {
    let sortedData;
    if (sortType === 'descending') {
      sortedData = [...data].sort((a, b) => {
        return b.name.localeCompare(a.name);
      });
    } else if (sortType === 'ascending') {
      sortedData = [...data].sort((a, b) => {
        return a.name.localeCompare(b.name);
      });
    } else {
      return data;
    }
    setData(sortedData);
  }

  return (
    <div className='content'>
      <header className='content__header'>
        <h1>Header placeholder</h1>
      </header>
      <div className='wrapper'>
        <div className='wrapper__sort-buttons'>
          <select
            defaultValue='default'
            onChange={(e) => setSortType(e.target.value)}
          >
            <option disabled value='default'>
              Sort by
            </option>
            <option value='ascending'>Ascending</option>
            <option value='descending'>Descending</option>
          </select>
        </div>
        <ul className='wrapper__list'>
          {data.map((country) => {
            country.key = uuidv4();
            return (
              <li key={country.key}>
                <Card
                  name={country.name}
                  region={country.region}
                  area={country.area}
                />
              </li>
            );
          })}
        </ul>
      </div>
    </div>
  );
}

export default App;

This is what I get just for a quick moment:

Display after sorting descending order

And then it just goes back to initial state:

Initial state


Solution

  • This might be happening for the reason that set state function is asynchronous in nature and the order in which setData is being called is different than you expect. So, for the initial call with sortType 'default', you are not noticing any change as you are returning the data as it is. But once you change it to 'descending', setData() from sortData() is called earlier than that from fetchData() so as you have already data in your state, you see a change in data in UI for few moments, but then setData() from the function fetchData is called and replaces your data with the one you got from the API call which is unsorted or in ascending order.

    POSSIBLE SOLUTION

    DON'T set the state inside fetchData method, rather just set it once inside the sortData method, as you are needing it anyhow. So your code will look something like this:

      // we will call sortData inside fetchData so remove it from here
      useEffect(() => {
        fetchData();
      }, [sortType]);
    
      const fetchData = async () => {
        const response = await fetch(
          'https://restcountries.com/v2/all?fields=name,region,area'
        );
        const data = await response.json();
        // using API response data as an input to sortData function
        sortData(data)
      };
    
      // using data from parameter instead of state
      function sortData(data) {
        let sortedData;
        if (sortType === 'descending') {
          sortedData = [...data].sort((a, b) => {
            return b.name.localeCompare(a.name);
          });
        } else if (sortType === 'ascending') {
          sortedData = [...data].sort((a, b) => {
            return a.name.localeCompare(b.name);
          });
        } else {
          return data;
        }
        setData(sortedData);
      }
    

    IMPROVEMENT

    Your API call is not depending upon the SORTING ORDER, so you don't need to call the API again and again, just call it once, and then sort the data on the value changed from dropdown.

      // call the API on initial load only
      useEffect(() => {
        fetchData();
      }, []);
    
      // and on sortType change you can handle it like this:
      useEffect(() => {
        sortData(data);
      }, [sortType]);
    
      // and using this approach you can use the exact same code for both functions implementation that you posted in your question above.