React 深入系列7:React 常用模式

艾特老幹部發表於2018-05-22

React 深入系列,深入講解了React中的重點概念、特性和模式等,旨在幫助大家加深對React的理解,以及在專案中更加靈活地使用React。

本篇是React深入系列的最後一篇,將介紹開發React應用時,經常用到的模式,這些模式並非都有官方名稱,所以有些模式的命名並不一定準確,請讀者主要關注模式的內容。

1. 受控元件

React 元件的資料流是由state和props驅動的,但對於input、textarea、select等表單元素,因為可以直接接收使用者在介面上的輸入,所以破壞了React中的固有資料流。為了解決這個問題,React引入了受控元件,受控元件指input等表單元素顯示的值,仍然是通過元件的state獲取的,而不是直接顯示使用者在介面上的輸入資訊。

受控元件的實現:通過監聽表單元素值的變化,改變元件state,根據state顯示元件最終要展示的值。一個簡單的例子如下:

class NameForm extends React.Component {
  constructor(props) {
    super(props);
    this.state = {value: ``};

    this.handleChange = this.handleChange.bind(this);
    this.handleSubmit = this.handleSubmit.bind(this);
  }

  handleChange(event) {
    this.setState({value: event.target.value});
  }

  handleSubmit(event) {
    alert(`A name was submitted: ` + this.state.value);
    event.preventDefault();
  }

  render() {
    return (
      <form onSubmit={this.handleSubmit}>
        <label>
          Name:
          <input type="text" value={this.state.value} onChange={this.handleChange} />
        </label>
        <input type="submit" value="Submit" />
      </form>
    );
  }
}

和受控元件對應的概念是非受控元件,非受控元件通過ref獲取表單元素的值,在一些場景下有著特有的作用(如設定表單元素的焦點)。

2. 容器元件

容器元件和展示元件是一組對應的概念,關注的是元件邏輯和元件展示的分離。邏輯由容器元件負責,展示元件聚焦在檢視層的展現上。在React 深入系列2:元件分類中已對容器元件和展示元件作過詳細介紹,這裡不再贅述。

3. 高階元件

高階元件是一種特殊的函式,接收元件作為輸入,輸出一個新的元件。高階元件的主要作用是封裝元件的通用邏輯,實現邏輯的複用。在React 深入系列6:高階元件中已經詳細介紹過高階元件,這裡也不再贅述。

4. Children傳遞

首先,這個模式的命名可能並不恰當。這個模式中,藉助React 元件的children屬性,實現元件間的解耦。常用在一個元件負責UI的框架,框架內部的元件可以靈活替換的場景。

一個示例:

// ModalDialog.js
export default function ModalDialog({ children }) {
  return <div className="modal-dialog">{ children }</div>;
};

// App.js
render() {
  <ModalDialog>
    <SomeContentComp/>
  </ModalDialog>
}

ModalDialog元件是UI的框,框內元件可以靈活替換。

5. Render Props

Render Props是把元件部分的渲染邏輯封裝到一個函式中,利用元件的props接收這個函式,然後在元件內部呼叫這個函式,執行封裝的渲染邏輯。

看一個官方的例子:

class Cat extends React.Component {
  render() {
    const mouse = this.props.mouse;
    return (
      <img src="/cat.jpg" style={{ position: `absolute`, left: mouse.x, top: mouse.y }} />
    );
  }
}

class Mouse extends React.Component {
  constructor(props) {
    super(props);
    this.handleMouseMove = this.handleMouseMove.bind(this);
    this.state = { x: 0, y: 0 };
  }

  handleMouseMove(event) {
    this.setState({
      x: event.clientX,
      y: event.clientY
    });
  }

  render() {
    return (
      <div style={{ height: `100%` }} onMouseMove={this.handleMouseMove}>
        {/*
        *   Mouse元件並不知道應該如何渲染這部分內容,
        *   這部分渲染邏輯是通過props的render屬性傳遞給Mouse元件 
        */}
        {this.props.render(this.state)}
      </div>
    );
  }
}

class MouseTracker extends React.Component {
  render() {
    return (
      <div>
        <h1>Move the mouse around!</h1>
        <Mouse render={mouse => (
          <Cat mouse={mouse} />
        )}/>
      </div>
    );
  }
}

Mouse監聽滑鼠的移動,並將滑鼠位置儲存到state中。但Mouse元件並不知道最終要渲染出的內容,需要呼叫this.props.render方法,執行渲染邏輯。本例中,Cat元件會渲染到滑鼠移動到的位置,但完全可以使用其他效果來跟隨滑鼠的移動,只需更改render方法即可。由此可見,Mouse元件只關注滑鼠位置的移動,而跟隨滑鼠移動的介面效果,由使用Mouse的元件決定。這是一種基於切面程式設計的思想(瞭解後端開發的同學應該比較熟悉)。

使用這種模式,一般習慣將封裝渲染邏輯的函式賦值給一個命名為render的元件屬性(如本例所示),但這並不是必需,你也可以使用其他的屬性命名。

這種模式的變種形式是,直接使用React元件自帶的children屬性傳遞。上面的例子改寫為:

class Mouse extends React.Component {
  // 省略

  render() {
    return (
      <div style={{ height: `100%` }} onMouseMove={this.handleMouseMove}>
        {/*
        *   Mouse元件並不知道應該如何渲染這部分內容,
        *   這部分渲染邏輯是通過props的children屬性傳遞給Mouse元件 
        */}
        {this.props.children(this.state)}
      </div>
    );
  }
}
Mouse.propTypes = {
  children: PropTypes.func.isRequired
};


class MouseTracker extends React.Component {
  render() {
    return (
      <div>
        <h1>Move the mouse around!</h1>
        <Mouse>
          {mouse => (
            <Cat mouse={mouse} />
          )}
        </Mouse>
      </div>
    );
  }
}

注意children的賦值方式。

React Router 和 React-Motion 這兩個庫都使用到了Render Props模式。很多場景下,Render Props實現的功能也可以通過高階元件實現。本例也可以用高階元件實現,請讀者自行思考。

6. Provider元件

這種模式藉助React的context,把元件需要使用的資料儲存到context,並提供一個高階元件從context中獲取資料。

一個例子:

先建立MyProvider,將共享資料儲存到它的context中,MyProvider一般作為最頂層的元件使用,從而確保其他元件都能獲取到context中的資料:

import React from "react";
import PropTypes from "prop-types";

const contextTypes = {
  sharedData: PropTypes.shape({
    a: PropTypes.bool,
    b: PropTypes.string,
    c: PropTypes.object
  })
};

export class MyProvider extends React.Component {

  static childContextTypes = contextTypes;

  getChildContext() {
    // 假定context中的資料從props中獲取
    return { sharedData: this.props.sharedData };
  }

  render() {
    return this.props.children;
  }
}

然後建立高階元件connectData,用於從context中獲取所需資料:

export const connectData = WrappedComponent =>
  class extends React.Component {
    static contextTypes = contextTypes;

    render() {
      const { props, context } = this;
      return <WrappedComponent {...props} {...context.sharedData} />;
    }
  };

最後在應用中使用:

const SomeComponentWithData = connectData(SomeComponent)

const sharedData = {
    a: true,
    b: "react",
    c: {}
};

class App extends Component {
  render() {
    return (
      <MyProvider sharedData={sharedData}>
        <SomeComponentWithData />
      </MyProvider>
    );
  }
}

Provider元件模式非常實用,在react-redux、mobx-react等庫中,都有使用到這種模式。

React 深入系列文章到此完結,希望能幫助大家更加深入的理解React,更加純熟的應用React。


我的新書《React進階之路》已上市,請大家多多支援!
連結:京東 噹噹

圖片描述

相關文章