3

Tom lược

Trong những năm qua, React đã phát triển với các mô hình và kỹ thuật khác nhau để giải quyết hai vấn đề cơ bản:

  1. Cách chia sẻ và sử dụng lại mã.
  2. Làm thế nào để quản lý nhà nước đơn hướng.

Trong các phiên bản đầu tiên của React, có khái niệm mixin. Chúng cuối cùng là một  giải pháp dễ vỡ . Mixins cuối cùng đã bị phản đối và một mô hình mới xuất hiện được gọi là  Thành phần bậc cao  (HoC). HoC giúp giải quyết vấn đề đầu tiên của chúng tôi về chia sẻ và tái sử dụng mã theo cách có thể duy trì và có thể kết hợp.

Dưới đây là định nghĩa từ tài liệu React:

Thành phần bậc cao (HOC) là một kỹ thuật nâng cao trong React để sử dụng lại logic thành phần. HOC không phải là một phần của API React, mỗi se. Chúng là một mô hình xuất hiện từ bản chất sáng tác của React.

Facebook cũng phát hành một mẫu gọi là  Flux . Flux quản lý trạng thái bằng cách sử dụng luồng dữ liệu đơn hướng. Một thư viện có tên Redux đã phát triển từ các ý tưởng của Flux và nhanh chóng trở thành một trong những cách thực tế để quản lý trạng thái trong các ứng dụng React. Các nhà phát triển đã trở nên phổ biến để quản lý toàn bộ trạng thái ứng dụng của họ trong Redux và sử dụng HoC để tái sử dụng mã.

Redux là một thư viện tuyệt vời nhưng nếu có cách quản lý trạng thái mà không sử dụng thư viện riêng thì sao? May mắn thay React hỗ trợ trạng thái thành phần cục bộ tích hợp. Sử dụng trạng thái cục bộ và một kỹ thuật gọi là đạo cụ kết xuất, chúng ta có thể chia sẻ và sử dụng lại mã cũng như quản lý trạng thái đơn hướng chỉ sử dụng các thành phần React. Đạo cụ kết xuất cung cấp một cách để sử dụng lại mã bằng cách đóng gói trạng thái hoàn toàn trong React. Kể từ React 16.3, đạo cụ kết xuất cung cấp một lợi thế khác - React đã phát hành API ngữ cảnh chính thức sử dụng hương vị của đạo cụ kết xuất được gọi là hàm như một mẫu con.

Hãy xây dựng một thành phần kết xuất Prop!

Một định nghĩa đơn giản về prop prop từ tài liệu React là:

Thuật ngữ kết xuất dữ liệu prop prop đề cập đến một kỹ thuật đơn giản để chia sẻ mã giữa các thành phần React bằng cách sử dụng prop có giá trị là một hàm.

Cách tốt nhất để minh họa điều này là thông qua một ví dụ.

Hãy giả vờ rằng bạn có nhiệm vụ xây dựng một thành phần Adder. Thành phần này cần lấy một giá trị ban đầu là giá trị hiện tại của Adder. Mỗi khi người dùng gửi một giá trị mới, nó sẽ thêm giá trị được gửi với giá trị hiện tại của Trình cộng. Thành phần này có thể trông giống như thế này:

import React from 'react'

export default class Adder extends React.Component {
  inputEl = React.createRef()

  state = {
    value: this.props.initialValue
  }

  static defaultProps = {
    initialValue: 0
  }

  handleAddValue = event => {
    event.preventDefault()

    this.setState(
      state => ({
        value: state.value + Number(this.inputEl.current.value)
      }),
      () => (this.inputEl.current.value = '')
    )
  }

  render() {
    return (
      <div>
        <div>My value is {this.state.value}</div>
        <input type="number" ref={this.inputEl} />
        <button type="button" onClick={this.handleAddValue}>
          Add Value
        </button>
      </div>
    )
  }
}

Thành phần  bổ sung gốc (CodeSandbox)

Thành phần này có giá trị  propValue ban đầu  hoặc mặc định là  0 . Mỗi lần nhấp vào nút, giá trị của đầu vào sẽ được thêm vào giá trị trạng thái  .

Mọi thứ đều hoạt động tuyệt vời; sau đó đồng nghiệp của bạn xuất hiện và muốn sử dụng lại thành phần Adder của bạn nhưng với lời cảnh báo rằng nó cũng cần xử lý các hoạt động toán học khác như trừ, nhân và chia; cũng như có thể hỗ trợ một cái nhìn và cảm nhận khác nhau.

Vì kiểu dáng có thể khác nhau giữa các thành phần, phần quan trọng nhất của mã để sử dụng lại là trạng thái của thành phần. Điều này có nghĩa là đối tượng trạng thái giữ giá trị của chúng tôi cũng như các phương thức cập nhật trạng thái. Trước khi chúng tôi bắt đầu chuyển đổi thành phần này để sử dụng đạo cụ kết xuất, điều quan trọng là phải hiểu cách thức hoạt động của JSX khi được dịch mã.

Đây là một chức năng kết xuất đơn giản:

render() {
  return (
    <button disabled={this.props.disabled}>
      <span>{this.props.icon}</span>
      {this.props.text}
    </button>
  )
}

Khi Babel biên dịch JSX, nó sẽ trở thành:

render() {
  return React.createElement(
    "button",
    { disabled: this.props.disabled },
    React.createElement("span", null, this.props.icon),
    this.props.text
  );
}

Babel lấy JSX và chuyển đổi nó thành  React.createEuity  có chữ ký sau:

React.createElement(type, [props], [...children])

Điều gì sẽ xảy ra nếu thay vì trả về JSX, chúng ta đã trả về một hàm từ   hàm render ?

render() {
  return this.props.render({ some: 'values' })
}

Sau đó, trong thành phần cha mẹ, chúng ta có thể truyền cho thành phần kết xuất:

render() {
  return <MyComponent render={(obj) => (...)} />
}

Khi điều này được biên soạn bởi Babel, nó trông giống như:

render() {
  return React.createElement(MyComponent, {
    render: obj => { ... }
  });
}

Lưu ý làm thế nào bây giờ   đối tượng đạo cụ (đối số thứ hai) trả về một hàm mong đợi  obj  làm đối số. Obj này   cư trú khi phương thức kết xuất thành phần được thực thi. Điều này có nghĩa là   giá trị obj ở trên nhận  {some:'values'} làm đối số. Đối tượng này có thể bị phá hủy để được viết là:

render() {
  return <MyComponent render={({ some }) => <h1>{some}</h1>} />
}

Bây giờ chúng ta đã hiểu rõ hơn về cách thức hoạt động của JSX và đạo cụ kết xuất, hãy cập nhật thành phần Adder của chúng ta bằng cách gọi prop render của chúng ta với đối tượng trạng thái cục bộ:

import React from 'react'

export default class Adder extends React.Component {
  inputEl = React.createRef()

  state = {
    value: this.props.initialValue
  }

  static defaultProps = {
    initialValue: 0
  }

  handleAddValue = event => {
    event.preventDefault()

    this.setState(
      state => ({
        value: state.value + Number(this.inputEl.current.value)
      }),
      () => (this.inputEl.current.value = '')
    )
  }

  render() {
    return this.props.render(this.state)
  }
}

Lưu ý:  Chúng tôi đang sử dụng một prop được gọi là  render  trong ví dụ của chúng tôi,  nhưng bất kỳ prop nào cũng có thể là prop prop !

Sau đó chúng ta có thể tạo một thành phần mới gọi là  AdderView:

export default class AdderView extends React.Component {
  render() {
    return (
      <Adder
        render={({ value }) => (
          <div>
            <div>My value is {value}</div>
            <input type="number" ref={el => (this.inputEl = el)} />
            <button type="button" onClick={this.handleAddValue}>
              Add Value
            </button>
          </div>
        )}
      />
    )
  }
}

Lưu ý cách  this.state  được truyền cho prop prop.

Điều này có nghĩa là chúng ta có thể hủy cấu trúc trên đối tượng trạng thái như ví dụ giới thiệu đầu tiên của chúng tôi vì hình dạng trạng thái là:

{
  value: ...
}

Điều đó thật tuyệt! Bây giờ chúng tôi đã tách rời quan điểm của chúng tôi và trạng thái của thành phần. Tuy nhiên, vẫn còn một vấn đề. Chúng tôi đã có một phương thức gọi là  handleAddValue  mà thành phần con không còn có quyền truy cập vào ví dụ của nó. Nếu bạn bấm vào  Thêm giá trị , nó không có cách nào để cập nhật trạng thái cha.

Chúng ta cần một cách để thành phần con báo cho thành phần cha mẹ cập nhật mà không phá vỡ dataflow một chiều. Vì chúng ta đang sử dụng trạng thái cục bộ, chúng ta cần một cách để thành phần con nói với thành phần cha mẹ gọi  setState . Một cách để giải quyết điều này là tạo một hàm trong thành phần cha mẹ và thêm nó làm thuộc tính trên đối tượng được truyền cho prop prop của chúng ta. Sau đó, bất cứ khi nào thành phần con cần cập nhật trạng thái, nó gọi hàm này.

Hàm này sau đó thực thi trong ngữ cảnh cha và gọi  setState . Khi  setState  được chạy, nếu bất kỳ giá trị trạng thái nào thay đổi, các giá trị mới đó sẽ truyền xuống prop prop của chúng ta và thành phần con bây giờ sẽ nhận được giá trị mới.

Điều này bảo tồn dataflow đơn hướng của chúng tôi. Hãy thực hiện những cập nhật đó ngay bây giờ:

render() {
  return this.props.render({
    ...this.state,
    addValue: this.handleAddValue
  })
}

Chúng tôi có thể điều chỉnh tay cầm hiện  tạiAddValue :

handleAddValue = value => {
  this.setState(state => ({
    value: state.value + value
  }))
}

Ngoài ra, tinh chỉnh  thành phần AdderView mới của chúng tôi  :

export default class AdderView extends React.Component {
  inputEl = React.createRef()

  render() {
    return (
      <Adder
        render={({ value, addValue }) => {
          return (
            <div>
              <div>My value is {value}</div>
              <input type="number" ref={this.inputEl} />
              <button
                type="button"
                onClick={e => {
                  e.preventDefault()

                  addValue(Number(this.inputEl.current.value))
                  this.inputEl.current.value = ''
                }}
              >
                Add Value
              </button>
            </div>
          )
        }}
      />
    )
  }
}

Bây giờ đối tượng giá trị của chúng ta được truyền vào hàm render là:

{
  value: ...,
  addValue: value => {...}
}

Hãy lùi lại một bước và suy nghĩ về đối tượng này và nó liên quan đến thành phần của chúng ta như thế nào. Bây giờ chúng ta có một đại diện theo nghĩa đen của trạng thái của chúng ta, với chức năng cập nhật trạng thái đó. Đây là một lợi thế lớn để sử dụng đạo cụ kết xuất. Bạn có thể thấy một ảnh chụp nhanh rõ ràng về trạng thái của một thành phần trong một đối tượng JavaScript đơn giản.

import React from "react";
import Adder from "./Adder";

class AdderView extends React.Component {
  inputEl = React.createRef();

  render() {
    return (
      <Adder
        render={({ value, addValue }) => {
          return (
            <div>
              <div>My value is {value}</div>
              <input type="number" ref={this.inputEl} />
              <button
                type="button"
                onClick={e => {
                  e.preventDefault();

                  addValue(Number(this.inputEl.current.value));
                  this.inputEl.current.value = "";
                }}
              >
                Add Value
              </button>
            </div>
          );
        }}
      />
    );
  }
}

export default AdderView;

Đây là thành phần kết xuất prop Adder cuối cùng  (CodeSandbox)

Bây giờ, trở lại vấn đề ban đầu. Chúng tôi có một thành phần quản lý trạng thái của chúng tôi và chuyển trạng thái đó làm đối số cho prop prop của chúng tôi; có nghĩa là bây giờ chúng ta có thể tiêu thụ bất kỳ quan điểm cần thiết. Điều này giải quyết một trong những vấn đề tái sử dụng chúng tôi đã có. Bây giờ, chúng ta chỉ cần thêm các phép toán khác - phép trừ, phép nhân và phép chia.

Vì tên thành phần của chúng tôi quá cụ thể cho các tính năng mới mà chúng tôi muốn triển khai, trước tiên, hãy đổi tên thành phần của chúng tôi từ Adder thành Math và thêm các hành động mới. Chúng ta cũng thêm một phương thức trả về các hành động như một đối tượng và trả về tất cả các hành động trong cùng một thuộc tính đối tượng. Điều này cho đồng nghiệp của bạn biết rõ giá trị nào đến từ trạng thái và giá trị nào là chức năng để cập nhật trạng thái cha. Để lặp lại với thuật ngữ Redux, hãy gọi hành động thuộc tính này  .

export default class Math extends React.Component {
  state = {
    value: this.props.initialValue
  }

  static defaultProps = {
    initialValue: 0
  }

  handleResetValue = () => {
    this.setState({
      value: this.props.initialValue
    })
  }

  handleAddValue = value => {
    this.setState(state => ({
      value: state.value + value
    }))
  }

  handleSubtractValue = value => {
    this.setState(state => ({
      value: state.value - value
    }))
  }

  handleMultiplyValue = value => {
    this.setState(state => ({
      value: state.value * value
    }))
  }

  handleDivideValue = value => {
    if (value !== 0) {
      this.setState(state => {
        return {
          value: state.value / value
        }
      })
    }
  }

  getActions = () => {
    return {
      resetValue: this.handleResetValue,
      addValue: this.handleAddValue,
      subtractValue: this.handleSubtractValue,
      multiplyValue: this.handleMultiplyValue,
      divideValue: this.handleDivideValue
    }
  }

  render() {
    return this.props.render({
      ...this.state,
      actions: this.getActions()
    })
  }
}

Bây giờ khi prop prop được gọi, nó chứa một đối tượng trạng thái trông như thế này:

{
  value: ...,
  actions: {
    resetValue: value => {...},
    addValue: value => {...},
    subtractValue: value => {...},
    multiplyValue: value => {...},
    divideValue: value => {...}
  }
}

Bây giờ đồng nghiệp của chúng tôi có thể lấy thành phần này và sử dụng lại tất cả logic trạng thái trong khi hiển thị cùng một chế độ xem hoặc một chế độ xem khác. Trạng thái của thành phần cũng được gói gọn hoàn toàn trong thành phần này, có nghĩa là thành phần có thể hiển thị nhiều lần trên cùng một trang mà không có bất kỳ vấn đề nào. Ví dụ:

render() {
  const style = { padding: '1em' }

  return (
    <div>
      <Math
        render={({ value, actions }) => (
          <Adder value={value} addValue={actions.addValue} />
        )}
      />
      <div style={style} />
      <Math render={mathProps => <AllOperations {...mathProps} />} />
    </div>
  )
}

Điều này hiển thị hai thành phần khác nhau có chứa một thể hiện trạng thái duy nhất trong mỗi thành phần.

Ví dụ đầy đủ có ở đây  (CodeSandbox)

Chia sẻ trạng thái với bối cảnh

Một câu hỏi bạn vẫn có thể có là nếu bạn muốn chia sẻ các giá trị từ một cá thể thành phần Toán học duy nhất, trong đó mỗi thành phần có thể có cha mẹ khác nhau. Đây là nơi Bối cảnh React có thể giúp đỡ (nếu bạn không quen thuộc với Ngữ cảnh, tài liệu React chính thức là một nơi tuyệt vời để  bắt đầu ). Tài liệu phản ứng mô tả Bối cảnh như:

Bối cảnh cung cấp một cách để truyền dữ liệu qua cây thành phần mà không phải truyền đạo cụ xuống ở mọi cấp độ.

Một bối cảnh có thể được coi là một thùng chứa trạng thái đóng gói có thể được chia sẻ với toàn bộ cây con. Như đã đề cập trước đó, Ngữ cảnh sử dụng một chức năng như một đứa trẻ là một kiểu kết xuất; nhưng thay vì sử dụng prop được gọi là  render , chúng tôi sử dụng   prop con là giá trị giữa các thẻ đóng và mở Element / Element trong JSX.

Một bối cảnh được tạo thành từ hai Thành phần - thành phần Nhà cung cấp và thành phần Người tiêu dùng. Nhà cung cấp đặt một giá trị ở đầu cây thành phần và phát giá trị này cho bất kỳ thành phần Tiêu dùng con nào. Để hiểu rõ hơn về điều này, trước tiên hãy tạo một thành phần Nhà cung cấp:

const MathContext = React.createContext()

export class MathContextProvider extends React.Component {
  render() {
    return (
      <Math
        render={values => (
          <MathContext.Provider value={values}>
            {this.props.children}
          </MathContext.Provider>
        )}
      />
  )
}

Như bạn có thể thấy, vì cả Ngữ cảnh và thành phần Toán học của chúng tôi đều sử dụng các đạo cụ kết xuất, chúng tôi chỉ có thể bao bọc Nhà cung cấp với thành phần Toán học của chúng tôi. Bất cứ khi nào trạng thái thành phần Toán học của chúng tôi thay đổi, nó sẽ chuyển các giá trị mới cho Nhà cung cấp, sau đó phát đối tượng này cho bất kỳ Người tiêu dùng nào. Thành phần tiêu dùng của chúng tôi trông như thế này:

import React from 'react'

const MathContext = React.createContext()

class MathContextProvider extends React.Component {
  render() {
    return (
      <Math
        render={values => (
          <MathContext.Provider value={values}>
            {this.props.children}
          </MathContext.Provider>
        )}
      />
    )
  }
}
class MathContextConsumer extends React.Component {
  render() {
    return (
      <MathContext.Consumer>
        {mathProps => (
          ... // consume the mathProps object
        )}
      </MathContext.Consumer>
    )
  }
}

Xem một ví dụ hoạt động đầy đủ ở đây  (CodeSandbox)

Tóm lược

Điều này có nghĩa là bạn nên loại bỏ mã Redux cũ hoặc HoCs và chuyển đổi tất cả chúng để kết xuất đạo cụ? Vâng, không, nếu mã của bạn đang hoạt động và bạn hài lòng với nó thì không có lý do gì để thay đổi. Mẫu hoặc thư viện tốt nhất phụ thuộc hoàn toàn vào trường hợp sử dụng!

Làm thế nào để kết xuất đạo cụ so với Redux khi quản lý trạng thái? Một triết lý cốt lõi trong Redux là có một nguồn sự thật duy nhất. Điều này làm cho việc đọc và cập nhật cho các cửa hàng toàn cầu có thể dự đoán được. Đây có thể là một lợi thế rất lớn trong kết xuất máy chủ khi tạo trạng thái ban đầu để hydrat hóa ứng dụng hoặc tối ưu hóa để chỉ kết xuất lại khi trạng thái thay đổi. Tuy nhiên, khả năng dự đoán đi kèm với một mức độ lễ nhất định xung quanh việc viết mã này. Mặt khác, kết xuất các đạo cụ xử lý đọc và cập nhật trạng thái với trạng thái cục bộ React. Sẽ khó hơn khi áp dụng tối ưu hóa kết xuất vào đạo cụ kết xuất so với Redux. Nó trì hoãn mọi tối ưu hóa cho trình điều hòa React để xử lý các bản cập nhật và kết xuất lại cho phù hợp. Đạo cụ kết xuất không yêu cầu thêm thư viện hoặc cấu hình để thêm vào ứng dụng.

Nếu bạn hài lòng với HoCs thì sao? Tại sao bạn nên xem xét sử dụng đạo cụ render? Chà, HoC có một số  cảnh báo  mà một nhà phát triển React dày dạn chắc chắn gặp phải. Hãy cẩn thận yêu cầu bạn xử lý các trường hợp cạnh phát sinh xung quanh việc bọc thành phần ban đầu bằng một thành phần container. Những vấn đề này được tránh với đạo cụ kết xuất bởi vì bạn chỉ truyền đạo cụ. Một điều tuyệt vời khác là HoC có thể được xây dựng với các thành phần kết xuất. Kiểm tra bộ định tuyến phản ứng  vớiRouter  HoC để biết ví dụ về giao diện của nó.

Đạo cụ kết xuất cung cấp một cấu trúc linh hoạt để xây dựng các thành phần trong React bằng cách tách trạng thái và chế độ xem. Chúng có thể được sử dụng để xây dựng HoC cũng như các thành phần Bối cảnh. Họ không yêu cầu thêm thư viện hoặc cấu hình để thêm vào các ứng dụng React hiện có vì họ sử dụng trạng thái cục bộ.

Tôi rất khuyên bạn nên khám phá đạo cụ kết xuất với các dự án của riêng bạn ngày hôm nay! Nếu bạn quan tâm đến việc tìm hiểu thêm về đạo cụ kết xuất, hãy xem danh sách các  dự án nguồn mở tuyệt vời này bằng cách sử dụng đạo cụ kết xuất !

|