react高級特性

用了那麼久的react, 竟不知道到原來react有那麼多高級特性. 假期沒事幹, 試用了下一些react高級特性. 下爲試用記錄.javascript

概覽

特性 特性描述 使用場景
代碼分割 提供異步組件,實現拆包 須要優化包體積時使用
Context 跨層級傳遞數據 優化多層級傳遞props問題
PropTypes 進行類型檢查 能夠對props的類型加上校驗器 但願及早暴露props類型錯誤
錯誤邊界 提供不過子組件錯誤和在錯誤返回指定state的生命週期 但願在渲染錯誤時提供降級UI或上報錯誤
Fragments 提供在一個組件返回多個元素的能力 但願在一個組件返回多個元素
Portals 提供將元素渲染到父元素以外的能力 Toast, Modal等
forwardRef 轉發傳進來的ref 但願將外部傳遞的ref轉移到別的元素上,而不是本身

代碼分割

將一個龐大的單頁應用打包成一個龐大的js, 首屏加載可能會很是糟糕, 這時可能會考慮作代碼分割, 即根據模塊或者路由分開打包js, 異步按需加載組件.css

藉助webpack和一些異步組件庫(好比react-loadable, 也能夠本身實現異步組件)就能很方便的實現這一點. 好比像下面這樣:html

// router.js
import React from 'react';
import Loadable from 'react-loadable';

const Loading = () => <div>Loading...</div>;

///////////////頁面路由配置////////////////

const Routers = {
    // 首頁
    '/': Loadable({
        loader: () => import(/* webpackChunkName: "index" */'./pages/Index.jsx'),
        loading: Loading,
      }),
    // 首頁
    '/index': Loadable({
        loader: () => import(/* webpackChunkName: "index" */'./pages/Index.jsx'),
        loading: Loading,
    }),
    '/404': Loadable({
        loader: () => import(/* webpackChunkName: "404" */'./pages/404/index'),
        loading: Loading, 
    })
}

export default Routers;

// App.js
import React, { Component } from 'react';
import { BrowserRouter as Router, Route, Link, Switch } from "react-router-dom";

import Routers from './router';

class App extends Component {
  componentDidMount() {

  }
  render() {
    return (
      <Router>
          <Switch>
            <Route path="/" exact component={Routers["/"]} />
            <Route path="/index" exact component={Routers["/index"]} />
            <Route component={Routers['/404']} />
          </Switch>
      </Router>
    );
  }
}

export default App;

咱們直接使用Loadable建立異步組件, 在合適的時候使用, webpack會幫我作好代碼分割, Loadable能夠幫咱們維護好異步組件的狀態, 而且可以支持定義加載中的組件. 上邊demo完整版參見web-test.java

其實, react已經原生提供了異步組件的支持, 其使用和Loadable大致相同, 可是看起來會更加優雅.node

import { BrowserRouter as Router, Route, Switch } from 'react-router-dom';
import React, { Suspense, lazy } from 'react';

const Home = lazy(() => import(/* webpackChunkName: "home" */'./pages/Home'));
const About = lazy(() => import(/* webpackChunkName: "about" */'./pages/About'));

const App = () => (
  <Router>
    <Suspense fallback={<div>Loading...</div>}>
      <Switch>
        <Route exact path="/" component={Home} />
        <Route path="/about" component={About} />
      </Switch>
    </Suspense>
  </Router>
);

export default App;

這裏咱們使用React.lazy方法建立異步組件, 和Loadable相似, 也是使用了import方法, webpack會幫咱們處理好這個import. 不一樣的是他並不支持定義loading, loading的自定義可使用Suspense組件. 在其fallback中能夠建立自定義的loading組件. 這個demo的完整版可參考react-demo.react

Context

第一次接觸Context是看redux源碼發現的, Context特性是redux實現的核心之一. Context可讓很深的props的傳遞變得簡單優雅, 再也不須要逐級傳遞.webpack

假設有以下組件, D組件須要拿A組件中數據, 可能須要從A經過props 傳到B, 從B傳到C, 從C 在經過props傳到D. 很是麻煩.git

<A>
  <B>
    <C>
      <D>
      </D>
    </C>
  </B>
</A>

看一下經過Context特性如何實現.github

// MyContext.js
import React from 'react';

const MyContext = React.createContext("我是來自A的默認值");
export default MyContext;

// A.js
import React from 'react';
import B from './B';
import MyContext from './MyContext';
export default class A extends React.Component {
    constructor(props) {
        super(props);
    }
    render() {
        return (
            <div>
                <MyContext.Provider value={'我是來自A的數據'}>
                    <B />
                </MyContext.Provider>
            </div>
        )
    }
}

// B.js
import React from 'react';
import C from './C';
class B extends React.Component {
    render() {
        return (
            <div>
                <h3>我是B組件</h3>
                <C />
            </div>
        );
    }
}
export default B;

// C.js
import React from 'react';
import MyContext from './MyContext';
import D from './D';

function C() {
    return (
        <MyContext.Consumer>
            {
                (value) => (
                    <div>
                        <h3>我是C組件</h3>
                        <div>我是來自A的數據: {value}</div>
                        <D />
                    </div>
                )
            }
        </MyContext.Consumer>
    )
}

export default C;

// D.js
import React from 'react';
import MyContext from './MyContext';

class D extends React.Component {
    render() {
        let context = this.context;
        return (
            <div>
                <h3>我是D組件</h3>
                <div>我拿到了A中傳遞過來的數據</div>
                {context}
            </div>
        );
    }
}

D.contextType = MyContext;

export default D;

能夠看到在C組件和D組件沒有經過任何props傳遞就拿到了A中的數據. 這個demo的完整版可參考react-demo. 這個例子可能看起來直接將須要共享的變量放到全局就能夠了, 可是放到全局的當他變動後無法setState從新渲染, 而Context中的數據能夠經過setState引發從新渲染.web

從上邊的Demo來看, Context的使用很是簡單

  1. 使用React.createContext()建立Context
  2. 在父組件使用Context.Provider傳值
  3. 在子組件消費

    • 對於class組件能夠生命靜態變量contextType消費, 見D
    • 對於函數是組件, 能夠用Context.Consumer來消費, 見C

## 使用 PropTypes 進行類型檢查

一個被人調用的組件能夠經過PropTypes對props參數類型進行校驗, 將類型問題及早通知給調用方. 經過給組件指定靜態屬性propTypes並結合prop-types庫能夠很方便實現. prop-types須要單獨安裝.

以下是prop-types提供的一些校驗器, 來自react中文文檔

import PropTypes from 'prop-types';

MyComponent.propTypes = {
  // 你能夠將屬性聲明爲 JS 原生類型,默認狀況下
  // 這些屬性都是可選的。
  optionalArray: PropTypes.array,
  optionalBool: PropTypes.bool,
  optionalFunc: PropTypes.func,
  optionalNumber: PropTypes.number,
  optionalObject: PropTypes.object,
  optionalString: PropTypes.string,
  optionalSymbol: PropTypes.symbol,

  // 任何可被渲染的元素(包括數字、字符串、元素或數組)
  // (或 Fragment) 也包含這些類型。
  optionalNode: PropTypes.node,

  // 一個 React 元素。
  optionalElement: PropTypes.element,

  // 一個 React 元素類型(即,MyComponent)。
  optionalElementType: PropTypes.elementType,

  // 你也能夠聲明 prop 爲類的實例,這裏使用
  // JS 的 instanceof 操做符。
  optionalMessage: PropTypes.instanceOf(Message),

  // 你可讓你的 prop 只能是特定的值,指定它爲
  // 枚舉類型。
  optionalEnum: PropTypes.oneOf(['News', 'Photos']),

  // 一個對象能夠是幾種類型中的任意一個類型
  optionalUnion: PropTypes.oneOfType([
    PropTypes.string,
    PropTypes.number,
    PropTypes.instanceOf(Message)
  ]),

  // 能夠指定一個數組由某一類型的元素組成
  optionalArrayOf: PropTypes.arrayOf(PropTypes.number),

  // 能夠指定一個對象由某一類型的值組成
  optionalObjectOf: PropTypes.objectOf(PropTypes.number),

  // 能夠指定一個對象由特定的類型值組成
  optionalObjectWithShape: PropTypes.shape({
    color: PropTypes.string,
    fontSize: PropTypes.number
  }),
  
  // An object with warnings on extra properties
  optionalObjectWithStrictShape: PropTypes.exact({
    name: PropTypes.string,
    quantity: PropTypes.number
  }),   

  // 你能夠在任何 PropTypes 屬性後面加上 `isRequired` ,確保
  // 這個 prop 沒有被提供時,會打印警告信息。
  requiredFunc: PropTypes.func.isRequired,

  // 任意類型的數據
  requiredAny: PropTypes.any.isRequired,

  // 你能夠指定一個自定義驗證器。它在驗證失敗時應返回一個 Error 對象。
  // 請不要使用 `console.warn` 或拋出異常,由於這在 `onOfType` 中不會起做用。
  customProp: function(props, propName, componentName) {
    if (!/matchme/.test(props[propName])) {
      return new Error(
        'Invalid prop `' + propName + '` supplied to' +
        ' `' + componentName + '`. Validation failed.'
      );
    }
  },

  // 你也能夠提供一個自定義的 `arrayOf` 或 `objectOf` 驗證器。
  // 它應該在驗證失敗時返回一個 Error 對象。
  // 驗證器將驗證數組或對象中的每一個值。驗證器的前兩個參數
  // 第一個是數組或對象自己
  // 第二個是他們當前的鍵。
  customArrayProp: PropTypes.arrayOf(function(propValue, key, componentName, location, propFullName) {
    if (!/matchme/.test(propValue[key])) {
      return new Error(
        'Invalid prop `' + propFullName + '` supplied to' +
        ' `' + componentName + '`. Validation failed.'
      );
    }
  })
};

也能夠給props指定默認值

class Greeting extends React.Component {
  render() {
    return (
      <h1>Hello, {this.props.name}</h1>
    );
  }
}

// 指定 props 的默認值:
Greeting.defaultProps = {
  name: 'Stranger'
};

// 渲染出 "Hello, Stranger":
ReactDOM.render(
  <Greeting />,
  document.getElementById('example')
);

檢驗和默認值也能夠這樣寫

class Greeting extends React.Component {
  static defaultProps = {
    name: 'stranger'
  }
  static propTypes = {
    name: PropTypes.string,
  }
  render() {
    return (
      <div>Hello, {this.props.name}</div>
    )
  }
}

錯誤邊界

錯誤邊界是一種 React 組件,這種組件能夠捕獲並打印發生在其子組件樹任何位置的 JavaScript 錯誤,而且,它會渲染出備用 UI,而不是渲染那些崩潰了的子組件樹。錯誤邊界在渲染期間、生命週期方法和整個組件樹的構造函數中捕獲錯誤。

當子組件拋出錯誤時, 下邊的兩個生命週期會被觸發, 能夠在這裏邊處理錯誤, 顯示降級UI, 向服務端上報錯誤.

錯誤邊界組件核心生命週期以下

static getDerivedStateFromError()
componentDidCatch()

下面是個小demo

// index.js

import React from 'react';
import ErrorComponent from './ErrorComponent';

export default class Home extends React.Component {
    constructor(props) {
        super(props);
        this.state = {
            hasError: false,
        }
    }

    static getDerivedStateFromError() {
        console.log('getDerivedStateFromError');
        return { hasError: true };
    }
    componentDidCatch (error, info) {
        console.log('componentDidCatch');
        console.log({
            error,
            info,
        })
    }
    render() {
        if (this.state.hasError) {
            return <div>發生了某種錯誤</div>
        }
        return (
            <div>
                <h3>錯誤邊界測試</h3>
                <ErrorComponent />
            </div>
        )
    }
}

// ErrorComponent.js
import React from 'react';

export default class Home extends React.Component {
    state = {
        showError: false,
    }
    componentDidMount() {
    }
    click = () => {
        this.setState({
            showError: true,
        })
    }
    render() {
        if (this.state.showError) {
            throw new Error("拋出錯誤");
        }
        return (
            <div onClick={this.click}>我是產生錯誤的組件</div>
        )
    }
}

咱們能夠在componentDidCatch(error, info) 獲取錯誤信息, 錯誤信息error.message, 錯誤堆棧error.stack, 組件堆棧info.componentStack, 這些信息能夠顯示給用戶, 也能夠上報到服務器. 能夠在getDerivedStateFromError返回state, 渲染降級組件.

Fragments

Fragments解決了一個組件不能返回多個元素的問題, 沒有Fragments時一個組件無法返回多個元素, 因此咱們常常用個div包一下, 結果是增長了一個多餘的dom節點, 甚至產生不合法的dom, 好比下邊這樣的.

// 組件1
function Columns() {
    return (
        <div>
            <td>第一列</td>
            <td>第二列</td>
        </div>
    )
}
// 組件2
function Table() {
  return (
    <table>
      <tr>
        <Columns/>
      </tr>
    </table>
  )
}

由於無法返回多個元素, 因此在Columns組件中使用了div包裹兩個td, 而後在Table組件使用, 結果就產生了tr裏邊放td的錯誤結構. 使用Fragments特性能夠很方便的解決這個問題. 以下. 只要用個<React.Fragment>包裝就能夠了, 也能夠寫成<>something</>.

function Columns() {
    return (
        <React.Fragment>
            <td>第一列</td>
            <td>第二列</td>
        </React.Fragment>
    )
}

Portals

Portal 提供了一種將子節點渲染到存在於父組件之外的 DOM 節點的方案. portal 的典型使用場景是當父組件有 overflow: hidden 或 z-index 樣式時,但你須要子組件可以在視覺上「跳出」其容器。例如,對話框、懸浮卡以及提示框.

以下是一個toast組件 demo, 完整版參考react-demo

// Toast.js
import React from 'react';
import ReactDOM from 'react-dom';
import './Toast.css';

export default class Toast extends React.Component {
  constructor(props) {
    super(props);
    this.el = document.querySelector('body');
  }

  render() {
    return ReactDOM.createPortal(
      (
          <div className="toast">
              <div className="toast-inner">
                {this.props.text}
              </div>
          </div>
      ),
      this.el,
    );
  }
}
// PortalTest.js
import React from 'react';
import Toast from './Toast';
export default class PortalTest extends React.Component {
    render() {
        return (
            <div>
                <h1>PortalTest</h1>
                <Toast text="toast提示"/>
            </div>
        )
    }
}

結果如圖

Portals測試結果

能夠發現Toast這個組件不是在其父元素中, 而是跑到了咱們指望的body裏邊. 這樣無論父組件寫overflow:hidden;仍是其餘都不會影響到這個toast.

forwardRef

forwardRef是一種將ref轉移到子組件的方式.

forwardRef 主要有兩種使用場景

  • 但願對基礎組件作一些封裝, 可是但願基礎組件的實例的方法能被調用
  • 高階組件中但願ref指向被包裹的組件而不是外層組件
  1. 關於第一種場景

以前作ReactNative時有個FlatList組件, 但願對他封裝一層, 可是又但願調用方可使用ref或則FlatList的實例, 方便調用上邊的方法. 這時就能夠用forwardRef. 下面舉的是 input的例子, 咱們但願封裝一下, 但讓調用方仍然能夠經過ref獲取dom調用focus.

import React from 'react';

const LabelInput =
    React.forwardRef((props, ref) => {
        return <div>
            <label>{props.label}</label>
            <input ref={ref} className="input" style={{ border: '1px solid red' }} />
        </div>
    })

export default class Home extends React.Component {
    constructor(props) {
        super(props);
        this.ref = React.createRef();
    }

    focus = () => {
        try {
            this.ref.current.focus();
        } catch (e) {
            console.log(e);
        }
    }
    render() {
        return (
            <div>
                <h1>測試forwardRef</h1>
                <LabelInput ref={this.ref} label="手機號"/>
                <button onClick={this.focus}>點擊input能夠獲取焦點</button>
            </div>
        )
    }
}

在LabelInput組件裏邊將ref轉到了input上, 從而外邊的調用方能夠直接掉focus方法. 若是不作轉發, 那麼ref將指向div, 再要找到裏邊的input就比較麻煩了, 並且破壞了組件的封裝性.

  1. 關於第二種場景
import React from 'react';

function logProps(Component) {
    class LogProps extends React.Component {
        componentDidUpdate(prevProps) {
            console.log('old props:', prevProps);
            console.log('new props:', this.props);
        }

        render() {
            const { forwardedRef, ...rest } = this.props;

            // 將自定義的 prop 屬性 「forwardedRef」 定義爲 ref
            return <Component ref={forwardedRef} {...rest} />;
        }
    }

    // 注意 React.forwardRef 回調的第二個參數 「ref」。
    // 咱們能夠將其做爲常規 prop 屬性傳遞給 LogProps,例如 「forwardedRef」
    // 而後它就能夠被掛載到被 LogPros 包裹的子組件上。
    return React.forwardRef((props, ref) => {
        return <LogProps {...props} forwardedRef={ref} />;
    });
}

class InnerComp extends React.Component {
    render() {
        return <div id="InnerComp">
            被包裹的組件-text={this.props.text}
        </div>
    }
}

const Comp = logProps(InnerComp);

export default class Home extends React.Component {
    constructor(props) {
        super(props);
        this.ref = React.createRef();
    }
    click = () => {
        console.log(this.ref.current);
    }
    render() {
        return (
            <div>
                <h1>測試forwardRef</h1>
                <Comp ref={this.ref} text="測試" />
                <button onClick={this.click}>點擊打印ref</button>
            </div>
        )
    }
}

這裏點擊打印的是InnerComp組件, 若是去掉forwardRef則打印LogProps組件. 可見經過forwardRef能夠成功將ref傳遞到被包裹的組件.

注意 函數組件不能給ref, 只有class組件能夠. 測試發現的.

相關文章
相關標籤/搜索