State and Lifecycle

Bài này sẽ giới thiệu khái niệm của state và lifecycle trong React component.

Xem xét ví dụ về đồng hồ từ một trong những bài trước, trong Rendering Elements, chúng ta chỉ được học một cách để cập nhật giao diện. Gọi hàm ReactDOM.render() để đầu ra đã được render:

function tick() {
  const element = (
    <div>
      <h1>Hello, world!</h1>
      <h2>It is {new Date().toLocaleTimeString()}.</h2>
    </div>
  );
  ReactDOM.render( element, document.getElementById('root')  );}

setInterval(tick, 1000);

Trong bài này, chúng ta sẽ học cách để component Clock thực sự có thể tái sử dụng và đóng gói. Clock sẽ tự động cài đặt bộ đếm giờ của nó và tự cập nhật từng giây.

Chúng ta có thể bắt đầu bằng cách đóng gói cách clock nên hiển thị:

function Clock(props) {
  return (
    <div>
      <h1>Hello, world!</h1>
      <h2>It is {props.date.toLocaleTimeString()}.</h2>
    </div>
  );
}

function tick() {
  ReactDOM.render(
    <Clock date={new Date()} />,
   document.getElementById('root')
  );
}

setInterval(tick, 1000);

Tuy nhiên, nó thiếu một yêu cầu quan trọng: sự thật là Clock cài đặt bộ đếm giờ và cập nhật giao diện từng giây nên là một chi tiết được thực hiện của Clock.

Lý tưởng là chúng tôi muốn viết phần này và có một Clock có thể tự cập nhật:

ReactDOM.render(
  <Clock />,
  document.getElementById('root')
);

Để triển khai, chúng ta cần thêm “state” vào component Clock.
State khá giống với props nhưng nó là riêng tư và hoàn toàn bị điều khiển bởi component.

Chuyển function thành class
Bạn có thể chuyển một function component như Clock thành một class chỉ trong năm bước sau:

  1. Tạo một class của ES6 cùng tên, kế thừa React.Component.
  2. Tạo một phương thức rỗng được gọi là render().
  3. Chuyển nội dung của function vào phương thức render().
  4. Thay “props” bằng “this.props” trong phần thân của render().
  5. Xóa phần hàm rỗng còn lại mà bạn đã khai báo.
class Clock extends React.Component {
  render() {
    return (
      <div>
        <h1>Hello, world!</h1>
        <h2>It is {this.props.date.toLocaleTimeString()}.</h2>
      </div>
    );
  }
}

Giờ thì Clock được định nghĩa giống một class hơn là một function.

Phương thức render() sẽ được gọi mỗi lần có cập nhật, nhưng miễn là chúng ta render Clock() trong cùng một DOM node, chỉ có một trường hợp của Clock class sẽ được sử dụng. Điều đó làm chúng ta phải sử dụng thêm các tính năng bổ sung như local state và lifecycle method.

Thêm local state vào class

Chúng ta sẽ chuyển date từ props vào state theo ba bước:

  1. Thay thế this.props.date bằng this.state.date trong phương thức render() :
 class Clock extends React.Component {
  render() {
    return (
      <div>
        <h1>Hello, world!</h1>
        <h2>It is {this.state.date.toLocaleTimeString()}.</h2>
      </div>
    );
  }
}
  1. Thêm constructor của class để khởi tạo this.state ban đầu:
class Clock extends React.Component {
  constructor(props) {
    super(props);
    this.state = {date: new Date()};  }

  render() {
    return (
      <div>
        <h1>Hello, world!</h1>
        <h2>It is {this.state.date.toLocaleTimeString()}.</h2>
      </div>
    );
  }
}

Lưu ý các chúng ta truyền props vào constructor gốc:

  constructor(props) {
    super(props);
    this.state = {date: new Date()};
  }

Class components nên luôn luôn gọi constructor với props.

  1. Loại bỏ biến date trong element:
ReactDOM.render(
  <Clock />,
  document.getElementById('root')
);

Chúng ta sẽ thêm phần code của bộ đếm giờ về chính component sau này.

Kết quả như sau:

class Clock extends React.Component {
  constructor(props) {
    super(props);
    this.state = {date: new Date()};
  }
  render() {
    return (
      <div>
        <h1>Hello, world!</h1>
        <h2>It is {this.state.date.toLocaleTimeString()}.</h2>
      </div>
    );
  }
}

ReactDOM.render(
  <Clock />,
  document.getElementById('root')
);

Tiếp theo, chúng ta sẽ làm Clock cài đặt bộ đếm giờ của nó vào tự cập nhật mỗi giây.

Thêm lifecycle method vào class

Trong app có rất nhiều component, giải phóng tài nguyên được sử dụng bởi các component khi chúng bị loại bỏ là điều rất quan trọng.

Chúng ta muốn cài đặt bộ đếm giờ mỗi khi Clock được render vào DOM trong lần đầu tiên. Điều đó được gọi là “mounting” trong React.

Chúng ta cũng muốn loại bỏ bộ đếm giờ mỗi khi DOM được tạo ra bởi Clock bị loại bỏ. Điều đó được gọi là “unmounting” trong React.

Chúng ta có thể khai báo các phương thức đặc biệt trong component class để chạy code mỗi khi component được mount hoặc unmount:

class Clock extends React.Component {
  constructor(props) {
    super(props);
    this.state = {date: new Date()};
  }

  componentDidMount() {  }
  componentWillUnmount() {  }
  render() {
    return (
      <div>
        <h1>Hello, world!</h1>
        <h2>It is {this.state.date.toLocaleTimeString()}.</h2>
      </div>
    );
  }
}

Những phương thức này được gọi là “lifecycle methods”.

Phương thức componentDidMount() chạy sau khi đầu ra của component được hiển thị vào DOM. Đây là một nơi tốt để cài đặt bộ đếm giờ:

 componentDidMount() {
    this.timerID = setInterval(
      () => this.tick(),
      1000
    );
  }

Lưu ý cách chúng ta lưu ID của bộ đếm giờ ngay trong this (this.timerID).

Trong khi this.props được React tự cài đặt và this.state có ý nghĩa đặc biệt, bạn có thể thoải mái thêm các trường bổ sung một cách thủ công vào class nếu bạn muốn lưu vài thứ không tham gia vào luồng dữ liệu (như timerID chẳng hạn).

Chúng ta sẽ phân tách bộ đếm giờ trong phương thức lifecycle - componentWillUnmount():

  componentWillUnmount() {
    clearInterval(this.timerID);
  }

Cuối cùng, chúng ta sẽ thực hiện phương thức được gọi là tick() mà component Clock sẽ sử dụng từng giây.

this.setState() sẽ được sử dụng để lên lịch cập nhật cho component local state:

class Clock extends React.Component {
  constructor(props) {
    super(props);
    this.state = {date: new Date()};
  }

  componentDidMount() {
    this.timerID = setInterval(
      () => this.tick(),
      1000
    );
  }

  componentWillUnmount() {
    clearInterval(this.timerID);
  }

  tick() {
    this.setState({
     date: new Date()
    });
  }
  render() {
    return (
      <div>
        <h1>Hello, world!</h1>
        <h2>It is {this.state.date.toLocaleTimeString()}.</h2>
      </div>
    );
  }
}

ReactDOM.render(
  <Clock />,
  document.getElementById('root')
);

Bây giờ đồng hồ sẽ tích tắc mỗi giây.

Hãy tóm tắt nhanh lại những gì đã xảy ra và thứ tự mà các phương thức được gọi:

  1. Khi được truyền vào ReactDOM.render(), React gọi constructor của component Clock. Khi Clock cần hiển thị thời gian hiện tại, nó khởi tạo this.state với một object sở hữu thời gian hiện tại. Chúng ta sẽ cập nhật state sau đó.

  2. Tiếp theo React gọi hàm render() của component Clock. Đây chính là cách React biết điều gì nên được hiển thị trên màn hình. Sau đó React cập nhật DOM để phù hợp với đầu ra render của Clock.

  3. Khi đầu ra của Clock được đưa vào DOM, React gọi phương thức lifecycle - componentDidMount(). Trong đó, component Clock yêu cầu trình duyệt cài đặt thời gian để gọi phương thức tick() của component mỗi giây.

  4. Mỗi giây trình duyệt gọi phương thức tick(). Trong đó, component Clock lên lịch cập nhật giao diện bằng cách gọi setState() với object chứa thời gian hiện tại. Nhờ việc gọi setState(), React biết được state đã thay đổi, và gọi phương thức render() một lần nữa để biết được điều gì nên hiển thị trên màn hình. Trong thời gian này, this.state.date trong phương thức render() sẽ thay đổi, và đầu ra của render sẽ có thời gian đã cập nhật. React cập nhật lại DOM một cách phù hợp.

  5. Nếu component Clock bị loại bỏ khỏi DOM, React gọi phương thức lifecycle - componentWillMount() làm bộ định thời gian dừng lại.

Cách sử dụng đúng state

Đây là ba điều bạn cần biết biết về setState().

Không được thay đổi trực tiếp state

Ví dụ, component sẽ không được render lại:

// Wrong
this.state.comment = 'Hello';

Thay vào đó, sử dụng setState():

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

Nơi duy nhất bạn có thể chỉ định this.state là trong constructor.

State cập nhật có thể không đồng bộ

React có thể mang nhiều câu lệnh gọi setState() trong một cập nhật vì hiệu năng.

Bởi vì this.props. và this.state có thể cập nhật không đồng bộ, bạn không nên dựa vào giá trị của chúng để tính toán state tiếp theo.

Ví dụ, đoạn code này có thể cập nhật counter thất bại:

// Wrong
this.setState({
  counter: this.state.counter + this.props.increment,
});

Để sửa, nên sử dụng dạng thứ hai của setState() chấp nhận hàm hơn là một object. Hàm này sẽ nhận state trước đó như đối số đầu tiên, và props vào thời gian mà cập nhật được cho phép là đối số thứ hai:

// Correct
this.setState((state, props) => ({
  counter: state.counter + props.increment
}));

Chúng ta sử dụng hàm mũi tên ở trên nhưng với hàm thông thường cũng có thể hoạt động:

// Correct
this.setState(function(state, props) {
  return {
    counter: state.counter + props.increment
  };
});

Các cập nhật của state được hợp nhất

Khi bạn gọi setState(), React hợp nhất object mà bạn cung cấp vào state hiện tại.

Ví dụ, state của bạn có thể chứa rất nhiều biến độc lập:

 constructor(props) {
    super(props);
    this.state = {
      posts: [],      comments: []    };
  }

Sau đó bạn có thể cập nhật chúng một cách độc lập với các lần gọi setState() riêng biệt:

componentDidMount() {
    fetchPosts().then(response => {
      this.setState({
        posts: response.posts      });
    });

    fetchComments().then(response => {
      this.setState({
        comments: response.comments      });
    });
  }

Việc hợp nhất là không sâu, nên this.setState({comments}) không tác động đến this.state.posts nhưng thay thế hoàn thoàn this.state.comments.

Dữ liệu truyền xuống

Ngay cả component cha hay component con cũng không biết component nào đó là có hay không có state, và chúng không nên quan tâm liệu nó được định nghĩa là function hay class.

Đó chính là lý do tại sao state thường được gọi cục bộ hoặc đóng gói. Ngoài component ở hữu và thiết lập state thì không có component nào truy cập được.

Component có thể chọn cách truyền state cho các component con như props:

<h2>It is {this.state.date.toLocaleTimeString()}.</h2>

Điều này cũng hoạt động với các component người dùng định nghĩa:

<FormattedDate date={this.state.date} />

Component FormattedDate sẽ nhận date trong props của nó và không biết được rằng nó đến từ state của Clock, từ props của Clock, hoặc được viết bằng tay:

function FormattedDate(props) {
  return <h2>It is {props.date.toLocaleTimeString()}.</h2>;
}

Điều này thường được gọi là luồng dữ liệu “top-down” hoặc “unidirectional”. Bất kỳ state luôn luôn được sở hữu bởi vài component đặc biệt, hay bất kỳ dữ liệu hoặc giao diện có nguồn gốc từ state đó chỉ có ảnh hưởng với các component đứng dưới.

Nếu bạn tưởng tượng cây component là thác hước của props, mỗi state của component giống như các nguồn nước bổ sung tập hợp vào một điểm bất kỳ nhưng cùng rơi xuống.

Để thể hiện rằng tất cả component được thực sự độc lập, chúng ta có thể tạo component App hiển thị ba component Clock:

function App() {
  return (
    <div>
      <Clock />
      <Clock />
      <Clock />
    </div>
  );
}

ReactDOM.render(
  <App />,
  document.getElementById('root')
);

Mỗi Clock đều cài đặt bộ đếm giờ riêng và cập nhật một cách độc lập.

Trong React apps, liệu một component là stateful hay stateless được xem xét là chi tiết implementation của component có thể thay đổi theo thời gian. Bạn có thể sử dụng stateless component trong stateful component, và ngược lại.