React Hooks 使用詳解

暖生發表於2019-04-08

本文對 16.8 版本之後 React 釋出的新特性 Hooks 進行了詳細講解,並對一些常用的 Hooks 進行程式碼演示,希望可以對需要的朋友提供點幫助。

一、Hooks 簡介

HooksReact v16.7.0-alpha 中加入的新特性。它可以讓你在 class 以外使用 state 和其他 React 特性。 本文就是演示各種 Hooks API 的使用方式,對於內部的原理這裡就不做詳細說明。


二、Hooks 初體驗

Example.js

import React, { useState  } from 'react';

function Example() {
    // 宣告一個名為“count”的新狀態變數
    const [count, setCount] = useState(0);

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

useState 就是一個 Hook,可以在我們不使用 class 元件的情況下,擁有自身的 state,並且可以通過修改 state 來控制 UI 的展示。


三、常用的兩個 Hooks

1、useState

語法

const [state, setState] = useState(initialState)

  • 傳入唯一的引數: initialState,可以是數字,字串等,也可以是物件或者陣列。
  • 返回的是包含兩個元素的陣列:第一個元素,state 變數,setState 修改 state值的方法。

與在類中使用 setState 的異同點:

  • 相同點:也是非同步的,例如在 onClick 事件中,呼叫兩次 setState,資料只改變一次。
  • 不同點:類中的 setState 是合併,而函式元件中的 setState 是替換。

使用對比

之前想要使用元件內部的狀態,必須使用 class 元件,例如:

Example.js

import React, { Component } from 'react';

class Example extends React.Component {
    constructor(props) {
        super(props);
        this.state = {
            count: 0
        };
    }

    render() {
        return (
            <div>
            <p>You clicked {this.state.count} times</p>
            <button onClick={() => this.setState({ count: this.state.count + 1 })}>
                Click me
            </button>
            </div>
        );
    }
}
複製程式碼

而現在,我們使用函式式元件也可以實現一樣的功能了。也就意味著函式式元件內部也可以使用 state 了。

Example.js

import React, { useState } from 'react';

function Example() {
    // 宣告一個名為“count”的新狀態變數
    const [count, setCount] = useState(0);

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

export default Example;
複製程式碼

優化

建立初始狀態是比較昂貴的,所以我們可以在使用 useState API 時,傳入一個函式,就可以避免重新建立忽略的初始狀態。

普通的方式:

// 直接傳入一個值,在每次 render 時都會執行 createRows 函式獲取返回值
const [rows, setRows] = useState(createRows(props.count));
複製程式碼

優化後的方式(推薦):

// createRows 只會被執行一次
const [rows, setRows] = useState(() => createRows(props.count));
複製程式碼

2、useEffect

之前很多具有副作用的操作,例如網路請求,修改 UI 等,一般都是在 class 元件的 componentDidMount 或者 componentDidUpdate 等生命週期中進行操作。而在函式元件中是沒有這些生命週期的概念的,只能 return 想要渲染的元素。 但是現在,在函式元件中也有執行副作用操作的地方了,就是使用 useEffect 函式。

語法

useEffect(() => { doSomething });

兩個引數:

  • 第一個是一個函式,是在第一次渲染以及之後更新渲染之後會進行的副作用。

    • 這個函式可能會有返回值,倘若有返回值,返回值也必須是一個函式,會在元件被銷燬時執行。
  • 第二個引數是可選的,是一個陣列,陣列中存放的是第一個函式中使用的某些副作用屬性。用來優化 useEffect

    • 如果使用此優化,請確保該陣列包含外部作用域中隨時間變化且 effect 使用的任何值。 否則,您的程式碼將引用先前渲染中的舊值。
    • 如果要執行 effect 並僅將其清理一次(在裝載和解除安裝時),則可以將空陣列([])作為第二個引數傳遞。 這告訴React你的 effect 不依賴於來自 props 或 state 的任何值,所以它永遠不需要重新執行。

雖然傳遞 [] 更接近熟悉的 componentDidMountcomponentWillUnmount 執行規則,但我們建議不要將它作為一種習慣,因為它經常會導致錯誤。

使用對比

假如此時我們有一個需求,讓 document 的 title 與 Example 中的 count 次數保持一致。

使用 class 元件:

Example.js

import React, { Component } from 'react';

class Example extends React.Component {
    constructor(props) {
        super(props);
        this.state = {
            count: 0
        };
    }

    componentDidMount() {
        document.title = `You clicked ${count} times`;
    }

    componentDidUpdate() {
        document.title = `You clicked ${count} times`;
    }

    render() {
        return (
            <div>
            <p>You clicked {this.state.count} times</p>
            <button onClick={() => this.setState({ count: this.state.count + 1 })}>
                Click me
            </button>
            </div>
        );
    }
}
複製程式碼

而現在在函式元件中也可以進行副作用操作了

Example.js

import React, { useState } from 'react';

function Example() {
    // 宣告一個名為“count”的新狀態變數
    const [count, setCount] = useState(0);

    // 類似於 componentDidMount 和 componentDidUpdate:
    useEffect(() => {
        // 使用瀏覽器API更新文件標題
        document.title = `You clicked ${count} times`;
    });

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

不僅如此,我們可以使用 useEffect 執行多個副作用(可以使用一個 useEffect 執行多個副作用,也可以分開執行)

useEffect(() => {
    // 使用瀏覽器API更新文件標題
    document.title = `You clicked ${count} times`;
});

const handleClick = () => {
    console.log('滑鼠點選');
}

useEffect(() => {
    // 給 window 繫結點選事件
    window.addEventListener('click', handleClick);
});
複製程式碼

現在看來功能差不多了。但是在使用類元件時,我們一般會在 componentWillMount 生命週期中進行移除註冊的事件等操作。那麼在函式元件中又該如何操作呢?

useEffect(() => {
    // 使用瀏覽器API更新文件標題
    document.title = `You clicked ${count} times`;
});

const handleClick = () => {
    console.log('滑鼠點選');
}

useEffect(() => {
    // 給 window 繫結點選事件
    window.addEventListener('click', handleClick);

    return () {
        // 給 window 移除點選事件
        window.addEventListener('click', handleClick);
    }
});
複製程式碼

可以看到,我們傳入的第一個引數,可以 return 一個函式出去,在元件被銷燬時,會自動執行這個函式

優化 useEffect

上面我們一直使用的都是 useEffect 中的第一個引數,傳入了一個函式。那麼 useEffect 的第二個引數呢?

useEffect 的第二個引數是一個陣列,裡面放入在 useEffect 使用到的 state 值,可以用作優化,只有當陣列中 state 值發生變化時,才會執行這個 useEffect

useEffect(() => {
    // 使用瀏覽器API更新文件標題
    document.title = `You clicked ${count} times`;
}, [ count ]);
複製程式碼

Tip:如果想模擬 class 元件的行為,只在 componetDidMount 時執行副作用,在 componentDidUpdate 時不執行,那麼 useEffect 的第二個引數傳一個 [] 即可。(但是不建議這麼做,可能會由於疏漏出現錯誤)


四、其他 Hoos API

1、useContext

語法

const value = useContext(MyContext);

接受上下文物件(從中React.createContext返回的值)並返回該上下文的當前上下文值。當前上下文值由樹中呼叫元件上方value最近的prop 確定<MyContext.Provider>。

useContext(MyContext) 則相當於 static contextType = MyContext 在類中,或者 <MyContext.Consumer>

用法

App.js 檔案中建立一個 context,並將 context 傳遞給 Example 子元件

App.js

import React, { createContext } from 'react';
import Example from './Example';

import './App.css';

export const ThemeContext = createContext(null);

export default () => {

    return (
        <ThemeContext.Provider value="light">
            <Example />
        </ThemeContext.Provider>
    )
}
複製程式碼

Example 元件中,使用 useContext API 可以獲取到傳入的 context

Example.js

import React, { useContext } from 'react';

import { ThemeContext } from './App';

export default () => {
    
    const context = useContext(ThemeContext);

    return (
        <div>Example 元件:當前 theme 是:{ context }</div>   
    )
}
複製程式碼

注意事項

useContext必須是上下文物件本身的引數:

  • 正確: useContext(MyContext)
  • 不正確: useContext(MyContext.Consumer)
  • 不正確: useContext(MyContext.Provider)

useContext(MyContext)只允許您閱讀上下文並訂閱其更改。您仍然需要<MyContext.Provider>在樹中使用以上內容來為此上下文提供值。

2、useReducer

語法

const [state, dispatch] = useReducer(reducer, initialArg, init);

useState 的替代方案。 接受型別為 (state, action) => newState 的reducer,並返回與 dispatch 方法配對的當前狀態。

當你涉及多個子值的複雜 state(狀態) 邏輯時,useReducer 通常優於 useState

用法

Example.js

import React, { useReducer } from 'react';

const initialState = {count: 0};

function reducer(state, action) {
    switch (action.type) {
        case 'increment':
            return {count: state.count + 1};
        case 'decrement':
            return {count: state.count - 1};
        default:
            throw new Error();
    }
}

export default () => {
    
    // 使用 useReducer 函式建立狀態 state 以及更新狀態的 dispatch 函式
    const [state, dispatch] = useReducer(reducer, initialState);
    return (
        <>
            Count: {state.count}
            <br />
            <button onClick={() => dispatch({type: 'increment'})}>+</button>
            <button onClick={() => dispatch({type: 'decrement'})}>-</button>
        </>
    );
}
複製程式碼

優化:延遲初始化

還可以懶惰地建立初始狀態。為此,您可以將init函式作為第三個引數傳遞。初始狀態將設定為 init(initialArg)

它允許您提取用於計算 reducer 外部的初始狀態的邏輯。這對於稍後重置狀態以響應操作也很方便:

Example.js

import React, { useReducer } from 'react';

function init(initialCount) {
    return {count: initialCount};
  }
  
function reducer(state, action) {
    switch (action.type) {
        case 'increment':
        return {count: state.count + 1};
        case 'decrement':
        return {count: state.count - 1};
        case 'reset':
        return init(action.payload);
        default:
        throw new Error();
    }
}

export default ({initialCount = 0}) => {
    
    const [state, dispatch] = useReducer(reducer, initialCount, init);
    return (
        <>
            Count: {state.count}
            <br />
            <button
                onClick={() => dispatch({type: 'reset', payload: initialCount})}>
                Reset
            </button>
            <button onClick={() => dispatch({type: 'increment'})}>+</button>
            <button onClick={() => dispatch({type: 'decrement'})}>-</button>
        </>
    );

}
複製程式碼

與 useState 的區別

  • state 狀態值結構比較複雜時,使用 useReducer 更有優勢。
  • 使用 useState 獲取的 setState 方法更新資料時是非同步的;而使用 useReducer 獲取的 dispatch 方法更新資料是同步的。

針對第二點區別,我們可以演示一下: 在上面 useState 用法的例子中,我們新增一個 button

useState 中的 Example.js

import React, { useState } from 'react';

function Example() {
    // 宣告一個名為“count”的新狀態變數
    const [count, setCount] = useState(0);

    return (
        <div>
            <p>You clicked {count} times</p>
            <button onClick={() => setCount(count + 1)}>
                Click me
            </button>
            <button onClick={() => {
                setCount(count + 1);
                setCount(count + 1);
            }}>
                測試能否連加兩次
            </button>
        </div>
    );
}

export default Example;
複製程式碼

點選 測試能否連加兩次 按鈕,會發現,點選一次, count 還是隻增加了 1,由此可見,useState 確實是 非同步 更新資料;

在上面 useReducer 用法的例子中,我們新增一個 button: useReducer 中的 Example.js

import React, { useReducer } from 'react';

const initialState = {count: 0};

function reducer(state, action) {
    switch (action.type) {
        case 'increment':
            return {count: state.count + 1};
        case 'decrement':
            return {count: state.count - 1};
        default:
            throw new Error();
    }
}

export default () => {
    
    // 使用 useReducer 函式建立狀態 state 以及更新狀態的 dispatch 函式
    const [state, dispatch] = useReducer(reducer, initialState);
    return (
        <>
            Count: {state.count}
            <br />
            <button onClick={() => dispatch({type: 'increment'})}>+</button>
            <button onClick={() => dispatch({type: 'decrement'})}>-</button>
            <button onClick={() => {
                dispatch({type: 'increment'});
                dispatch({type: 'increment'});
            }}>
                測試能否連加兩次
            </button>
        </>
    );
}
複製程式碼

點選 測試能否連加兩次 按鈕,會發現,點選一次, count 增加了 2,由此可見,每次dispatch 一個 action 就會更新一次資料,useReducer 確實是 同步 更新資料;

3、useCallback

語法

const memoizedCallback = useCallback(() => { doSomething(a, b); }, [a, b]);

返回值 memoizedCallback 是一個 memoized 回撥。傳遞內聯回撥和一系列依賴項。useCallback將返回一個回憶的memoized版本,該版本僅在其中一個依賴項發生更改時才會更改。當將回撥傳遞給依賴於引用相等性的優化子元件以防止不必要的渲染(例如shouldComponentUpdate)時,這非常有用。

這個 Hook 的 API 不能夠一兩句解釋的清楚,建議看一下這篇文章:useHooks 第一期:聊聊 hooks 中的 useCallback。裡面介紹的比較詳細。

4、useMemo

語法

const memoizedValue = useMemo(() => computeExpensiveValue(a, b), [a, b]);

返回一個memoized值。 傳遞“建立”函式和依賴項陣列。useMemo只會在其中一個依賴項發生更改時重新計算memoized值。此優化有助於避免在每個渲染上進行昂貴的計算。

useMemo在渲染過程中傳遞的函式會執行。不要做那些在渲染時通常不會做的事情。例如,副作用屬於useEffect,而不是useMemo。

用法

useMemo 可以幫助我們優化子元件的渲染,比如這種場景: 在 A 元件中有兩個子元件 B 和 C,當 A 元件中傳給 B 的 props 發生變化時,A 元件狀態會改變,重新渲染。此時 B 和 C 也都會重新渲染。其實這種情況是比較浪費資源的,現在我們就可以使用 useMemo 進行優化,B 元件用到的 props 變化時,只有 B 發生改變,而 C 卻不會重新渲染。

例子:

ExampleA.js

import React from 'react';

export default ({ text }) => {
    
    console.log('Example A:', 'render');
    return <div>Example A 元件:{ text }</div>

}
複製程式碼

ExampleB.js

import React from 'react';

export default ({ text }) => {
    
    console.log('Example A:', 'render');
    return <div>Example A 元件:{ text }</div>

}
複製程式碼

App.js

import React, { useState } from 'react';
import ExampleA from './ExampleA';
import ExampleB from './ExampleB';

import './App.css';

export default () => {

    const [a, setA] = useState('ExampleA');
    const [b, setB] = useState('ExampleB');

    return (
        <div>
            <ExampleA text={ a } />
            <ExampleB text={ b } />
            <br />
            <button onClick={ () => setA('修改後的 ExampleA') }>修改傳給 ExampleA 的屬性</button>
            &nbsp;&nbsp;&nbsp;&nbsp;
            <button onClick={ () => setB('修改後的 ExampleB') }>修改傳給 ExampleB 的屬性</button>
        </div>
    )
}
複製程式碼

此時我們點選上面任意一個按鈕,都會看到控制檯列印了兩條輸出, A 和 B 元件都會被重新渲染。

現在我們使用 useMemo 進行優化

App.js

import React, { useState, useMemo } from 'react';
import ExampleA from './ExampleA';
import ExampleB from './ExampleB';

import './App.css';

export default () => {

    const [a, setA] = useState('ExampleA');
    const [b, setB] = useState('ExampleB');

+    const exampleA = useMemo(() => <ExampleA />, [a]);
+    const exampleB = useMemo(() => <ExampleB />, [b]);

    return (
        <div>
+            {/* <ExampleA text={ a } />
+            <ExampleB text={ b } /> */}
+            { exampleA }
+            { exampleB }
            <br />
            <button onClick={ () => setA('修改後的 ExampleA') }>修改傳給 ExampleA 的屬性</button>
            &nbsp;&nbsp;&nbsp;&nbsp;
            <button onClick={ () => setB('修改後的 ExampleB') }>修改傳給 ExampleB 的屬性</button>
        </div>
    )
}
複製程式碼

此時我們點選不同的按鈕,控制檯都只會列印一條輸出,改變 a 或者 b,A 和 B 元件都只有一個會重新渲染。

5、useRef

語法

const refContainer = useRef(initialValue);

useRef 返回一個可變的 ref 物件,其 .current 屬性被初始化為傳遞的引數(initialValue)。返回的物件將存留在整個元件的生命週期中。

  • 從本質上講,useRef就像一個“盒子”,可以在其.current財產中保持一個可變的價值。
  • useRef() Hooks 不僅適用於 DOM 引用。 “ref” 物件是一個通用容器,其 current 屬性是可變的,可以儲存任何值(可以是元素、物件、基本型別、甚至函式),類似於類上的例項屬性。

注意:useRef() 比 ref 屬性更有用。與在類中使用 instance(例項) 欄位的方式類似,它可以 方便地保留任何可變值。

注意,內容更改時useRef 不會通知您。變異.current屬性不會導致重新渲染。如果要在React將引用附加或分離到DOM節點時執行某些程式碼,則可能需要使用回撥引用。

使用

下面這個例子中展示了可以在 useRef() 生成的 refcurrent 中存入元素、字串

Example.js

import React, { useRef, useState, useEffect } from 'react'; 

export default () => {
    
    // 使用 useRef 建立 inputEl 
    const inputEl = useRef(null);

    const [text, updateText] = useState('');

    // 使用 useRef 建立 textRef 
    const textRef = useRef();

    useEffect(() => {
        // 將 text 值存入 textRef.current 中
        textRef.current = text;
        console.log('textRef.current:', textRef.current);
    });

    const onButtonClick = () => {
        // `current` points to the mounted text input element
        inputEl.current.value = "Hello, useRef";
    };

    return (
        <>
            {/* 儲存 input 的 ref 到 inputEl */}
            <input ref={ inputEl } type="text" />
            <button onClick={ onButtonClick }>在 input 上展示文字</button>
            <br />
            <br />
            <input value={text} onChange={e => updateText(e.target.value)} />
        </>
    );

}
複製程式碼

點選 在 input 上展示文字 按鈕,就可以看到第一個 input 上出現 Hello, useRef;在第二個 input 中輸入內容,可以看到控制檯列印出對應的內容。

6、useLayoutEffect

語法

useLayoutEffect(() => { doSomething });

useEffect Hooks 類似,都是執行副作用操作。但是它是在所有 DOM 更新完成後觸發。可以用來執行一些與佈局相關的副作用,比如獲取 DOM 元素寬高,視窗滾動距離等等。

進行副作用操作時儘量優先選擇 useEffect,以免阻止視覺更新。與 DOM 無關的副作用操作請使用 useEffect

用法

用法與 useEffect 類似。

Example.js

import React, { useRef, useState, useLayoutEffect } from 'react'; 

export default () => {

    const divRef = useRef(null);

    const [height, setHeight] = useState(100);

    useLayoutEffect(() => {
        // DOM 更新完成後列印出 div 的高度
        console.log('useLayoutEffect: ', divRef.current.clientHeight);
    })
    
    return <>
        <div ref={ divRef } style={{ background: 'red', height: height }}>Hello</div>
        <button onClick={ () => setHeight(height + 50) }>改變 div 高度</button>
    </>

}
複製程式碼

五、嘗試編寫自定義 Hooks

這裡我們就仿照官方的 useReducer 做一個自定義的 Hooks

1、編寫自定義 useReducer

src 目錄下新建一個 useReducer.js 檔案:

useReducer.js

import React, { useState } from 'react';

function useReducer(reducer, initialState) {
    const [state, setState] = useState(initialState);

    function dispatch(action) {
        const nextState = reducer(state, action);
        setState(nextState);
    }

    return [state, dispatch];
}
複製程式碼

tip: Hooks 不僅可以在函式元件中使用,也可以在別的 Hooks 中進行使用。

2、使用自定義 useReducer

好了,自定義 useReducer 編寫完成了,下面我們看一下能不能正常使用呢?

改寫 Example 元件

Example.js

import React from 'react';

// 從自定義 useReducer 中引入
import useReducer from './useReducer';

const initialState = {count: 0};

function reducer(state, action) {
    switch (action.type) {
        case 'increment':
            return {count: state.count + 1};
        case 'decrement':
            return {count: state.count - 1};
        default:
            throw new Error();
    }
}

export default () => {
    
    // 使用 useReducer 函式建立狀態 state 以及更新狀態的 dispatch 函式
    const [state, dispatch] = useReducer(reducer, initialState);
    return (
        <>
            Count: {state.count}
            <br />
            <button onClick={() => dispatch({type: 'increment'})}>+</button>
            <button onClick={() => dispatch({type: 'decrement'})}>-</button>
        </>
    );
}
複製程式碼

五、Hooks 使用及編寫規範

  • 不要從常規 JavaScript 函式呼叫 Hooks;
  • 不要在迴圈,條件或巢狀函式中呼叫 Hooks;
  • 必須在元件的頂層呼叫 Hooks;
  • 可以從 React 功能元件呼叫 Hooks;
  • 可以從自定義 Hooks 中呼叫 Hooks;
  • 自定義 Hooks 必須使用 use 開頭,這是一種約定;

六、使用 React 提供的 ESLint 外掛

根據上一段所寫,在 React 中使用 Hooks 需要遵循一些特定規則。但是在程式碼的編寫過程中,可能會忽略掉這些使用規則,從而導致出現一些不可控的錯誤。這種情況下,我們就可以使用 React 提供的 ESLint 外掛:eslint-plugin-react-hooks。下面我們就看看如何使用吧。

安裝 ESLint 外掛

$ npm install eslint-plugin-react-hooks --save
複製程式碼

在 .eslintrc 中使用外掛

// Your ESLint configuration
{
  "plugins": [
    // ...
    "react-hooks"
  ],
  "rules": {
    // ...
    "react-hooks/rules-of-hooks": "error", // Checks rules of Hooks
    "react-hooks/exhaustive-deps": "warn" // Checks effect dependencies
  }
}
複製程式碼

七、參考文件

React 官網

React Hooks FAQ

相關文章