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
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>