reactjsdeep-copyreact-componentreact-stateshallow-copy

useState results in shallow copy of initial value


I would like to pass data (which is saved as a state) to a react component that graphs that data. That graph should also be able to filter the data.

The data is a nested object structured as follows.

{
  "mylog": {
    "entries": [
      {"Bool1": false, "Bool2": true, ...},
      {"Bool1": true, "Bool2": true, ...},
      ...
    ]
  },
  "another_log": {...},
  ...
}

My approach has been to define a state called filteredData within the graph component, set it to the data passed to the graph, and then update the filteredData state when I want to filter the data.

function App(props) {
  const [data, setData] = useState({...}); // Initial data here

  return (
    <div>
      <Graph data={data} />
    </div>
  );
}

function Graph(props) {
  const [filteredData, setFilteredData] = useState(props.data);

  const filter = () => {
    setFilteredData(data => {
      ...
    });
  }

  return (
    ...
  );
}

However, when filteredData gets filtered, data in the App component also gets filtered (and that breaks things). I've tried substituting {..props.data} for props.data in a couple of places, but that hasn't fixed it. Any ideas? Thanks in advance.

Here is a minimum, reproducible example: https://codesandbox.io/s/elastic-morse-lwt9m?file=/src/App.js


Solution

  • The fact that updating the local state is mutating the prop actually tells us that you're mutating state as well.

    data[log].entries = in your filter is the offender.

    const filter = () => {
      setFilteredData((data) => {
        for (const log in data) {
          data[log].entries = data[log].entries.filter((s) => s.Bool1);
    //    ^^^^^^^^^^^^^^^^^^^ Here is the mutation
        }
        return { ...data }; // Copying data ensures React realizes
        // the state has been updated (at least in this component).
      });
    };
    

    The return { ...data } part is also a signal that the state is not being updated correctly. It is a workaround that "fixes" the state mutation locally.

    You should make a copy of each nested array or object before modifying it.

    Here is an option for correcting your state update which will also solve the props issue.

    setFilteredData((data) => {
      const newData = {...data};
    
      for (const log in data) {
        newData[log] = { 
          ...newData[log],
          entries: data[log].entries.filter((s) => s.Bool1)
        }
      }
    
      return newData;
    });
    

    Running example below:

    const {useState} = React;
    
    function App() {
      const [data, setData] = useState({
        mylog: {
          entries: [{ Bool1: false }, { Bool1: true }]
        }
      });
    
      return (
        <div>
          <h3>Parent</h3>
          {JSON.stringify(data)}
    
          <Graph data={data} />
        </div>
      );
    }
    
    function Graph(props) {
      const [filteredData, setFilteredData] = useState(props.data);
    
      const filter = () => {
        setFilteredData((data) => {
    
          const newData = {...data};
    
          for (const log in data) {
            newData[log] = { 
              ...newData[log],
              entries: data[log].entries.filter((s) => s.Bool1)
            }
          }
          return newData;
        });
      };
    
      return (
        <div>
          <h3>Child</h3>
          <button onClick={filter}>Filter</button>
    
          {JSON.stringify(filteredData)}
        </div>
      );
    }
    
    ReactDOM.render(<App />, document.getElementById('root'));
    <script src="https://cdnjs.cloudflare.com/ajax/libs/react/16.8.4/umd/react.production.min.js"></script>
    <script src="https://cdnjs.cloudflare.com/ajax/libs/react-dom/16.8.4/umd/react-dom.production.min.js"></script>
    <div id="root"></div>