本文對 16.8 版本之後 React 釋出的新特性 Hooks 進行了詳細講解,並對一些常用的 Hooks 進行程式碼演示,希望可以對需要的朋友提供點幫助。
一、Hooks 簡介
Hooks
是 React 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 的任何值,所以它永遠不需要重新執行。
雖然傳遞 [] 更接近熟悉的
componentDidMount
和componentWillUnmount
執行規則,但我們建議不要將它作為一種習慣,因為它經常會導致錯誤。
使用對比
假如此時我們有一個需求,讓 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>
<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>
<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()
生成的 ref
的 current
中存入元素、字串
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
}
}
複製程式碼