Vì sao Higher Order Components lại quan trọng với sự phát triển của React?

4537

Nếu mới làm quen với React, bạn hẳn đã nghe qua  “Higher Order Components” và “Container” component. Và tự hỏi rằng sao mọi người có vẻ quan tâm đến bọn này cũng như cách chúng hoạt động ra sao.

Là một nhân viên bảo trì cho Apollo’s React integration – Open source library nổi tiếng với High Order Components. Tôi cũng phải mất một thời gian mới có thể hiểu rõ về chúng.

Vì thế mà hi vọng bài viết này sẽ cung cấp thông tin hữu ích cho bạn về Higher Order Components

React re-primer

Đầu tiên, trước khi đọc bài viết này, bạn cần sẽ biết chút đỉnh về React. Nếu có thời gian thì hãy vào xem bài viết của Sacha Greif’s 5 React Concepts. Do tôi muốn giữ mọi thứ ngắn gọn cho dễ đọc nên sẽ chỉ tóm tắt vài điều bạn cần phải biết về React.

Một React Application bao gồm một set của components. Component là tập hợp các input properties (props) và cho ra HTML vốn được rendered lên màn hình hiển thị. Khi component’s props thay đổi thì nó sẽ  re-render và HTML cũng bị thay đổi.

Khi user tương tác với HTML thông qua một event gì đó như click chuột, component  sẽ xử lí quá trình trên nhờ vào callback prop để thay đổi internal state. Hành động trên cũng sẽ khiến cho các state re-render.

Điều đó còn được gọi là component lifecycle, khi component lần đầu tiên được render, và gắn vào DOM cũng như thông qua các props mới, etc.

Component’s render function giúp trả lại một hoặc nhiều component khác. Từ đó tạo ra một nhánh cây – view tree. Nói cách khác, khi tương tác diễn ra sẽ dẫn đến việc các component phản ứng theo trình tự và qui định.

React UI vs statefulness

Có lẽ giờ thì cách này đã trở nên lỗi thời nhưng hồi trước, mọi thứ đều được phân biệt một cách rõ ràng vào 3 nhóm Models, Views và Controllers. Nhiệm vụ của một View là render và xử lý tương tác của người dùng trong khi Controller sẽ chuẩn bị Data cần thiết.

Tuy nhiên,  nhiều developer bắt đầu dùng tới functional stateless components trên React. Chúng là các “pure” component chỉ thực hiện việc chuyển đổi props thành HTML và callback props dựa trên các tương tác của user đưa ra

function MyInput({ title, value, onValueChange }) {
  return (
    <div>
      <label>{title}</label>
      <input type="text" value={value} onChange={onValueChange} />
    </div>
  );
}

Nói cách khác bạn có thể xem chúng là functional, như vậy view tree cũng  được xem là một function khổng lồ dành riêng cho việc chuyển đổi Props thành HTML.

Một lợi thế nổi bật của functional stateless component là vì chúng rất dễ để test và đơn giản. Nói cách khác, nó dễ phát triển và check phát hiện bug nhanh chóng.

Thế nhưng không có nghĩa bạn sẽ luôn dùng tới chúng. Bởi UI cần có state, và như vậy bạn sẽ phải dùng tới class-based components. Vấn đề sẽ bắt đầu trở nên phức tạp hơn khi “global state” của UI dính dáng vào view tree.

Global State

Global state của UI là state không trực tiếp liên kết với chỉ duy nhất một component. Nó thông thường bao gồm 2 phần:

Phần data trong ứng dụng của bạn vốn đến từ server. Thông thường các thông tin này được sử dụng cho nhiều vị trí khác nhau chỉ không phải cho một component duy nhất.

Global UI state – ta thường gắn nó với component ở vị trí cao nhất trong app của bạn và cho phép các component cấp thấp sử dụng nó nếu cần. Và những thay đổi sẽ được đưa ngược lại lên state, quá trình này con được gọi callback.

Phương thức này lại không được tốt bởi do nó sẽ chở nên chậm chạp nhanh chóng. Đó là bởi vì component gốc cần biết và hiểu hết tất cả yêu cầu đưa ra của toàn bộ các thành viên trong view tree.

Containers và Presentational Components

Vấn đề trên thường sẽ được giải quyết bằng cho phép  component truy cập global state từ bất cứ đâu trong view tree.

Nói cách khác, ta sẽ chia component thành nhóm được phép kết nối với global state và nhóm không.

Các “pure” components thuộc nhóm không sẽ dễ hiểu và test hơn (đặc biệt nếu chúng là functional stateless component). Nói cách khác khi chúng trở thành “impure” – làm nhiều chức năng khác thì sẽ khó hơn cho việc phát triển và test chúng.

Vì thế mà ta sẽ chia những “impure” component đó thành 2 phần:

  • Container component: Chuyên làm những việc mờ ám cho global state
  • Presentational component: Không làm cho global state

Và giờ đây ta có thể xem các component này như “Pure” bởi ra chi cách những phần phải làm việc đen tối cho global state vào một container.

Container

Bạn sẽ thấy việc viết ra container rất là thú vị bởi dù nó thuộc component, thế nhưng lại chả giống gì bởi:

  • Lấy và đưa một phần của global state cho các component cần
  • Chạy one data-accessing query và đưa ra kết quả cho các component cấp thấp hơn

Nói khác, chỉ một phần của component mà container chỉ định sẽ được render, đồng thời các component đó cũng phải có liên kết với container.

Tổng quát hóa Container

Các container khác nhau tùy thuộc vào loại component mà chúng liên kết với và loại data mà chúng lấy từ global state. Trong Redux, một container sẽ như sau:

// This is a vastly simplified implementation of what a Redux container would do
class MyComponentContainer extends Component {
  mapStateToProps(state) {
    // this function is specific to this particular container
    return state.foo.bar;
  }

  render() {
    // This is how you get the current state from Redux,
    // and would be identical, no mater what mapStateToProps does
    const { state } = this.context.store.getState();
    
    const props = this.mapStateToProps(state);
    
    return <MyComponent {...this.props} {...props} />;
  }
}

Tuy rằng container này sẽ có hết mọi tính năng như một Redux container thực thụ, bạn cũng có thể thấy ngoài mapStateToPropsMyComponent, chúng ta phải ghi nhập mã lệnh mỗi khi viết một Redux-accessing container.

Cách tạo ra Container

Bạn sẽ ngạc nhiên khi biết rằng khá dễ để viết một function chuyên generate container component dựa trên những thông tin thích hợp.

function buildReduxContainer(ChildComponentClass, mapStateToProps) {
  return class Container extends Component {
    render() {
      const { state } = this.context.store.getState();

      const props = mapStateToProps(state);

      return <ChildComponentClass {...this.props} {...props} />;
    }
  }
}

Đó là Higher Order Component (HOC), một function chuyên lấy một component và các thông tin khác nhằm tạo ra container cho chính component đó.

Gọi là high order là bởi vì đây là function có cấp cao. Nói cách khác, bạn ó thể xem React Components như những function tạo ra UI. Không chỉ thể mà nó vẫn xài được cho functional stateless components và cả pure stateful presentational component.

Một số ví dụ cho HOC

Redux’s connect function có lẽ được biết đến nhiều nhất, `buildReduxContainer` function trong bài viết này chính là một phiên bản dỏm dựa trên nó.

React Router’s withRouter function với tính năng lấy router từ context để biến thành thành prop cho các component.

react-apollovới interface chính là graphql HOC, khi được cung cấp component và GraphQL query sẽ đưa ra kết quả dành cho query và component đó.

Custom HOCs

Viết ra một HOC là một chuyện rất dễ dàng và tôi khuyên các bạn nên đọc qua bài viết rất tổng quát của Fran Guijarro để hiểu thêm về cách thức làm.

Lời kết

Higher order component chính là trái tim của việc phân chia components theo function để giải quyết chúng. Ở các phiên bản của React trong quá khứ, ta phải dùng đến classes và mixins để reuse code, thế nhưng trong tương lai, higher order components sẽ ảnh hưởng mạnh mẽ hơn trong quá trình phát triển của React.

Nếu bạn lo lắng về sự phức tạp thì đừng lo bởi team phát triển React đã dày công nghiên cứu và cải thiện giúp cho chúng trở nên đơn giản hơn ngay cả cho cả người mới.

Tham khảo thêm vị trí tuyển dụng React hấp dẫn tại đây

Nguồn: Topdev via Medium