React – Performance (2) – Refactor Component

Not using state correctly

class Todos extends Component {
  constructor(props) {
    super(props);
    this.state = {
      items: ['foo', 'bar'],
      value: ''
    };
    this.handleChange = this.handleChange.bind(this);
    this.handleClick = this.handleClick.bind(this);
  }

  handleChange({ target: { value } }) {
    this.setState({
      value
    });
  }

  handleClick() {
    const items = this.state.items.slice();
    items.unshift(this.state.value);
    this.setState({
      items
    });
  }

  render() {
    return (
      <div>
        <ul>
          {this.state.items.map(item => <li key={item}>{item}</li>)}
        </ul>
        <div>
          <input
            type="text"
            value={this.state.value}
            onChange={this.handleChange}
          />
          <button onClick={this.handleClick}>+</button>
        </div>
      </div>
    );
  }
}

Investigation

The reason for the reduction in performance as soon as the number of items grows is that the list re-renders every time a user types in the field.
When we update the state with the new value of the controlled component, in fact, React calls the render again to see if the elements are different.
The only change is the value of the input element, and that is going to be the only mutation applied to the DOM; but, to figure out which operations are needed, React has to render the entire component and all its children, and rendering a big list of items many times over is expensive.

Reason

Now, if we look at the state object of the component, it’s pretty clear that it is not well structured. In fact, we are storing the items as well as the value of the form field, which are two entirely different things.
Our goal is always to have components that do one thing very well, rather than components with multiple purposes.

Solution

If we don’t want to re-render the list every time a user types in the field, we also do not want to render the list again as soon as the form is submitted and the new item is added.

So, change the Todos component to store only the list of items, which is the part of the store that is shared between the list and the form.

Then we create a separate list that only receives the items and a form that has its state for controlling the input field. The field fires a callback on the common parent to update the list when the form is submitted.

Improvement

class Todos extends Component {
  constructor(props) {
    super(props);
    this.state = {
      items: ['foo', 'bar']
    };
    this.handleSubmit = this.handleSubmit.bind(this);
  }

  handleSubmit() {
    const items = this.state.items.slice();
    items.unshift(this.state.value);
    this.setState({
      items
    });
  }

  render() {
    return (
      <div>
        <List items={this.state.items} />
        <Form onSubmit={this.handleSubmit} />
      </div>
    );
  }
}
class List extends PureComponent {
  render() {
    return (
      <ul>
        {this.props.items.map(item => <li key={item}>{item}</li>)}
      </ul>
    );
  }
}
class Form extends PureComponent {
  constructor(props) {
    super(props);
    this.state = {
      value: ''
    };
    this.handleChange = this.handleChange.bind(this);
  }

  handleChange({ target: { value } }) {
    this.setState({
      value
    });
  }

  render() {
    return (
      <div>
        <input
          type="text"
          value={this.state.value}
          onChange={this.handleChange}
        />
        <button onClick={() => this.props.onSubmit(this.state.value)}>+</button>
      </div>
    );
  }
}

Done! Now, the list and the form have two separate states and they render only when their props change.

For example, if we try to add hundreds of items inside the list, we see that the performance is not affected and the input field is not laggy.

We’ve solved a performance issue just by refactoring the component and changing the design a bit by separating the responsibilities correctly.

“Constants props” makes component re-rendered in even with PureComponent

render() {
  return (
    <div>
      <ul>
        {this.state.items.map(item => (
          <Item
            key={item}
            item={item}
            onClick={console.log}
            statuses={['open', 'close']}
          />
        ))}
      </ul>
      <button onClick={this.handleClick}>+</button>
    </div>
  );
}

Investigation

In the preceding code, for each Item, we set the key and the item prop to the current value of item. We fire console.log when the onClick event is fired inside Item, and we now have a new prop, which represents the possible statuses of item.
So, even if the values inside the array stay the same, on every render we are passing a new instance of the array to Item which makes the component is re-rendered.

Reason

The reason behind this behavior is that all the objects return a new instance when created and a new array is never equal to another, even if they contain the same values:
[] === []
false

Solution

What we can do to solve the issue here is to create the array once, and always pass the same instance to every render:

import React, { Component } from 'react';
import Item from './Item';

const statuses = ['open', 'close'];

class List extends Component {
  constructor(props) {
    super(props);
    this.state = {
      items: ['foo', 'bar']
    };
    this.handleClick = this.handleClick.bind(this);
  }

  handleClick() {
    const items = this.state.items.slice();
    items.unshift('baz');
    this.setState({
      items
    });
  }

  render() {
    return (
      <div>
        <ul>
          {this.state.items.map(item => (
            <Item
              key={item}
              item={item}
              onClick={console.log}
              statuses={statuses}
            />
          ))}
        </ul>
        <button onClick={this.handleClick}>+</button>
      </div>
    );
  }
}

export default List;

If we try the component again in the browser, we see that the console now contains no message, which means that the items do not re-render themselves unnecessarily when the new element is added.

Be the first to comment

Leave a Reply

Your email address will not be published.


*