ReactJS: Lifecycle của một component trong React (Phần 2)

Tiếp nối phần 1, mời bạn đón xem phần 2 của chủ đề: Lifecycle của một component trong React!

Cập nhật thông tin

Xem lại Phần 1: Lifecycle của một component trong React

Đầu năm 2018, nhóm phát triển React đã đưa ra một số vấn đề cần thay đổi để đáp ứng render bất đông bộ. Một số vấn đề về lifecycle được phát hiện và đã tìm ra giải pháp khắc phục.

Một trong những vấn đề lớn nhất là khi thực hiện các câu lệnh trong một số hàm của lifecycle không được an toàn. Một số hàm trong giai đoạn trước khi render bị hiểu sai và lạm dụng. Hơn nữa, việc sử dụng sai chức năng có thể gây ra nhiều vấn đề hơn khi render bất đồng bộ. Ví thế, React quyết định thêm tiền tố “UNSAFE_” vào tên các phương thức trong các phiên bản React sau này.

Đổi tên một số lifecycle

React không loại bỏ đột ngột các lifecyle không an toàn, mà chúng ta sẽ được cảnh báo và loại bỏ dần dần trong các phiên bản React, cụ thể là 3 phương thức:

  • componentWillMount()
  • componentWillReceiveProps()
  • componentWillUpdate()

16.3: React giới thiệu về các phương thức unsafe lifecycle: UNSAFE_componentWillMount(), UNSAFE_componentWillReceiveProps(), UNSAFE_componentWillUpdate(). Ở phiên bản này, cả hai tên phương thức cũ và mới đều sẽ làm việc bình thường.

16.3+: React bật cảnh báo trong console.log (DEV-mode warning) khi ta sử dụng các phương thức trên với tên cũ mà không có tiền tố “UNSAFE_”. Cả hai tên phương thức cũ và mới vẫn sẽ làm việc bình thường.

17.0: Ở phiên bản này trở đi, React sẽ loại bỏ hoàn toàn tên cũ của 3 phương thức trên. Chỉ khi sử dụng tên mới với “UNSAFE_ ” thì mới có thể hoạt động, nếu không sẽ gây ra lỗi.

Ghi chú: Nếu dự án của chúng ta đang sử dụng phiên bản React <17.0 thì những tên phương thức cũ được nêu trên vẫn hoạt động bình thường, không cần phải viết lại chúng ngay lập tức. Chỉ khi bạn muốn cập nhật để sử dụng phiên bản >=17.0 thì mới cần thiết đổi tên, React khuyên dùng lệnh ‘codemod” để có thể tự động chuyển đổi tên theo đúng chuẩn của họ.

cd your_project
npx react-codemod rename-unsafe-lifecycles

Các Lifecycle mới

Trong phiên bản 16.3, ngoài việc ra mắt 3 phương thức đã được đổi tên như trên, React có thêm 2 hàm trong lifecycle mới là getDerivedStateFromProps()getSnapshotBeforeUpdate(), 2 phương thức mới này đều nằm trong giai đoạn Updating của một component.

Phương thức getDerivedStateFromProps()

Chúng ta hiểu nôm na phương thức này là “nhận state mới được tạo ra bởi props“.

Hàm render được khởi chạy khi xảy ra 2 trường hợp sau:

  • Trường hợp 1: Khi ta gọi hàm setState để cập nhật lại state trong component.
  • Trường hợp 2: Khi component đó là con có props được truyền vào từ component cha bị thay đổi.

Phương thức này được sử dụng ở trường hợp thứ 2, được khởi chạy trước khi component được re-render (render lần thứ 2 trở đi). Hàm này có 2 arguments là nextProps (props mới được thay đổi) và currentState (state hiện tại của component).

Mặc định nếu props thay đổi thì component sẽ re-render. Nhưng có một trường hợp là được props thay đổi nhưng component con không nhận biết được để re-render, đó là chúng ta khởi tạo state bằng chính props. Ví dụ:

class ParentComponent extends React.Component {
  constructor(props) {
    super(props);
    this.state = { gender: "" };
  }

  handleChangeGender = (event) => {
    this.setState({ gender: event.target.value });
  };
  render() {
    return (
      <>
        <hr />
        <label htmlFor="gender"> Select your gender: </label>
        <select name="gender" onChange={this.handleChangeGender}>
          <option value="">-Select-</option>
          <option value="male">Male</option>
          <option value="female">Female</option>
        </select>
        <hr />

        <Input gender={this.state.gender} />
      </>
    );
  }
}
class ChildComponent extends React.Component {
  constructor(props) {
    super(props);
    this.state = { name: props.gender };
  }

  handleChange = (event) => {
    this.setState({ gender: event.target.value });
  };

  render() {
    return (
      <>
        <label htmlFor="name"> Input your name: </label>
        <input
          name="name"
          onChange={this.handleChange}
          value={this.state.name}
        />
        <hr />
        <h2>Hello! {this.state.name}</h2>
      </>
    );
  }
}

Theo ví dụ trên, ChildComponent có một state là ‘name’ được gán bằng ‘props.gender’ trong hàm constructor(). Cách này làm cho component không nhận biết được khi nào props được thay đổi. Lúc đó, chúng ta cần sử dụng hàm getDerivedStateFromProps() để bắt được sự thay đổi của this.props, chúng ta so sánh nextProps và currentState để cập nhật lại state với giá trị props được thay đổi. Thêm đoạn code sau dưới hàm constructor trong ví dụ để có thể cập nhật lại state:

static getDerivedStateFromProps = (nextProps, currentState) => {
    // Bất cứ lúc nào props.gender thay đổi thì cập nhật lại state.
    if (nextProps.gender !== currentState.name) {
      return {
        name: nextProps.gender,
      };
    }
    // Trả về rỗng để biểu thị không có cập nhật state
    return null;
};

Ghi chú: Hạn chế gán giá trị của props vào lúc khởi tạo state để đơn giản hóa component và tránh xảy ra lỗi.

Phương thức getSnapshotBeforeUpdate()

Hàm này được khởi chạy trước khi component re-render thành công. Chạy sau hàm render() nhưng trước hàm componentDidUpdate(). Giá trị được trả về trong giai đoạn này được truyền vào argument thứ 3 (snapshot) của componentDidUpdate().

Xét ví dụ sau để xem cách sử dụng của hàm này. Đề bài là khi chúng ta thêm tin nhắn vào danh sách log, nếu danh sách quá dài thì thanh scroll xuất hiện, sau đó thanh scroll sẽ tự động scroll đến dòng mới nhất vừa được thêm vào:

class PostLog extends React.Component {
  constructor(props) {
    super(props);
    this.state = {
      t: 0,
      messages: [],
    };
    this.chatRef = React.createRef();
  }

  componentDidMount() {
    this.timerID = setInterval(() => this.addMessage(), 500);
  }

  addMessage() {
    let { messages, t } = this.state;
    if (messages.length < 1000) {
      const newMessage = `Datetime: ${new Date().toISOString()} added log ${t}`;
      messages = [...messages, newMessage];
      t++;

      this.setState({ messages, t });
    }
  }

  renderMessage(msg, i) {
    return <li key={i}>{msg}</li>;
  }

  render() {
    return (
      <React.Fragment>
        <h1>Message Log</h1>
        <div ref={this.chatRef} className="log">
          <ul>
            {this.state.messages.map((msg, i) => {
              return this.renderMessage(msg, i);
            })}
          </ul>
        </div>
      </React.Fragment>
    );
  }
}

Ví dụ trên, một log sẽ được tự động thêm vào mỗi 500ms. Và ta sẽ thấy xuất hiện thanh scroll, nhưng không tự động scroll xuống log mới nhất. Thêm đoạn code sau bên dưới hàm componentDidMount() để thấy được sự thay đổi:

  // Hàm này sẽ trả về biến 'snapshot' trước khi DOM update thành công.
  getSnapshotBeforeUpdate(prevProps, prevState) {
    const { current } = this.chatRef;
    const isScrolledToBottom =
      current.scrollTop + current.offsetHeight >= current.scrollHeight;
    return { isScrolledToBottom };
  }

  // Recieve the snapshot and check if the user is scrolled to the bottom of the log
  // Hàm này nhận argument 'snapshot' và update lại vị trí scroll của element mới nhất.
  componentDidUpdate(prevProps, prevState, snapshot) {
    if (snapshot.isScrolledToBottom) {
      this.chatRef.current.scrollTop = this.chatRef.current.scrollHeight;
    }
  }

Kết quả hiển thị như sau:

Phương thức getSnapshotBeforeUpdate() có 2 arguments là prevProps (props trước đó) và prevState (state trước đó) và trả về là một giá trị bất kì. Hàm này không hoạt động riêng lẻ mà kết hợp sử dụng với componentDidUpdate().

Bên trên là những thay đổi về các phương thức mới lẫn cũ của lifecycle. Hy vọng bài viết này sẽ hữu ích với bạn. Cám ơn các bạn đã theo dõi bài viết.

Bài viết có tham khảo thông tin từ link: https://vi.reactjs.org/blog/2018/03/27/update-on-async-rendering.html