Reputation: 9251
It is considered bad practice to assign props to a components state. But sometimes this seems necessary so that you can mutate that state and have the component rerender.
Currently my source of data is passed in from props, then I want to assign it to state in my constructor so that it defaults. Then when an button is clicked, I want to sort / filter that data then set state and have the component re-render.
class UsersList extends Component {
constructor(props){
super(props)
this.state = {
users: props.users
}
}
filterByLastName = (lastName) => {
const { users } = this.state
const filtered = users.map(u => u.lastName === lastName)
this.setState({ users: filtered })
}
render(){
const { users } = this.state
return(
<>
<button onClick={this.filterByLastName("Smith")}>Filter</button>
<ul>
{
users.map(u => (
<>
<li>{u.firstName}</li>
<li>{u.lastName}</li>
</>
)
)}
</ul>
</>
)
}
}
The problem with doing this is the if the components props change i.e. this.props.users
, it will cause a rerender but the constructor wont get called so the component will still have the old list of users assigned to it's props.users
value. You could use componentDidUpdate
(or another life cycle method) but this seems messy and over complicated.
If your data comes from props, it seems bad practice to assign it to state as you end up with 2 sources of truth.
What is the best way around this?
Upvotes: 1
Views: 184
Reputation: 1736
The source of triggering the render is some sort of "active the filter" action, right? I would move filterByLastName
logic to the beginning of the render method, then have a nameFilter
var at state and make filterByLastName
method set it instead.
This way you can check nameFilter
state var to apply the filtering on the incoming prop at render, so render will only occur when filter is changed and you keep a single source of truth with no need to save the prop into state.
Not tested but I guess it could be something like this:
class UsersList extends Component {
state = {
nameFilter: null
}
filterByLastName = lastName => this.setState({ nameFilter: lastName })
render(){
const { users } = this.props
const { nameFilter } = this.state
const filtered = nameFilter ? users.filter(u => u.lastName === nameFilter) : users
return(
<>
<button onClick={() => this.filterByLastName("Smith")}>Filter</button>
<ul>
{
filtered.map(u => (
<>
<li>{u.firstName}</li>
<li>{u.lastName}</li>
</>
)
)}
</ul>
</>
)
}
}
Upvotes: 2
Reputation: 222354
This is what getDerivedStateFromProps
is for. It calculates derived state on each component update.
It's necessary to keep all and filtered users separate because the list of filtered users needs to be reset on filter change.
const filterByLastName = (users, lastName) => users.map(u => u.lastName === lastName);
class UsersList extends Component {
static getDerivedStateFromProps(props, state) {
return {
users: state.filter ? props.users : filterByLastName(props.users, state.filter)
}
}
render(){
const { users } = this.state
return(
<>
<button onClick={() => this.setState({ filter: "Smith" })}>Filter</button>
<ul>
{
users.map(u => (
<>
<li>{u.firstName}</li>
<li>{u.lastName}</li>
</>
)
)}
</ul>
</>
)
}
}
Upvotes: 2