I have a simple app that calls an API, returns the data (as an array of objects), sets a data state, and populates a few charts and graphs.
const loadData = async () => {
const url = 'https://my-api/api/my-api';
const response = await fetch(url);
const result = await response.json();
setData(result.data);
}
After setting the data, the data state is sent to every component and everything is populated. I created a filters pane that can filter the existing, populated data (for example, a gender filter that filters the data on the selected gender). What I did, and it’s obviously wrong, is created an onChange handler that filters the data to the selected gender then uses the setData
(sent as a prop; also the state variable, data
) to set the filtered data. When I clear the filter, the original, non-filtered data is replaced by the filtered data so the original data is lost.
const genderFilterHanlder = (e) => {
const filteredData = data.filter(x => x.gender === e.target.value);
setData(filteredData);
}
I tried creating an intermediary state the preserves the original data then upon clearing the filters, it sets the data (setData
) to the original. But this breaks when I have a filter that allows you to choose multiple values (like multiple languages; I can choose one language, clear it successfully, but if I choose two languages, then clear one, it breaks as the data is now the first chosen filter data).
How would I go about this?
Advertisement
Answer
I’d leave data
itself alone and have a separate filteredData
state member that you set using an effect:
const [filteredData, setFilteredData] = useState(data);
const [filter, setFilter] = useState("");
// ...
useEffect(() => {
const filteredData = filter ? data.filter(/*...apply filter...*/) : data;
setFilteredData(filteredData);
}, [filter, data]); // <=== Note our dependencies
// ...
// ...render `filteredData`, not `data`...
Then your change handler just updates filter
(setFilter(/*...the filter...*/)
).
That way, any time the filter changes, or any time data
changes, the data gets filtered and rendered.
Live Example:
const { useState, useEffect } = React;
const Child = ({data}) => {
const [filteredData, setFilteredData] = useState(data);
const [filter, setFilter] = useState("");
useEffect(() => {
if (!filter) {
setFilteredData(data);
return;
}
const lc = filter.toLocaleLowerCase();
const filteredData = filter
? data.filter(element => element.toLocaleLowerCase().includes(lc))
: data;
setFilteredData(filteredData);
}, [filter, data]); // <=== Note our dependencies
return <div>
<input type="text" value={filter} onChange={({currentTarget: {value}}) => setFilter(value)} />
<ul>
{filteredData.map(element => <li key={element}>{element}</li>)}
</ul>
</div>;
};
const greek = [
"alpha",
"beta",
"gamma",
"delta",
"epsilon",
"zeta",
"eta",
"theta",
"iota",
"kappa",
"lambda",
"mu",
"nu",
"xi",
"omicron",
"pi",
"rho",
"sigma",
"tau",
"upsilon",
"phi",
"chi",
"psi",
"omega",
];
const initialData = greek.slice(0, 4);
const Example = () => {
const [data, setData] = useState(initialData);
useEffect(() => {
const handle = setInterval(() => {
setData(data => {
if (data.length < greek.length) {
return [data, greek[data.length]];
}
clearInterval(handle);
return data;
});
}, 800);
return () => {
clearInterval(handle);
};
}, []);
return <Child data={data} />;
};
ReactDOM.render(<Example />, document.getElementById("root"));
<div id="root"></div>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/17.0.2/umd/react.development.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react-dom/17.0.2/umd/react-dom.development.js"></script>