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:
And then it just goes back to initial state:
CodePudding user response:
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.
CodePudding user response:
It appears the way you're using useEffect is causing your component to refetch the data each time you change the sort type. This could be causing a race condition due to multiple places updating your data state at different times.
I would move the sorting logic into a useMemo and only fetch the data in useEffect on initial load:
import { useEffect, useMemo, useState } 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");
// Move sort logic here...
const sortedData = useMemo(() => {
let result = data;
if (sortType === "descending") {
result = [...data].sort((a, b) => {
return b.name.localeCompare(a.name);
});
} else if (sortType === "ascending") {
result = [...data].sort((a, b) => {
return a.name.localeCompare(b.name);
});
}
return result;
}, [data, sortType]);
// Only fetch data once on component mount...
useEffect(() => {
fetchData();
}, []);
const fetchData = async () => {
const response = await fetch(
"https://restcountries.com/v2/all?fields=name,region,area"
);
const data = await response.json();
setData(data);
};
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">
{/* Use sortedData here instead of data... */}
{sortedData.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;
Here's a basic example in a Codesandbox (I commented out your styles/card component): https://codesandbox.io/s/goofy-tdd-8lio9?file=/src/App.js


