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>