Nếu bạn đã sử dụng React một thời gian, bạn có thể cảm thấy cần có nhiều phiên bản của cùng một logic trong nhiều thành phần khác nhau. Một số trường hợp sử dụng bao gồm:
- Cuộn vô hạn trong ba chế độ xem khác nhau, mỗi chế độ có dữ liệu khác nhau.
- Các thành phần sử dụng dữ liệu từ đăng ký của bên thứ ba.
- Các thành phần ứng dụng cần dữ liệu người dùng đã đăng nhập.
- Hiển thị nhiều danh sách (ví dụ: Người dùng, Vị trí) với tính năng tìm kiếm.
- Nâng cao các chế độ xem card khác nhau với đường viền và bóng giống nhau.
Bạn có thể tự hỏi liệu có cách nào trong React để chia sẻ logic qua nhiều thành phần mà không cần viết lại nó.
Tuyệt đúng! Có một kỹ thuật tiên tiến để xử lý những quan tâm cắt ngang như vậy gọi là Higher-Order Components (HOCs).
Trong bài viết này, chúng ta sẽ hiểu rõ về HOCs, khi nào sử dụng mẫu này và trình bày các ví dụ thực tế với các ví dụ trong cuộc sống.
Hãy bắt đầu bằng việc hiểu cách mẫu này phát triển từ lập trình hàm.
Giới thiệu về Lập trình hàm
Một xu hướng đã bắt đầu được chú ý mạnh mẽ trong vài năm qua là lập trình hàm (FP).
Tiếp cận hướng đối tượng truyền thống khuyến khích việc phân tách một chương trình thành "đối tượng" liên quan đến một lĩnh vực cụ thể. Trái lại, tiếp cận hàm chỉ đạo nhà phát triển phân tách một chương trình thành các hàm nhỏ, sau đó kết hợp chúng để tạo thành một ứng dụng.
React và thực tế là JS hiện đại sử dụng rất nhiều kỹ thuật lập trình hàm cùng với mã mệnh lệnh có tính chất, tác động bên ngoài, idempotent, sự hợp thành của hàm, bao gói, chức năng cao cấp, currying, đánh giá lười biếng, v.v.
Trước khi đi xa hơn, tôi đề nghị bạn xem xét các hàm thuần túy và tác động bên ngoài từ FP. Liên kết này sẽ giúp bạn hiểu cơ sở của lập trình hàm.
Bây giờ chúng ta hãy đi vào hiểu rõ hơn về hàm cao cấp và thành phần cao cấp với sự trợ giúp của các ví dụ.
Một chút về hàm cao cấp trong Lập trình hàm
Một Hàm Cao cấp lấy một hàm làm đối số và / hoặc trả về một hàm.
Higher Order Component của React là một mô hình xuất phát từ tính chất của React ưa sự hợp thành hơn việc kế thừa.
Xem xét ví dụ này - // Ví dụ #1 const twice = (f, v) => f(f(v)) const add3 = v => v + 3 twice(add3, 7) // 13
// Ví dụ #2 const filter = (predicate, xs) => xs.filter(predicate) const is = type => x => Object(x) instanceof type filter(is(Number), [0, '1', 2, null]) // [0, 2]
// Ví dụ #3
const withCounter = fn => {
let counter = 0
return (...args) => {
console.log(Counter is ${++counter}
)
return fn(...args)
}
}
const add = (x, y) => x + y
const countedSum = withCounter(add)
console.log(countedSum(2, 3)) // Output - Counter is 1, 5
console.log(countedSum(2, 1)) // Output - Counter is 2, 3
Trong ví dụ trên, ví dụ 1 và 2 giải thích quá trình chuyển giao chức năng bên trong hàm cao cấp. Ví dụ 3 cho thấy chức năng bên trong hàm cao cấp có thể sử dụng biến bên ngoài cho mục đích như đếm.
Các thành phần cao cấp là gì?
Một thành phần cao cấp là một hàm lấy một thành phần và trả về một thành phần mới.
Mô hình thành phần cao cấp của React là một mẫu phát sinh từ tính chất của React ưa sự hợp thành hơn việc kế thừa.
Xem xét ví dụ này -
import React from 'react'
const higherOrderComponent = WrappedComponent => {
class HOC extends React.Component {
render() {
return
Trong ví dụ trên, higherOrderComponent là một hàm lấy một thành phần được gọi là WrappedComponent làm đối số. Chúng ta đã tạo một thành phần mới gọi là HOC trả về
Chúng ta có thể gọi HOC như sau:
const SimpleHOC = higherOrderComponent(MyComponent);
Một thành phần cao cấp biến một thành phần thành một thành phần khác. Lưu ý rằng một thành phần cao cấp không sửa đổi thành phần đầu vào. Thay vào đó, một thành phần cao cấp tạo thành phần gốc bằng cách bọc nó trong một thành phần container.
Một HOC là một hàm thuần túy không có tác động bên ngoài.
Lập trình một thành phần cao cấp thực tế
Giả sử chúng ta muốn tạo một danh sách các sản phẩm với tính năng tìm kiếm. Chúng ta muốn lưu trữ mảng sản phẩm của chúng ta trong một tệp phẳng và tải nó như một thành phần riêng như dưới đây -
import products from './products.json'
Tạo thành phần đầu tiên của chúng tôi
Hãy bắt đầu bằng việc tạo thành phần đầu tiên của chúng tôi ProductCard
. Thành phần này là một thành phần chức năng xử lý hiển thị dữ liệu của chúng tôi. Dữ liệu (sản phẩm) sẽ được nhận qua props và mỗi sản phẩm sẽ được chuyển xuống thành phần ProductCard
.
const ProductCard = props => { return (
Title: {props.title}
Style: {props.style}
Price: {props.price}
Description: {props.description}
Free shipping: {props.isFreeShipping}
Tiếp theo, chúng ta cần một thành phần sẽ lặp lại dữ liệu (sản phẩm) bằng cách sử dụng hàm .map()
. Hãy gọi thành phần này là ProductsList
.
const ProductsList = props => { let { data: products } = props; return (
Products
Chúng ta muốn người dùng có thể tìm kiếm các mặt hàng bằng cách sử dụng trường nhập. Danh sách các mặt hàng hiển thị trên ứng dụng phải được quyết định bởi trạng thái của tìm kiếm. Đây là một thành phần có trạng thái với đầu vào người dùng được lưu trữ trong giá trị trạng thái gọi là searchTerm
. Hãy gọi nó là ProductsListWithSearch
.
import products from './products.json'
class ProductsListWithSearch extends React.PureComponent { state = { searchTerm: '' }
handleSearch = event => { this.setState({ searchTerm: event.target.value }) }
render() { const { searchTerm } = this.state let filteredProducts = filterProducts(searchTerm);
return (
>
)
}
const filterProducts = (searchTerm) =>{
searchTerm = searchTerm.toUpperCase()
return products.filter(product => {
let str = ${product.title} ${product.style} ${product.sku}
.toUpperCase();
return str.indexOf(searchTerm) >= 0;
})}
}
searchTerm được gán giá trị trạng thái là một chuỗi rỗng. Giá trị được nhập bởi người dùng trong hộp tìm kiếm được thu thập và được sử dụng để đặt trạng thái mới cho searchTerm
.
Và bây giờ, nếu chúng ta muốn hiển thị một danh sách người dùng với tính năng tìm kiếm?
Chúng tôi sẽ tạo một thành phần mới tương tự với thành phần trên. Hãy gọi nó là UsersListWithSearch
.
Chuyển ProductsListWithSearch của chúng tôi thành HOC
Bây giờ bạn có thể nhận ra rằng chúng tôi sẽ phải lặp lại logic tìm kiếm trong cả hai thành phần. Đây là lúc chúng ta cần một Thành phần Cao cấp. Hãy tạo một HOC gọi là withSearch
.
const withSearch = WrappedComponent => { class WithSearch extends React.Component { state = { searchTerm: '' }
handleSearch = event => {
this.setState({ searchTerm: event.target.value })
}
render() {
let { searchTerm } = this.state
let filteredProducts = filterProducts(searchTerm)
return (
>
)
}
}
WithSearch.displayName = WithSearch(${getDisplayName(WrappedComponent)})
return WithSearch
}
const getDisplayName = WrappedComponent => { return WrappedComponent.displayName || WrappedComponent.name || 'Component' }
// Render danh sách sản phẩm với tính năng tìm kiếm const ProductsListWithSearch = withSearch(ProductsList);
function App() { return (
const rootElement = document.getElementById("root");
ReactDOM.render(
Trong mã trên, đầu tiên chúng ta đã nhập thành phần cao cấp. Sau đó, chúng ta đã thêm một phương thức filter để lọc dữ liệu dựa trên những gì người dùng nhập vào trong hộp tìm kiếm. Cuối cùng, chúng tôi bọc nó với thành phần withSearch
.
Đó là nó! Chúng ta đã có một thành phần cao cấp hoàn toàn chức năng!
Hãy xem ví dụ hoàn chỉnh trong codepen bên dưới -
Xem Pen React Hoc của Mohan Dere (@mohandere) trong CodePen.
Gỡ lỗi HOCs
Gỡ lỗi HOCs có thể khó vì chúng ta có thể có nhiều phiên bản của cùng một HOC trên cùng một trang.
Để giải quyết vấn đề này, chúng ta có thể chọn một tên hiển thị tốt hơn để thông báo rằng đó là kết quả của một HOC. Ví dụ, nếu thành phần cao cấp của bạn được đặt tên WithSearch, và tên hiển thị của thành phần được bọc là ProductsList, bạn nên sử dụng tên hiển thị WithSearch(ProductsList) như dưới đây:
WithSearch.displayName = WithSearch(${getDisplayName(WrappedComponent)})
;
Hãy so sánh nhanh kết quả với và không có thuộc tính displayName
trong Công cụ Phát triển React.
Không có displayName Ảnh
Có displayName Ảnh
Lưu ý đặc biệt
Không sử dụng HOCs trong phương thức render
Quá trình hòa giải của React sử dụng danh tính thành phần để quyết định xem có cập nhật cây con hiện tại hay gắn cây con mới giữa hai lần render. Điều này có nghĩa là danh tính thành phần phải nhất quán qua các lần render.
Ngoài ra, khi trạng thái của thành phần thay đổi, React phải tính toán xem có cần cập nhật DOM không. Điều này được thực hiện bằng cách tạo ra một DOM ảo và so sánh nó với DOM hiện tại. Trong ngữ cảnh này, DOM ảo sẽ chứa trạng thái mới của thành phần.
render() { // Một phiên bản mới của ProductsListWithSearch được tạo ra trong mọi lần render const ProductsListWithSearch = withSearch(ProductsList);
// Điều này làm cho toàn bộ cây con bị unmount/hoàn tác mỗi lần!
return
Nếu bạn sử dụng HOCs bên trong phương thức render, thì danh tính của HOCs không thể được bảo tồn qua các lần render và điều này ảnh hưởng đến hiệu suất tổng thể của ứng dụng.
Nhưng vấn đề ở đây không chỉ liên quan đến hiệu suất - việc gắn lại một thành phần làm mất trạng thái của thành phần đó và tất cả các thành phần con của nó.
Thay vào đó, chúng ta nên áp dụng HOCs bên ngoài định nghĩa thành phần để chỉ tạo thành phần kết quả một lần.
Các phương thức tĩnh phải được sao chép
Việc sử dụng phương thức tĩnh trên một thành phần React không hữu ích, đặc biệt là nếu bạn muốn áp dụng HOCs vào nó. Khi bạn áp dụng một HOC vào một thành phần, nó trả về một thành phần nâng cao mới. Trong thực tế, thành phần mới không có bất kỳ phương thức tĩnh nào của thành phần gốc.
Để giải quyết vấn đề này, bạn có thể sử dụng hoist-non-react-statics
để tự động sao chép tất cả các phương thức tĩnh không phải là React:
import hoistNonReactStatic from 'hoist-non-react-statics';
const withSearch = WrappedComponent => { class WithSearch extends React.Component { /.../ }
hoistNonReactStatic(WithSearch, WrappedComponent); return WithSearch; };
// Định nghĩa một phương thức tĩnh ProductsList.staticMethod = function() { /.../ };
// Áp dụng một HOC const ProductsListWithSearch = withSearch(ProductsList);
// Thành phần nâng cao không có phương thức tĩnh typeof ProductsListWithSearch.staticMethod === 'undefined'; // true
Không truyền tiếp refs
Bạn có thể muốn truyền tất cả các props cho thành phần được bọc. Tuy nhiên, bạn cần lưu ý về các ref vì chúng không được truyền tiếp. Điều này là do ref thực sự không phải là một prop - giống như key, nó được xử lý đặc biệt bởi React.
Giải pháp cho vấn đề này là sử dụng API React.forwardRef
.
Kết luận
Ở mức cơ bản nhất, React Higher-Order Component là hàm trả về một lớp và là sự thực thi của các nguyên tắc lập trình hàm trong mã nguồn của bạn. Cũng có khả năng chuyển hệ thống cao cấp thành các thành phần cao cấp khác vì thông thường chúng nhận các thành phần như đầu vào và trả về các thành phần khác như đầu ra. Điều này có thể khiến cây thành phần của bạn trở nên không cần thiết phức tạp.
Thư viện như Redux's connect, react-router's withRouter là những ví dụ tốt về tại sao chúng ta nên xem xét việc triển khai thành phần cao cấp.
Chúng ta sử dụng các thành phần cao cấp để tái sử dụng logic trong các ứng dụng React. Tuy nhiên, họ phải hiển thị một số giao diện người dùng. Do đó, HOCs không thuận tiện khi bạn muốn chia sẻ một số logic không phải là giao diện người dùng. Trong trường hợp đó, Hooks trong React có vẻ là cơ chế hoàn hảo cho việc tái sử dụng mã.