Sử dụng state trong react một cách hiệu quả

Khi viết một ứng dụng React, việc sử dụng state là việc khó tránh khỏi. Vậy sử dụng state như thế nào cho đúng, tôi xin đưa ra một vài quy tắc giúp việc sử dụng state hiệu quả và đúng đắn hơn

Đưa dữ liệu vào state khi cần thiết

Đầu tiên cũng là phần quan trọng nhất, việc quản lí dữ liệu trong state rất quan trọng, dữ liệu state của một component không nên bị phụ thuộc vào props truyền vào. Bạn có thể nghĩ như sau nếu state bị phụ thuộc vào props, ta sẽ có một câu hỏi props truyền vào thì khi props thay đổi state có thay đổi không?

Để dễ hiểu hơn ta sẽ lấy một ví dụ như sau

var React = require('react/addons');

class UserWidget extends React.Component {
  // ...

  // BAD: set this.state.fullName with values received through props
  constructor (props) {
    this.state = {
      fullName: `${props.firstName} ${props.lastName}`
    };
  }

  render () {
    var fullName = this.state.fullName;
    var picture = this.props.picture;

    return (
      <div>
        <img src={picture} />
        <h2>{fullName}</h2>
      </div>
    );
  }
}

Trong ví dụ này ta có thể thấy có những vấn đề sau

  • Component UserWidget hoàn toàn không quản lí dữ liệu firstNamelastName. Đơn giản nó chỉ nhận firstName và lastName rồi hiển thị ra giá trị tương ứng.

  • Khi props firstName và lastName thay đổi thì phần hiển thị của component UserWidget vẫn không thay đổi. Bởi vì hàm khởi tạo (contructor) chỉ chạy duy nhất một lần khi khởi tạo component nên state vẫn giữ nguyên giá trị ban đầu trong khi props đã thay đổi rồi.

Để giải quyết vấn đề này việc đơn giản là ta không xử dụng state để lưu giá trị fullName mà sẽ tính toán trực tiếp từ props

render () {
  var fullName = `${this.props.firstName} ${this.props.lastName}`;
  // ...
}

Sau khi thay đổi như trên thì giá trị của fullName luôn được thay đổi tương ứng với giá trị props firstName và lastName.

Lưu trữ dữ liệu một cách đơn giản và dễ thao tác nhất

Cấu trúc dữ liệu của state nên đơn giản, dễ hiểu dễ thao tác nhất có thể. Dưới đây là một ví dụ việc sử dụng dữ liệu state không đúng, rất khó để thao tác, làm cho logic code trở nên rất rối và khó hiểu

var React = require('react/addons');
var cx = React.addons.classSet;

class ArbitraryWidget extends React.Component {
  // ...

  constructor () {
    this.state = {
      classes: []
    };
  }

  // BAD: push 'hover' into this.state.classes when mousing over the component
  handleMouseOver () {
    var classes = this.state.classes;
    classes.push('hover');

    this.setState({ classes: classes });
  }

  // BAD: remove 'hover' from this.state.classes when the mouse leaves the component
  handleMouseOut () {
    var classes = this.state.classes;
    var index = classes.indexOf('hover');
    classes.splice(index, 1);

    this.setState({ classes: classes });
  }

  // BAD: toggle 'active' in this.state.classes when the component is clicked
  handleClick () {
    var classes = this.state.classes;
    var index = classes.indexOf('active');

    if (index != -1) {
      classes.splice(index, 1);
    } else {
      classes.push('active');
    }

    this.setState({ classes: classes });
  }

  render () {
    var classes = this.state.classes;

    return (
      <div
        className={cx(classes)}
        onClick={this.handleClick.bind(this)}
        onMouseOver={this.handleMouseOver.bind(this)}
        onMouseOut={this.handleMouseOut.bind(this)} />
    );
  }
}

Đọc một lúc thì bạn cũng có thể hiểu được component này làm những gì. Chúng ta có state classes lưu trữ các className của component này bao gồm activehover. Khi người dùng hover qua component thì state classes của component này sẽ bao gồm cả phần tử hover, ngược lại khi người dùng không hover nữa thì cũng xóa phần tử hover ra khỏi state classes tương tự với className active. Bạn có thể thấy nó vẫn hoạt động bình thường. Tuy nhiên việc thao tác với dữ liệu kiểu mảng (array) trong trường hợp này là không đúng và gây khó hiểu. Ta thấy việc component này có className hover hay active không giống với dữ liệu kiểu boolean (true, false) hơn, và việc tách chúng độc lập cũng dễ thao tác hơn. Như vậy ta sẽ chuyển sang sử dụng 2 flag là isHovering, isActive tương ứng với horver và active.

var React = require('react/addons');
var cx = React.addons.classSet;

class ArbitraryWidget extends React.Component {
  // ...

  constructor () {
    this.state = {
      isHovering: false,
      isActive: false
    };
  }

  // GOOD: set this.state.isHovering to true on mouse over
  handleMouseOver () {
    this.setState({ isHovering: true });
  }

  // GOOD: set this.state.isHovering to false on mouse leave
  handleMouseOut () {
    this.setState({ isHovering: false });
  }

  // GOOD: toggle this.state.isActive on click
  handleClick () {
    var active = !this.state.isActive;
    this.setState({ isActive: active });
  }

  render () {
    // use the classSet addon to concat an array of class names together
    var classes = cx([
      this.state.isHovering && 'hover',
      this.state.isActive && 'active'
    ]);

    return (
      <div
        className={cx(classes)}
        onClick={this.handleClick.bind(this)}
        onMouseOver={this.handleMouseOver.bind(this)}
        onMouseOut={this.handleMouseOut.bind(this)} />
    );
  }
}

Sau khi tối ưu hóa code (refactor) ta thấy đoạn code này dễ đọc hơn rất nhiều và logic code cũng trở nên đơn giản hơn. Ta thấy this.state.isHovering sẽ dễ hiểu hơn rất nhiều so với việc sử dụng this.state.classes.indexOf('hover') != -1 để xác định xem component này có được hover không. Việc thao tác với isHovering cũng đơn giản hơn nhiều so với classes. Sau khi tối ưu hóa lại code thì component này cũng dễ mở rộng và kiểm thử hơn.

Không tính toán hay so sánh điều kiện ở phương thức render

Việc tính toán quá nhiều hoặc có nhiều điều kiện so sánh làm cho phương thức render trở nên phức tạp và khó kiểm soát. Hơn nữa việc viết tất cả một khối code phức tạp ở trong một phương thức cũng là không nên. Tốt nhất là ta nên di chuyển nó ra thành các phương thức hỗ trợ (helper) với tên mô tả đúng công việc mà nó thực hiện. Điều này làm giảm sự phức tạp của phương thức render giúp đoạn code của bạn trở nên sáng sủa và dễ đọc hơn.

// GOOD: Helper function to render fullName
renderFullName () {
  return `${this.props.firstName} ${this.props.lastName}`;
}

render () {
  var fullName = this.renderFullName();
  // ...
}

Không lưu trữ các giá trị vào trong một đối tượng của một component như là một thuộc tính

var React = require('react/addons');

class ArbitraryWidget extends React.Component {
  // ...

  constructor () {
    this.derp = 'something';
  }

  handleClick () {
    this.derp = 'somethingElse';
  }

  render () {
    var something = this.derp;
    // ...
  }
}

Việc lưu trữ các giái trị vào một thuộc tính của component là không tốt, không chỉ bởi vì nó đi ngược lại với quy định lưu trữ trạng thái của component trong state mà còn bởi vì khi lưu trữ theo kiểu này khi các thuộc tính thay đổi thì component cũng không tự động render theo giá trị mới được cập nhật.

Không thay đổi state một cách trực tiếp

State nên được thay đổi thông qua phương thức setState, việc thay đổi trực tiếp giá trị của state mà ko qua setState sẽ làm cho trạng thái của state không khớp với phần hiện thị ra bên ngoài (khi thay đổi state một cách trực tiếp component sẽ không tự động render). Dưới đây là một ví dụ

// Wrong
this.state.comment = 'Hello';
// Correct
this.setState({comment: 'Hello'});

Chú ý: việc thay đổi giá trị của state một cách trực tiếp chỉ được phép sử dụng ở trong hàm tạo (contructor).

State được update một cách không đồng bộ

Đây là một điểm đặc biệt cần chú ý. Vì setState hoạt động bất đồng bộ, nên khi sử dụng setState cần chú ý trong trường hợp dùng lại giá trị của state cũ. Ta có ví dụ sau

// Current: this.state.counter is 0
// Wrong
this.setState({ counter: this.state.counter + 1});
this.setState({ counter: this.state.counter + 1});
this.setState({ counter: this.state.counter + 1});

Trong ví dụ trên ta thấy ban đầu state counter có giá trị là 0. Sau 3 lần chạy lệnh setState tăng giá trị của counter lên 1 đơn vị. Nhưng thực tế thì giá trị của counter chỉ tăng thêm 1 đơn vị chứ không phải là 3 đơn vị như chúng ta vẫn nghĩ. Vậy tại sao lại có điểm khác biệt này? Chúng ta có thể nghĩ đơn giản là 3 lệnh này chạy bất đồng bộ, lệnh thứ 2 không cần lệnh thứ nhất phải chạy xong mới chạy tiếp. Có thể coi 3 lệnh này chạy cùng lúc, tại thời điểm chạy thì cả 3 lệnh đều nhận state counter là 0 và chúng đều cập nhật state counter thành 1. Để giải quyết được vấn đề trên ta làm như sau

// Current: this.state.counter is 0
// Correct
this.setState((prevState) => ({
  counter: prevState.counter + 1
}));

this.setState((prevState) => ({
  counter: prevState.counter +1
}));

this.setState((prevState) => ({
  counter: prevState.counter + 1
}));

Sau khi thay đổi thành như trên ta sẽ thấy, mỗi lệnh setState sẽ được truyền vào là một function có parameter là prevState (state trước đó). Phần thân của function sẽ tính toán counter bằng cách lấy state counter trước đó cộng thêm 1 đơn vị. Sau 3 lần chạy lệnh setState trên ta nhận được state counter tăng lên 3 giá trị, đúng với giá trị ta mong muốn. Các bạn cần chú ý đặc điểm này để tránh những lỗi phát sinh khi sử dụng react !!!

All Rights Reserved