I have a simple component, which builds a list of reports from an array of objects, i'm trying to add filter and sort functionality (which does work - to a degree), The sort functionality works, but i fear i'm mutating state despite trying to copy the original state into a new array.
My filter works for the firs time, but then it will not filter other results because state has been mutated, or because i cannot filter through the initial state? This has had me confused for hours now, any help is much appreciated.
Thanks very much
constructor(props) {
super(props);
this.state = {
reports: props.data
};
this.handleSortBy = this.handleSortBy.bind(this);
this.handleFilterType = this.handleFilterType.bind(this);
}
handleSortBy(event) {
const copy = [...this.state.reports];
if (event.target.value === 'A-Z') {
return this.setState({
reports: copy.sort((a, b) => a.name.localeCompare(b.name))
});
}
if (event.target.value === 'Z-A') {
this.setState({
reports: copy
.sort((a, b) => a.name.localeCompare(b.name))
.reverse()
});
}
}
handleFilterType(event) {
this.setState({
reports: this.state.reports.filter(item => {
return item.type === event.target.value;
})
});
}
Thanks in advance :)
One easy way to do it depending on your use-case is to replace this.state.reports
with this.props.data
in your two update functions. (Although, if you do it this way, you couldn't apply sort and filter at the same time)
But I agree with ModestLeech that it's better to store the filter in state instead. To do that, change your filter code to something like:
handleFilterType(event) {
this.setState({
filter: event.target.value;
})
});
}
And in your render method, assuming you're using a map, you can change the map to this.state.reports.filter(item => (this.state.filter===undefined || item.type===this.state.filter)).map(...)
.
EDIT:
There are two types of mutation you need to worry about with React.
One is not mutating your props (i.e. mutating state that your component doesn't control). Since Javascript passes in arrays and objects by reference, if you were to sort
on this.props.data
it would actually modify the array that exists in your component's parent (or grandparent, or wherever it's defined). The good news is that your original code avoided that already - possibly accidentally. handleSortBy
made a copy of the array before mutating it, and handleFilterType
uses Array.prototype.filter
, which creates a new array.
The second mutation you have to worry about is changing local state without using setState
. Calling this.state.reports.sort(...)
in render
will change local state, but React won't know and won't re-render. This isn't a problem, since you're sorting right before you need it, but directly mutating state is dangerous because it could lead to bugs where you as the developer think something should or should not change under certain conditions but the reality is different.
Your original problem with filter
had nothing to do with accidental mutation. The issue was that by calling this.setState
the first time with a filter, you overwrote this.state.reports
and had no way to get it back. It also had the problem that if this.props.data
ever changes in the parent, the new reports would never appear in this component since you only read the props when the component is first created.
The recommended React way of doing this is not to store something in state that comes from a prop unless you are just using the prop to set some initial state and all future updates will come from inside the component. But assuming this component does not ultimately own the list of reports, it would be simpler in the long run to make a copy of this.props.data
in render
and apply your sort and filter to it. Then, if anything is added or removed higher in the component tree, you won't have to write any special logic to update the local state of this component.