Stateful và Stateless Functional Components trong React

React là một thư viện javascript phổ biến để xây dựng nên một giao diện người dùng có tính tương tác cao. Mặc dù có rất nhiều khái niệm quan trọng, nhưng không thể phủ nhận rằng các Component chính là điều làm nên trái tim và tâm hồn của React. Có hiểu biết tốt về Components sẽ giúp công việc của bạn dễ dàng hơn khi là một lập trình viên React.

1. Components là gì?

Components là các thực thể độc lập, tự duy trì mà mô tả một phần giao diện người dùng của bạn. Giao diện người dùng của một ứng dụng có thể chia thành các component nhỏ hơn, mỗi component sẽ có cấu trúc, code, API riêng của nó. Kiến trúc component cho phép bạn nghĩ đến từng thành phần cô lập với nhau. Mỗi component có thể cập nhật mọi thứ trong phạm vi của nó mà không cần phải quan tâm đến việc nó ảnh hưởng đến các component khác như thế nào. Các components cũng có thể tái sử dụng. Nếu bạn cần cùng một component ở nhiều nơi thì thật dễ dàng để thực hiện điều đó. Với sự trợ giúp của cú pháp JSX, bạn có thể khai báo các component bất cứ nơi nào bạn muốn chúng xuất hiện.

<div>
    Số đếm hiện tại: {this.state.count}
    <hr />
    {/* Ví dụ tái sử dụng component. */ }
    <Button sign="+" count={this.state.count} 
        updateCount={this.handleCount.bind(this) }/>
    <Button sign="-" count={this.state.count}  
        updateCount={this.handleCount.bind(this) }/>
</div>

2. Props và state

Các components cần dữ liệu để làm việc. Có hai cách khác nhau mà bạn có thể kết hợp các component và dữ liệu: props và state. Props và state xác định những gì một component hiển thị và nó hoạt động như thế nào. Hãy bắt đầu với props nhé.

2.1. Props

Nếu các component là các hàm JavaScript thuần thì props sẽ là tham số đầu vào của hàm. Do đó, một component chấp nhận đầu vào (cái mà chúng ta gọi là props), xử lý nó, và sau đó cho ra một số mã JSX.

Mặc dù dữ liệu trong props có thể được truy cập vào component, nguyên tắc của React là các props không được thay đổi và truyền từ trên xuống dưới. Điều này có nghĩa là một component cha có thể truyền bất cứ dữ liệu nào nó muốn cho con của nó dưới dạng props, nhưng component con không thể sửa đổi props của chính nó. Vì vậy, nếu bạn cố gắng chỉnh sửa các props như dưới đây, bạn sẽ nhận được lỗi TypeError "Cannot assign to read-only".

const Button = (props) => {
    // props are read only
    props.count = 21;
...
}

2.2. State

State, mặt khác, là một đối tượng thuộc sở hữu của component nơi nó được khai báo. Phạm vi của nó được giới hạn trong component hiện tại. Một component có thể khởi tạo state của nó và cập nhật nó bất cứ khi nào cần thiết. State của component cha thường kết thúc là props của các components con.

Bây giờ chúng ta đã biết những điều cơ bản về component, hãy xét việc phân loại cơ bản các component.

3. Class Components và Functional Components

Một React component có thể có hai loại: component kiểu lớp hoặc component kiểu function. Sự khác biệt giữa hai loại trên có thể thấy rõ ràng từ tên của chúng.

3.1. Class Components

Class components cung cấp nhiều tính năng hơn. Lý do chính để chọn class component thay vì functional components là chúng có thể có state.

Có hai cách để bạn có thể tạo một class component. Cách truyền thống là sử dụng React.createClass(). ES6 giới thiệu một cú pháp cho phép bạn viết các class kế thừa từ React.Component. Tuy nhiên, cả hai phương pháp đều có kết quả tương tự nhau.

Các class components cũng có thể tồn tại mà không có state. Dưới đây là một ví dụ của một class component chấp nhận props đầu vào và trả về JSX.

class Hello extends React.Component {
    constructor(props) {
        super(props);
    }
     
    render() {
        return(
            <div>
                Hello {props}
            </div>
        )
    }
}

Chúng ta định nghĩa một hàm khởi tạo constructor chấp nhận các props như là tham số đầu vào. Trong constructor, chúng ta gọi super() để truyền bất cứ thứ gì đang được thừa kế từ lớp cha. Dưới đây là một vài chi tiết:

  • Thứ nhất, constructor là không bắt buộc khi định nghĩa một component. Trong ví dụ trên, component không có state, và constructor dường như không làm bất cứ điều gì hữu ích. this.props được sử dụng bên trong render() sẽ chạy được bất kể constructor được định nghĩa hay không.
  • Thứ hai, nếu bạn đang sử dụng một hàm constructor, bạn cần phải gọi super(). Đây là điều bắt buộc, và bạn sẽ nhận được lỗi cú pháp "Missing super() call in constructor" nếu không làm điều đó.
  • Và điều cuối cùng là về việc sử dụng super() với super(props). super(props) nên được sử dụng nếu bạn sẽ gọi this.props bên trong constructor. Nếu không, chỉ cần sử dụng super() là đủ.

3.2. Functional Components

Functional components chỉ là các hàm JavaScript. Chúng nhận vào một tham số tuỳ chọn được gọi là props, như mình đã nói ở trên.

Nhiều lập trình viên thích sử dụng một kỹ thuật mới mà ES6 hỗ trợ là arrow functions để định nghĩa các component. Arrow functions cung cấp cú pháp ngắn gọn để viết nên các functions. Bằng cách sử dụng arrow functions, chúng ta có thể bỏ qua việc sử dụng hai từ khóa, functionreturn, và một cặp ngoặc nhọn { }. Với cú pháp mới, bạn có thể định nghĩa một component chỉ trong một dòng như thế này.

const Hello = ({name}) => (<div>Hello, {name}!</div>);

4. Stateful Components và Stateless Components

Đây là một cách phổ biến khác để phân loại các component. Và các tiêu chuẩn để phân loại cũng khá đơn giản: các component có state và các component không có state.

4.1. Stateful Components

Stateful component luôn là class component. Như đã đề cập trước đó, các stateful components có một state được khởi tạo trong constructor.

constructor(props) {
  super(props);
  this.state = {
    count: 0
  };
}

Chúng ta đã tạo ra một object là state và khởi tạo nó với count = 0. Có một cú pháp thay thế được đề xuất để khiến việc này dễ dàng hơn được gọi là class fields. Nó chưa phải là một phần của đặc tả ECMAScript, nhưng nếu bạn đang sử dụng trình biên dịch Babel, cú pháp này cũng sẽ hoạt động tốt.

class App extends Component {
  /*
  // Việc này không còn bắt buộc nữa
  constructor() {
      super();
      this.state = {
        count: 0
      }
  }
  */
   
  state = {count: 0};
   
  handleCount(value) {
      this.setState((prevState) => ({count: prevState.count + value}));
  }
 
  render() {
    ...
  }
}

Bạn có thể tránh sử dụng constructor cùng với cú pháp mới này. Bây giờ chúng ta có thể truy cập vào state trong các phương thức của lớp bao gồm cả render(). Nếu bạn sử dụng chúng bên trong render() để hiển thị giá trị của count hiện tại, bạn cần đặt nó bên trong ngoặc nhọn như sau:

render() {
  return (
    Số đếm hiện tại: {this.state.count}
  );
}

Từ khóa this ở đây đề cập đến thể hiện của thành phần hiện tại. Khởi tạo state là không đủ - chúng ta cần phải có khả năng cập nhật state để tạo ra một ứng dụng có tính tương tác. Nếu bạn nghĩ rằng cách sau đây sẽ chạy đúng, thật ra không phải như vậy.

// Cách không đúng
 
handleCount(value) {
    this.state.count = this.state.count + value;
}

Các React component cung cấp một phương thức gọi là setState dùng để cập nhật state. setState chấp nhận một object chứa trạng thái mới của count.

// Như thế này sẽ chạy đúng
handleCount(value) {
    this.setState({count: this.state.count + value});
}

setState() chấp nhận một object như một tham số đầu vào, và chúng ta tăng giá trị trước của count lên 1 số value, nó sẽ làm việc như mong đợi. Tuy nhiên, có một nhược điểm. Khi có nhiều lời gọi setState mà đọc giá trị trước đó của state và viết một giá trị mới vào nó, chúng ta có thể kết thúc với race condition (là một tình huống xảy ra khi nhiều threads cùng truy cập và cùng lúc muốn thay đổi dữ liệu). Vì setState chạy bất đồng bộ. Điều này có nghĩa là kết quả cuối cùng sẽ không khớp với các giá trị mong muốn. Do đó, thay vì trực tiếp truyền vào một object, bạn có thể truyền vào đó một hàm updater có cú pháp: (prevState, props) => stateChange prevState là một tham chiếu đến state trước và được bảo đảm là mới nhất. Props đề cập đến các props của component, và chúng ta không cần props để cập nhật state ở đây, vì vậy chúng ta có thể bỏ qua tham số đó. Do đó, chúng ta có thể sử dụng cách này để cập nhật state và tránh được race condition.

// Cách dùng đúng
 
handleCount(value) {     
  this.setState((prevState) => {
    count: prevState.count + value
  });
}

setState() đặt lại component, và bạn có một stateful component hoạt động tốt.

4.2. Stateless Components

Bạn có thể sử dụng một hàm hoặc một lớp để tạo ra các stateless components. Nhưng trừ khi bạn cần sử dụng vòng đời trong các component của mình, không thì bạn nên sử dụng stateless functional component. Có rất nhiều lợi ích nếu bạn quyết định sử dụng stateless functional component ở đây; chúng rất dễ viết, dễ hiểu và dễ kiểm tra, và bạn có thể tránh được từ khoá this hoàn toàn. Tuy nhiên, theo React v16, không có lợi ích về hiệu suất từ việc sử dụng các stateless functional component thay cho class component. Nhược điểm là bạn không thể sử dụng vòng đời của React Component. Một phương thức của vòng đời là shouldComponentUpdate() thường được sử dụng để tối ưu hóa hiệu suất và tự kiểm soát những gì được render lại. Bạn không thể sử dụng nó với các functional components. Và refs cũng không được hỗ trợ.

All Rights Reserved