jrnxf
jrnxf

Reputation: 974

Using props to initially set state

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

Answers (1)

Harry
Harry

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:

  • Real DOM render: slow
  • Virtual DOM render: (is designed to be) fast

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

  1. React uses a diffing algorithm to compare the Real DOM and the Virtual DOM then extract the differences.
  2. React does a Real DOM 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

Related Questions