Bài 17: ReactJS - Higher Order Components - Học ReactJS Full Đầy Đủ Nhất

Đăng bởi: Admin | Lượt xem: 3084 | Chuyên mục: Javascript


Tìm hiểu về React Higher Order bằng cách thực hiện HOC pattern
Trong bài này chúng ta sẽ tìm hiểu về các khái niệm cần thiết để có thể tạo nên một higher-order components (HOCs). Chúng ta sẽ thực hiện HOC để có thể lưu state vào localStorage, gọi là withStorage, điều này sẽ cho phép bạn inject vào chức năng của component mà không cần phải lặp lại logic trên toàn bộ application của bạn.

I- Giới thiệu về HOC

Một HOC trong React là một patter được dùng để chia sẻ function chung giữa các component mà không phải lặp lại phần code chung. Nó thực tế không phải là component mà là function. Một HOC function nhận một component giống như tham số và trả về một component. Nó biến đổi một component thành component khác và thêm vào data hoặc function. Tức là:
const NewComponent = (BaseComponent) => {
// … create new component from old one and update
return UpdatedComponent
}
Trong hệ sinh thái React bạn có thể dễ dàng bắt gặp HOC được triển khai trong connect của Redux và withRouter trong React Router. Function connect trong Redux nhận một component truy cập vào global state trong Redux store, sau đó đưa vào component như là props. withRouter inject vào thông tin router và function trong component, cho phép lập trình viên có thể truy cập hoặc thay đổi route.

II- The higher-order component pattern

Higher-order component là một funtion, nó sẽ nhận một component giống như là một tham số và trả về một component. Điều này nghĩa là HOC sẽ luôn luôn có dạng như sau:
import React from 'react';
const higherOrderComponent = (WrappedComponent) => {
   class HOC extends React.Component {
    render() {
     return <WrappedComponent />
   }
  return HOC;
}
higherOrderComponent là function nhận một component gọi là WrappedComponent giống như là tham số. Chúng ta sẽ tạo một component mới gọi là HOC nó sẽ trả về <WrappedComponent/> từ chính render function. Thực tế ví dụ trên không thêm vào bất kỳ function nào trong HOC tuy nhiên nó là pattern phổ biến cho mọi HOC tuân theo. Sau khi tạo được HOC cách chúng ta gọi nó sẽ là:
const SimpleHOC = higherOrderComponent(MyComponent);

III - Phân tích vấn đề

Chúng ta hãy cùng đi vào một ví dụ thực tế: Giả sử bạn có một trang web thường xuyên sử dụng modal, như hình chẳng hạn.
Các thao tác cần hiện modal đó là:
  • Click vào từng dòng trong table.
  • Click vào icon User Profile để hiển thị thông tin người dùng đang đăng nhập hoặc login form.
  • ....
    Đây là cách mình xử lý modal cho table.
import React from 'react';

const TableModal = ({data, closeModal}) => {
    return (
        // hiển thị dữ liệu
        // xử lý đóng modal
    );
}
const TableRow = ({ data, showModal }) => {
    return (
        <tr onClick={() => showModal(data)}>
            <td>Name</td>
            <td>Gender</td>
            <td>Address</td>
        </tr>
    );
}

class Table extends React.Component {
    constructor() {
        super();
        this.state = {
            modal: {
                show: false,
                data: null
            }
        }
    }
    // hiển thị modal với data được truyền vào.
    showModal = (data) => {
        const { modal } = this.state;
        this.setState({modal: {
            modal: {
                show: true,
                data: data
            }
        })
    }
    // đóng modal, xóa bỏ dữ liệu trong modal
    closeModal = () => {
        this.setState({
            show: false,
            data: null
        })
    }
    
    render() {
        const { modal } = this.state;
        return (
            <table>
                <thead>
                    // table header
                </thead>
                <tbody>
                    <TableRow />
                    <TableRow />
                    <TableRow />
                    {
                        modal.show
                        ? <TableModal data={modal.data} closeModal={this.closeModal} />
                        : null
                    }
                <tbody>
            </table>
        );
    }
}
Và do cái icon User Profile cũng cần hiển thị modal chứa thông tin user khi click vào, chúng ta cũng sẽ phải viết ra một vài component và phải xử lý phần hiển thị modal => lặp lại code xử lý modal.
class UserProfile extends React.Component {
    constructor() {
        super();
        this.state = {
            modal: {
                show: false,
                data: null
            }
        }
    }
    // đoạn code trong khoảng dấu "*" này đã bị lặp lại, và khi chúng ta
    // có nhiều phần xử lý modal tương tự như thế này,
    // chúng ta rất có khả năng copy đoạn code này nhiều lần.
    *************************************************
    // hiển thị modal với data được truyền vào.
    showModal = (data) => {
        const { modal } = this.state;
        this.setState({modal: {
            modal: {
                show: true,
                data: data
            }
        })
    }
    // đóng modal, xóa bỏ dữ liệu trong modal
    closeModal = () => {
        this.setState({
            show: false,
            data: null
        })
    }
    *************************************************
    render() {
        return (
            {
                this.state.showModal
                ? <UserModal />
                : null
            }
            <button type="button">User Profile</button>
        );
    }
}
Vậy, chúng ta giải quyết tình huống này như thế nào???

IV- HOC và áp dụng vào giải quyết bài toán

Như đã nói ở trên, trong React có một khái niệm gọi là Higher-Order Component. Bản chất của Higher-Order Component là nhận vào một component và trả về một component - Ý tưởng rất đơn giản phải không nào còn việc gì xảy ra bên trong thì ta không biết  . Lợi ích của việc sử dụng HOC là chúng ta gom nhóm việc xử lý, tái sử dụng được component. Một HOC có hình dáng như sau
const withModal = WrappedComponent => {
    return class extends React.Component {
     // do something
    }
}
Chúng ta hãy cũng dùng HOC để giải quyết vấn đề vừa nêu trên.
Như đã phân tích, chúng ta đang bị lặp code ở phần xử lý hiển thị và đóng modal. Chúng ta mong muốn rằng HOC mà chúng ta đang chuẩn bị viết sẽ phải xử lý được phần modal đó và sẽ phải áp dụng được với bất kì component nào mà nó nhận vào.
Hãy cùng xem.
import ModalComponent from '../ModalComponent' // giả sử bạn đã có component này.

const withModal = WrappedComponent => {
    return class extends React.Component {
        constructor() {
            super();
            this.state = {
                show: false,
                data: null,
            }
        }
        showModal = (data) => {
            this.setState({
                show: true,
                data: data
            });
        }
        closeModal = () => {
            this.setState({
                show: false,
                data: null
            })
        }
        render() {
            return (
                <React.Fragment>
                    // truyền hàm xử lý hiển thị modal vào component mà kích hoạt hành động click
                    <WrappedComponet showModal={this.showModal} />
                    {
                        this.state.show
                        ? <ModalComponent data={this.state.data}
                                closeModal={this.closeModal}
                          /> // truyền data và hàm xử lý đóng modal vào ModalComponent
                        : null
                    }
                </React.Fragment>
            );
        }
    }
}
export default withModal;
Và trong WrappedCompoent, ta viết như sau
import React from 'react';
import withModal from './withModal';
class UserProfile extends React.Component {
// viết xử lý của bạn
}
// chúng ta export ra component đã được "nâng cấp"
export default withModal(UserProfile);
Đến đây, chúng ta đã có một HOC đẹp đẽ, phân tách được logic xử lý, ko lặp lại code :v. Tuy nhiên, còn một vấn đề nữa ở đây, với mỗi WrappedComponent chúng ta cần các Modal component khác nhau. Cụ thể: modal hiển thị nội dung khi click vào table khác với modal hiển thị nội dung khi click vào icon User Profile. (khác nhau về giao diện, về cách hiển thị). Lúc này, HOC của chúng ta cần thay đổi một chút
const withModal = ModalComponent => WrappedComponent => {
    // xử lý như đoạn code ở trên
}
Việc chúng ta viết HOC mới như trên sẽ giúp bạn linh động trong việc viết các component modal, giả sử, bạn muốn viết mới một modal component, thì chỉ cần viết mới và truyền vào withModal, chứ không cần phải sửa lại modal component cũ. Lúc này, WrappedComponent của bạn sẽ viết như sau
import React from 'react';
import withModal from './withModal';
import UserModal from './UserModal';

class UserProfile extends React.Component {
    ...
}

export default withModal(UserModal)(UserProfile)
// bạn có thấy giống hàm connect của redux không???

V- Các ví dụ :

Ví dụ 1 :
Bây giờ chúng ta sẽ mở rộng thêm higher-order component pattern trên để inject data vào phần wrapper.
Trong ví dụ dưới đây chúng ta đã chỉ rõ secretToLife sẽ là 42. Một số component của chúng ta cũng cần được chia sẻ thông tin này, và chúng ta sẽ tạo một HOC gọi là withSecretToLife để đưa thông tin này vào như là pros.
import React from 'react'; 
const withSecretToLife = (WrappedComponent) => {  
   class HOC extends React.Component {    
       render() {      
        return (       
          <WrappedComponent          
          {...this.props}          
          secretToLife={42}        
          />     
        );    
       }  
    }      
   return HOC;
};
Để ý thấy rằng ví dụ này gần giống với ví dụ trước. Điều khác biệt là chúng ta thêm vào một prop secretToLife={42}, điều này cho phép các component sử dụng nó có thể truy cập đến giá trị gọi là this.props.secretToLife.
Thêm vào đó chúng ta spread (syntax …) props để pass vào component. Điều này để chắc chắn rằng tất cả các props đều được pass vào wrapped component và nó có thể truy cập thông qua this.props .
import React from 'react';
import withSecretToLife from 'components/withSecretToLife'; 
const DisplayTheSecret = props => (  
    <div>    The secret to life is {props.secretToLife}.  </div>
); 
const WrappedComponent = withSecretToLife(DisplayTheSecret); 
export default WrappedComponent;
Component WrappedComponent chỉ là một version nâng cao của <DisplayTheSecret /> sẽ cho phép chúng ta sử dụng secretToLife giống như prop.
Ví dụ 2 :
Chúng ta hãy xem một ví dụ đơn giản để dễ dàng hiểu cách hoạt động của khái niệm này. MyHOC là một hàm bậc cao chỉ được sử dụng để chuyển dữ liệu đến MyComponent. Hàm này lấy MyComponent, tăng cường nó với newData và trả về thành phần nâng cao sẽ được hiển thị trên màn hình.
import React from 'react';

var newData = {
   data: 'Data from HOC...',
}

var MyHOC = ComposedComponent => class extends React.Component {
   componentDidMount() {
      this.setState({
         data: newData.data
      });
   }
   render() {
      return <ComposedComponent {...this.props} {...this.state} />
   }
};
class MyComponent extends React.Component {
   render() {
      return (
         <div>
            <h1>{this.props.data}</h1>
         </div>
      )
   }
}

export default MyHOC(MyComponent);
Chạy App và ta được kết quả như sau :

VI- Những điều cần cân nhắc khi sử dụng Higher-order component

  • Một HOC nên là pure function không có side-effects. Nó không nên làm thay bất cứ thứ gì và chỉ kết hợp original comonent bằng cách bao nó bằng một component khác.
  • Không sử dụng HOC trong phương thức render của một component. Truy cập HOC ngoài component.
  • Static method phải được copy để vẫn có thể truy cập chúng. Một cách đơn giản để làm điều này đó là dùng hoist-non-react-statics
  • Không dùng Refs
Trên đây là các kiến thức mà mình tìm hiểu được về các thành phần trong Higher Order Components rất mong được sự góp ý của mọi người. Cảm ơn mọi người đã theo dõi .
Bài tiếp theo: ReactJS - Bản rút gọn(Quick guide - P1) >>
vncoder logo

Theo dõi VnCoder trên Facebook, để cập nhật những bài viết, tin tức và khoá học mới nhất!