Reputation: 974
I've been doing some research tonight surrounding the topic of using props to set the initial state of a component, and I've come across people arguing both sides. My question, therefore has two parts.
1) Is what I am doing considered the anti-pattern? From what I can tell it is not -- according to this article If so, what specifically is wrong with it?
2) Is there another way I could re-write this logic without using props to set the state?
Parent Component:
class App extends Component {
constructor(props){
super(props);
this.state ={
todos: []
}
}
componentDidMount() {
axios.get('https://jsonplaceholder.typicode.com/todos')
.then(response => {
this.setState({ todos: response.data })
});
}
render() {
if(!this.state.todos){
return <div>Loading...</div>
}
return (
<div className="container">
<div className="row">
{
this.state.todos.map((todo, i) => {
return (
<Todo todo={todo} key={i}/>
)
})
}
</div>
</div>
);
}
}
Child Component
class Todo extends Component{
constructor(props) {
super(props);
var { title, completed, userId } = this.props.todo;
this.state = { title, completed, userId }
}
changeCompletion = () => {
this.setState({completed: !this.state.completed})
}
render() {
return(
<div className="col-md-4 col-sm-6">
<div className={"card card-inverse text-center " + (this.state.completed ? 'card-success' : 'card-danger')}>
<div className="card-block">
<blockquote className="card-blockquote">
<p>{ this.state.title }</p>
</blockquote>
<button onClick={this.changeCompletion} className={"btn btn-sm " + (this.state.completed ? 'btn-danger' : 'btn-success')}>{ this.state.completed ? 'incomplete' : 'complete'} </button>
</div>
</div>
</div>
)
}
}
Upvotes: 0
Views: 153
Reputation: 5707
1. Is what I am doing considered the anti-pattern? - Maybe, YES
The problem is the constructor of your child component only runs once across multiple renders. If you use setState()
in your child component to re-render the constructor won't run again thus the props and the state are out of sync which is the "multiple sources of truth" problem the article mentions.
If you re-render your parent component (either through state change, props change, or this.forceUpdate()
), you child component's constructor won't re-execute. That is, React does this to improve performance internally, you can use console.log()
to investigate the component's life-cycles.
Usually, if I bump into your situation, I will add a life-cycle method called ComponentWillReceiveProps(nextProps)
and use the nextProps
there to re-initiate the state. Although it is not the perfect solution because other developers may modify the props in the component and we are back at the "multiple sources of truth" problem. You need to tell or educate other developers in your project that they are not allowed to modify props directly or indirectly in the component but even though, they may forget sometimes. But at least adding the life-cycle method does improve the situation.
2. Is there another way I could re-write this logic without using props to set the state?
You can try this approach: Basically, you expose a callback in child component's props and in the parent component, you execute it to trigger re-render of the parent component which in turn trigger re-render of all child components which is what we want.
And keep in mind that, when React re-renders all child components, it does not destroy then re-create all of them thus their constructor
won't re-execute. Only their life-cycle method ComponentWillReceiveProps(nextProps)
will be executed.
Parent Component:
<Todo todo={todo} key={i} onChangeCompletionCallback={() => {
let clonedState = Object.assign({}, this.state);
clonedState.todos[i].completed = !clonedState.todos[i].completed;
this.setState(clonedState);
}}/>
Child Component
changeCompletion = () => {
this.props.onChangeCompletionCallback();
}
One follow-up question, In this particular example, since there are 200 todos being rendered, and I'm only changing the completion status one at a time, would this be an instance where manually configuring the shouldComponentUpdate() function could be the difference of 1 render versus 200? Is it inherently bad to render all 200 todos?
Let's first clarify the render
term. There are 2 kinds of render
in React:
render
: slowrender
: (is designed to be) fastNow, if you Real DOM render
200 todos, that is definitely bad. But if you Virtual DOM render
200 todos, that depends. But most of the case, it will be fast.
When you render
a component via props change
, state change
or this.forceUpdate()
, you are doing a Virtual DOM render
. After that,
render
based on the differences above. It only
Real DOM renders
what is needed.So I don't think manually configuring the shouldComponentUpdate(...)
life-cycle method will help much in this case. It looks like you saved 199 Virtual DOM renders
but actually you are wasting your effort.
this.setState(...)
is asynchronous. That means whenever you call this.setState(...)
the Virtual DOM render
does not happen immediately. React will attempt to batch multiple Virtual DOM renders
in one big Virtual DOM render
. So even if you issue 199 virtual DOM renders
, react is smart enough to batch them together so only 1 virtual DOM render
will happen. Doing it manually in shouldComponentUpdate(...)
is not necessary.
And finally, my explanation is theoretically (based on the docs). If you take performance optimization problem seriously, you may need to do investigation to get more solid information (fact & figures). But at least, my theoretical explanation could give you some good start, I hope.
Upvotes: 2