'Store copy of data state before applying and after clearing filter

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?



Solution 1:[1]

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>

Solution 2:[2]

You need to have two states in your case. You can follow below example.

  const data = [
    { id: 1, name: "Mahela" },
    { id: 2, name: "Sangakkara" },
    { id: 3, name: "Dilshan" },
    { id: 4, name: "Malinga" },
    { id: 5, name: "Rangana" },
    { id: 6, name: "Kulasekara" }
  ];

  const [inputValue, setInputValue] = useState("");
  const [filtered, setFiltered] = useState([]);

  const inputChangeHandler = (e) => {
    setInputValue(e.target.value);
    setFiltered(data.filter((item) => item.name === e.target.value));
  };

  return (
    <div className="App">
      <input type="text" value={inputValue} onChange={inputChangeHandler} />
      {filtered.length > 0 && filtered.map((item) => <p>{item.name}</p>)}
      {filtered.length === 0 && data.map((item) => <p>{item.name}</p>)}
    </div>
  );

Here I have used constant for data array. You can use your data state. Don't replace data state with new data instead use second state for the filtered data.

Solution 3:[3]

dont change your state after filtering . hold your original data and get a copy of it in a variable and do your stuff on it like filtering. then use that variable for rendering the filtered data and if you want to clear the input or anything you can use your original state that you have been storing.

const FilterData = () => {
  const [data, setData] = useState([]);
  const [select, setSelect] = useState();

  const loadData = async () => {
    const url = "https://my-api/api/my-api";
    const response = await fetch(url);
    const result = await response.json();

    setData(result.data);
  };

  const filteredData = data.filter((item) => {
    if (!select) return null;
    return item.gender !== select;
  });

  return (
    <div>
      <select>...</select>
      {filteredData.map(...)}
    </div>
  );
};

Solution 4:[4]

In this value must the male/female (or any) and filter must be the type (key) now on change on the filter or value filterData gives you the result

use filterData to render elements and data state will remain constant always.

const[data, setDate] = useState([]);
const[filterData, setFilterData] = useState([]);
const[filter, setFilter] = useState('gender');
const[value, setValue] = useState('male');

const fetchData = () => {
   const url = '';
   const data = await fetch(url);
   setData([...data]);
   setFilterData([...data]);
}    


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

useEffect(() => {

   //initially I have set the filter to gender, but it should be null, once the user selects any filter change the state of the filter, so for the first time when the screen mounts else part executes. 
   if(filter){
      const newFilterData = data.filter(item => item[filter] == value);
      setFilterData([...newFilterData]);
   }

}, [filter, value]);

Sources

This article follows the attribution requirements of Stack Overflow and is licensed under CC BY-SA 3.0.

Source: Stack Overflow

Solution Source
Solution 1
Solution 2 Rahul
Solution 3 soheil hassanjani
Solution 4 Hari Karthikkeyyan