深入淺出 React Hooks

原文鏈接:https://www.v2ex.com/t/570176#reply10

React Hooks 是什么?

Hooks 顧名思義,字面意義上來說就是 React 鉤子的概念。通過一個 case 我們對 React Hooks 先有一個第一印象。

假設現在要實現一個計數器的組件。如果使用組件化的方式,我們需要做的事情相對更多一些,比如說聲明 state,編寫計數器的方法等,而且需要理解的概念可能更多一些,比如 Javascript 的類的概念,this 上下文的指向等。

示例

import React, { Component } from 'react';
import ReactDOM from 'react-dom';

class Counter extends React.Component {
  state = {
    count: 0
  }

  countUp = () => {
    const { count } = this.state;
    this.setState({ count: count + 1 });
  }

  countDown = () => {
    const { count } = this.state;
    this.setState({ count: count - 1 });
  }

  render() {
    const { count } = this.state;
    return (
      <div>
        <button onClick={this.countUp}>+</button>
        <h1>{count}</h1>
        <button onClick={this.countDown}>-</button>
      </div>
    )
  }
}

ReactDOM.render(<Counter />, document.getElementById('root'));
復制代碼

使用 React Hooks,我們可以這么寫。

示例

import React, { useState } from 'react';
import ReactDOM from 'react-dom';

function Counter() {
  const [count, setCount] = useState(0);
  return (
    <div>
      <button onClick={() => setCount(count + 1)}>+</button>
      <h1>{count}</h1>
      <button onClick={() => setCount(count - 1)}>-</button>
    </div>
  )
}

ReactDOM.render(<Counter />, document.getElementById('root'));
復制代碼

通過上面的例子,顯而易見的是 React Hooks 提供了一種簡潔的、函數式(FP)的程序風格,通過純函數組件和可控的數據流來實現狀態到 UI 的交互(MVVM)。

Hooks API

useState

useState 是最基本的 API,它傳入一個初始值,每次函數執行都能拿到新值。

import React, { useState } from 'react';
import ReactDOM from 'react-dom';

function Counter() {
  const [count, setCount] = useState(0);
  return (
    <div>
      <button onClick={() => setCount(count + 1)}>+</button>
      <h1>{count}</h1>
      <button onClick={() => setCount(count - 1)}>-</button>
    </div>
  )
}

ReactDOM.render(<Counter />, document.getElementById('root'));
復制代碼

需要注意的是,通過 useState 得到的狀態 count,在 Counter 組件中的表現為一個常量,每一次通過 setCount 進行修改后,又重新通過 useState 獲取到一個新的常量。

useReducer

useReducer 和 useState 幾乎是一樣的,需要外置外置 reducer (全局),通過這種方式可以對多個狀態同時進行控制。仔細端詳起來,其實跟 redux 中的數據流的概念非常接近。

import { useState, useReducer } from 'react';
import ReactDOM from 'react-dom';

function reducer(state, action) {
  switch (action.type) {
    case 'up':
      return { count: state.count + 1 };
    case 'down':
      return { count: state.count - 1 };
  }
}

function Counter() {
  const [state, dispatch] = useReducer(reducer, { count: 1 })
  return (
    <div>
      {state.count}
      <button onClick={() => dispatch({ type: 'up' })}>+</button>
      <button onClick={() => dispatch({ type: 'down' })}>+</button>
    </div>
  );
}

ReactDOM.render(<Counter />, document.getElementById('root'));
復制代碼

useEffect

一個至關重要的 Hooks API,顧名思義,useEffect 是用于處理各種狀態變化造成的副作用,也就是說只有在特定的時刻,才會執行的邏輯。

import { useState, useEffect } from 'react';
import ReactDOM from 'react-dom';

function Example() {
  const [count, setCount] = useState(0);

  // => componentDidMount/componentDidUpdate
  useEffect(() => {
    // update 
    document.title = `You clicked ${count} times`;
    // => componentWillUnMount
    return function cleanup() {
        document.title = 'app';
    }
  }, [count]);

  return (
    <div>
      <p>You clicked {count} times</p>
      <button onClick={() => setCount(count + 1)}>
        Click me
      </button>
    </div>
  );
}

ReactDOM.render(<Example />, document.getElementById('root'));
復制代碼

useMemo

useMemo 主要用于渲染過程優化,兩個參數依次是計算函數(通常是組件函數)和依賴狀態列表,當依賴的狀態發生改變時,才會觸發計算函數的執行。如果沒有指定依賴,則每一次渲染過程都會執行該計算函數。

const memoizedValue = useMemo(() => computeExpensiveValue(a, b), [a, b]);
復制代碼
import { useState, useMemo } from 'react';
import ReactDOM from 'react-dom';

function Time() {
    return <p>{Date.now()}</p>;
}

function Counter() {
  const [count, setCount] = useState(0);

  const memoizedChildComponent = useMemo((count) => {
    return <Time />;
  }, [count]);

  return (
    <div>
      <h1>{count}</h1>
      <button onClick={() => setCount(count + 1)}>+</button>
      <div>{memoizedChildComponent}</div>
    </div>
  );
}

ReactDOM.render(<Counter />, document.getElementById('root'));
復制代碼

useContext

context 是在外部 create ,內部 use 的 state,它和全局變量的區別在于,如果多個組件同時 useContext,那么這些組件都會 rerender,如果多個組件同時 useState 同一個全局變量,則只有觸發 setState 的當前組件 rerender。

示例-未使用 useContext

import { useState, useContext, createContext } from 'react';
import ReactDOM from 'react-dom';

// 1\. 使用 createContext 創建上下文
const UserContext = new createContext();

// 2\. 創建 Provider
const UserProvider = props => {
  let [username, handleChangeUsername] = useState('');
  return (
    <UserContext.Provider value={{ username, handleChangeUsername }}>
      {props.children}
    </UserContext.Provider>
  );
};

// 3\. 創建 Consumer
const UserConsumer = UserContext.Consumer;

// 4\. 使用 Consumer 包裹組件
const Pannel = () => (
  <UserConsumer>
    {({ username, handleChangeUsername }) => (
      <div>
        <div>user: {username}</div>
        <input onChange={e => handleChangeUsername(e.target.value)} />
      </div>
    )}
  </UserConsumer>
);

const Form = () => <Pannel />;

const App = () => (
  <div>
    <UserProvider>
      <Form />
    </UserProvider>
  </div>
);

ReactDOM.render(<App />, document.getElementById('root'));
復制代碼

示例 - 使用 useContext

import { useState, useContext, createContext } from 'react';
import ReactDOM from 'react-dom';

// 1\. 使用 createContext 創建上下文
const UserContext = new createContext();

// 2\. 創建 Provider
const UserProvider = props => {
  let [username, handleChangeUsername] = useState('');
  return (
    <UserContext.Provider value={{ username, handleChangeUsername }}>
      {props.children}
    </UserContext.Provider>
  );
};

const Pannel = () => {
  const { username, handleChangeUsername } = useContext(UserContext); // 3\. 使用 Context
  return (
    <div>
      <div>user: {username}</div>
      <input onChange={e => handleChangeUsername(e.target.value)} />
    </div>
  );
};

const Form = () => <Pannel />;

const App = () => (
  <div>
    <UserProvider>
      <Form />
    </UserProvider>
  </div>
);

ReactDOM.render(<App />, document.getElementById('root'));
復制代碼

useRef

useRef 返回一個可變的 ref 對象,其 .current 屬性初始化為傳遞的參數(initialValue)。返回的對象將持續整個組件的生命周期。事實上 useRef 是一個非常有用的 API,許多情況下,我們需要保存一些改變的東西,它會派上大用場的。

示例

function TextInputWithFocusButton() {
  const inputEl = useRef(null);
  const onButtonClick = () => {
    // `current` points to the mounted text input element
    inputEl.current.focus();
  };
  return (
    <>
      <input ref={inputEl} type="text" />
      <button onClick={onButtonClick}>Focus the input</button>
    </>
  );
}
復制代碼

React 狀態共享方案

說到狀態共享,最簡單和直接的方式就是通過 props 逐級進行狀態的傳遞,這種方式耦合于組件的父子關系,一旦組件嵌套結構發生變化,就需要重新編寫代碼,維護成本非常昂貴。隨著時間的推移,官方推出了各種方案來解決狀態共享和代碼復用的問題。

Mixins

React 中,只有通過 createClass 創建的組件才能使用 mixins。這種高耦合,依賴難以控制,復雜度高的方式隨著 ES6 的浪潮逐漸淡出了歷史舞臺。

HOC

高階組件源于函數式編程,由于 React 中的組件也可以視為函數(類),因此天生就可以通過 HOC 的方式來實現代碼復用。可以通過屬性代理和反向繼承來實現,HOC 可以很方便的操控渲染的結果,也可以對組件的 props / state 進行操作,從而可以很方便的進行復雜的代碼邏輯復用。

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

// 屬性代理
class Show extends React.Component {
  static propTypes = {
    children: PropTypes.element,
    visible: PropTypes.bool,
  };

  render() {
    const { visible, children } = this.props;
    return visible ? children : null;
  }
}

// 反向繼承
function Show2(WrappedComponent) {
  return class extends WrappedComponent {
    render() {
      if (this.props.visible === false) {
        return null;
      } else {
        return super.render();
      }
    }
  }
}

function App() {
    return (
    <Show visible={Math.random() > 0.5}>hello</Show>
  );
}
復制代碼

Redux 中的狀態復用是一種典型的 HOC 的實現,我們可以通過 compose 來將數據組裝到目標組件中,當然你也可以通過裝飾器的方式進行處理。

import React from 'react';
import { connect } from 'react-redux';

// use decorator
@connect(state => ({ name: state.user.name }))
class App extends React.Component{
  render() {
        return <div>hello, {this.props.name}</div>
  }
}

// use compose
connect((state) => ({ name: state.user.name }))(App);
復制代碼

Render Props

顯而易見,renderProps 就是一種將 render 方法作為 props 傳遞到子組件的方案,相比 HOC 的方案,renderProps 可以保護原有的組件層次結構。

import React from 'react';
import ReactDOM from 'react-dom';
import PropTypes from 'prop-types';

// 與 HOC 不同,我們可以使用具有 render prop 的普通組件來共享代碼
class Mouse extends React.Component {
  static propTypes = {
    render: PropTypes.func.isRequired
  }

  state = { x: 0, y: 0 };

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

  render() {
    return (
      <div style={{ height: '100%' }} onMouseMove={this.handleMouseMove}>
        {this.props.render(this.state)}
      </div>
    );
  }
}

function App() {
  return (
    <div style={{ height: '100%' }}>
      <Mouse render={({ x, y }) => (
          // render prop 給了我們所需要的 state 來渲染我們想要的
          <h1>The mouse position is ({x}, {y})</h1>
        )}/>
    </div>
  );
}

ReactDOM.render(<App/>, document.getElementById('root'));
復制代碼

Hooks

通過組合 Hooks API 和 React 內置的 Context,從前面的示例可以看到通過 Hook 讓組件之間的狀態共享更清晰和簡單。

React Hooks 設計理念

基本原理

function FunctionalComponent () {
  const [state1, setState1] = useState(1);
  const [state2, setState2] = useState(2);
  const [state3, setState3] = useState(3);
}
復制代碼
{
  memoizedState: 'foo',
  next: {
    memoizedState: 'bar',
    next: {
      memoizedState: 'bar',
      next: null
    }
  }
}
復制代碼

函數式貫徹到底

capture props

函數組件天生就是支持 props 的,基本用法上和 class 組件沒有太大的差別。需要注意的兩個區別是:

  • class 組件 props 掛載在 this 上下文中,而函數式組件通過形參傳入;
  • 由于掛載位置的差異,class 組件中如果 this 發生了變化,那么 this.props 也會隨之改變;而在函數組件里 props 始終是不可變的,因此遵守 capture value 原則(即獲取的值始終是某一時刻的),Hooks 也遵循這個原則。

通過一個示例來理解一下 capture value,我們可以通過 useRef 來規避 capture value,因為 useRef 是可變的。

state

class 組件 函數組件
創建狀態 this.state = {} useState, useReducer
修改狀態 this.setState() set function
更新機制 異步更新,多次修改合并到上一個狀態,產生一個副本 同步更新,直接修改為目標狀態
狀態管理 一個 state 集中式管理多個狀態 多個 state,可以通過 useReducer 進行狀態合并(手動)
性能 如果 useState 初始化狀態需要通過非常復雜的計算得到,請使用函數的聲明方式,否則每次渲染都會重復執行

生命周期

  • componentDidMount / componentDidUpdate / componentWillUnMount

useEffect 在每一次渲染都會被調用,稍微包裝一下就可以作為這些生命周期使用;

  • shouldComponentUpdate

通常我們優化組件性能時,會優先采用純組件的方式來減少單個組件的渲染次數。

class Button extends React.PureComponent {}
復制代碼

React Hooks 中可以采用 useMemo 代替,可以實現僅在某些數據變化時重新渲染組件,等同于自帶了 shallowEqual 的 shouldComponentUpdate。

強制渲染 forceUpdate

由于默認情況下,每一次修改狀態都會造成重新渲染,可以通過一個不使用的 set 函數來當成 forceUpdate。

const forceUpdate = () => useState(0)[1];
復制代碼

實現原理

基于 Hooks,增強 Hooks

來一套組合拳吧!

由于每一個 Hooks API 都是純函數的概念,使用時更關注輸入 (input) 和輸出 (output),因此可以更好的通過組裝函數的方式,對不同特性的基礎 Hooks API 進行組合,創造擁有新特性的 Hooks。

  • useState 維護組件狀態
  • useEffect 處理副作用
  • useContext 監聽 provider 更新變化

useDidMount

import { useEffect } from 'react';

const useDidMount = fn => useEffect(() => fn && fn(), []);

export default useDidMount;
復制代碼

useDidUpdate

import { useEffect, useRef } from 'react';

const useDidUpdate = (fn, conditions) => {
  const didMoutRef = useRef(false);
  useEffect(() => {
    if (!didMoutRef.current) {
      didMoutRef.current = true;
      return;
    }
    // Cleanup effects when fn returns a function
    return fn && fn();
  }, conditions);
};

export default useDidUpdate
復制代碼

useWillUnmount

在講到 useEffect 時已經提及過,其允許返回一個 cleanup 函數,組件在取消掛載時將會執行該 cleanup 函數,因此 useWillUnmount 也能輕松實現~

import { useEffect } from 'react';

const useWillUnmount = fn => useEffect(() => () => fn && fn(), []);

export default useWillUnmount;
復制代碼

useHover

示例

// lib/onHover.js
import { useState } from 'react';

const useHover = () => {
  const [hovered, set] = useState(false);
  return {
    hovered,
    bind: {
      onMouseEnter: () => set(true),
      onMouseLeave: () => set(false),
    },
  };
};

export default useHover;
復制代碼
import { useHover } from './lib/onHover.js';

function Hover() {
  const { hovered, bind } = useHover();
  return (
    <div>
      <div {...bind}>
        hovered:
        {String(hovered)}
      </div>
    </div>
  );
}
復制代碼

useField

示例

// lib/useField.js

import { useState } from 'react';

const useField = (initial) => {
  const [value, set] = useState(initial);

  return {
    value,
    set,
    reset: () => set(initial),
    bind: {
      value,
      onChange: e => set(e.target.value),
    },
  };
}

export default useField;
復制代碼
import { useField } from 'lib/useField';

function Input {
  const { value, bind } = useField('Type Here...');

  return (
    <div>
      input text:
      {value}
      <input type="text" {...bind} />
    </div>
  );
}

function Select() {
  const { value, bind } = useField('apple')
  return (
    <div>
      selected:
      {value}
      <select {...bind}>
        <option value="apple">apple</option>
        <option value="orange">orange</option>
      </select>
    </div>
  );
}
復制代碼

注意事項

  • Hook 的使用范圍:函數式的 React 組件中、自定義的 Hook 函數里;
  • Hook 必須寫在函數的最外層,每一次 useState 都會改變其下標 (cursor),React 根據其順序來更新狀態;
  • 盡管每一次渲染都會執行 Hook API,但是產生的狀態 (state) 始終是一個常量(作用域在函數內部);

結語

React Hooks 提供為狀態管理提供了新的可能性,盡管我們可能需要額外去維護一些內部的狀態,但是可以避免通過 renderProps / HOC 等復雜的方式來處理狀態管理的問題。Hooks 帶來的好處如下:

  • 更細粒度的代碼復用,并且不會產生過多的副作用
  • 函數式編程風格,代碼更簡潔,同時降低了使用和理解門檻
  • 減少組件嵌套層數
  • 組件數據流向更清晰

事實上,通過定制各種場景下的自定義 Hooks,能讓我們的應用程序更方便和簡潔,組件的層次結構也能保證完好,還有如此令人愉悅的函數式編程風格,Hooks 在 React 16.8.0 版本已經正式發布穩定版本,現在開始用起來吧!!!

參考資料

?著作權歸作者所有,轉載或內容合作請聯系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發布,文章內容僅代表作者本人觀點,簡書系信息發布平臺,僅提供信息存儲服務。
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市,隨后出現的幾起案子,更是在濱河造成了極大的恐慌,老刑警劉巖,帶你破解...
    沈念sama閱讀 230,002評論 6 542
  • 序言:濱河連續發生了三起死亡事件,死亡現場離奇詭異,居然都是意外死亡,警方通過查閱死者的電腦和手機,發現死者居然都...
    沈念sama閱讀 99,400評論 3 429
  • 文/潘曉璐 我一進店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人,你說我怎么就攤上這事。” “怎么了?”我有些...
    開封第一講書人閱讀 178,136評論 0 383
  • 文/不壞的土叔 我叫張陵,是天一觀的道長。 經常有香客問我,道長,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 63,714評論 1 317
  • 正文 為了忘掉前任,我火速辦了婚禮,結果婚禮上,老公的妹妹穿的比我還像新娘。我一直安慰自己,他們只是感情好,可當我...
    茶點故事閱讀 72,452評論 6 412
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著,像睡著了一般。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發上,一...
    開封第一講書人閱讀 55,818評論 1 328
  • 那天,我揣著相機與錄音,去河邊找鬼。 笑死,一個胖子當著我的面吹牛,可吹牛的內容都是我干的。 我是一名探鬼主播,決...
    沈念sama閱讀 43,812評論 3 446
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了?” 一聲冷哼從身側響起,我...
    開封第一講書人閱讀 42,997評論 0 290
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后,有當地人在樹林里發現了一具尸體,經...
    沈念sama閱讀 49,552評論 1 335
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內容為張勛視角 年9月15日...
    茶點故事閱讀 41,292評論 3 358
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發現自己被綠了。 大學時的朋友給我發了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 43,510評論 1 374
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖,靈堂內的尸體忽然破棺而出,到底是詐尸還是另有隱情,我是刑警寧澤,帶...
    沈念sama閱讀 39,035評論 5 363
  • 正文 年R本政府宣布,位于F島的核電站,受9級特大地震影響,放射性物質發生泄漏。R本人自食惡果不足惜,卻給世界環境...
    茶點故事閱讀 44,721評論 3 348
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧,春花似錦、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 35,121評論 0 28
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至,卻和暖如春,著一層夾襖步出監牢的瞬間,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 36,429評論 1 294
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人。 一個月前我還...
    沈念sama閱讀 52,235評論 3 398
  • 正文 我出身青樓,卻偏偏與公主長得像,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當晚...
    茶點故事閱讀 48,480評論 2 379

推薦閱讀更多精彩內容