Hook 是 React 16.8 的新增特性。它可以讓你在不編寫 class 的情況下使用 state 以及其他的 React 特性
問題

- 在組件之間復(fù)用狀態(tài)邏輯很難
- 復(fù)雜組件變得難以理解
- 難以理解的 class
漸進(jìn)策略
沒有計(jì)劃從 React 中移除 class, 最重要的是,Hook 和現(xiàn)有代碼可以同時(shí)工作,你可以漸進(jìn)式地使用他們。
Hook 全家福
Basic Hooks
- useState
- useEffect
- useContext
自定義 Hook
Additional Hooks
- useReducer
- useCallback
- useMemo
- useRef
- useImperativeHandle
- useLayoutEffect
- useDebugValue
Hook 使用規(guī)則
Hook 就是 JavaScript 函數(shù),但是使用它們會(huì)有兩個(gè)額外的規(guī)則:
- 只能在函數(shù)最外層調(diào)用
Hook。不要在循環(huán)、條件判斷或者子函數(shù)中調(diào)用。 - 只能在
React的函數(shù)組件中調(diào)用Hook。不要在其他 JavaScript 函數(shù)中調(diào)用(自定義Hook除外)。
ESLint插件 可以用來強(qiáng)制執(zhí)行這兩條規(guī)則
npm install eslint-plugin-react-hooks
// 你的 ESLint 配置
{
"plugins": [
// ...
"react-hooks"
],
"rules": {
// ...
"react-hooks/rules-of-hooks": "error", // 檢查 Hook 的規(guī)則
"react-hooks/exhaustive-deps": "warn" // 檢查 effect 的依賴
}
}
useState
通過在函數(shù)組件里調(diào)用它來給組件添加一些內(nèi)部
state。React會(huì)在重復(fù)渲染時(shí)保留這個(gè)state。
import React, { useState } from 'react';
function Example() {
// 聲明一個(gè)叫 “count” 的 state 變量。
const [count, setCount] = useState(0);
return (
<div>
<p>You clicked {count} times</p>
<button onClick={() => setCount(count + 1)}>
Click me
</button>
</div>
);
}
useState 會(huì)返回一對(duì)值: 當(dāng)前的狀態(tài)和更新這個(gè)狀態(tài)的函數(shù)。
更新狀態(tài)的函數(shù)就類似 class 組件的 setState方法, 但是更新新狀態(tài)的函數(shù)不會(huì)將新的 state 和舊的 state 合并, 而是直接替換舊的 state。
入?yún)?/em> : 是作為 state 的初始值, 只在第一次渲染的時(shí)候用到??梢允?number, boolean, string, object 的值, 如果初始值需要而外的計(jì)算也可以是一個(gè) function。
出參 : [state, setState] state => 當(dāng)前的狀態(tài); setState => 更改這個(gè)狀態(tài)的函數(shù)。
useEffect
useEffect就是一個(gè)Effect Hook,給函數(shù)組件增加了操作副作用的能力。它跟class 組件中的componentDidMount、componentDidUpdate和componentWillUnmount具有相同的用途,只不過被合并成了一個(gè) API。
import React, { useState, useEffect } from 'react';
function Example() {
const [count, setCount] = useState(0);
// 相當(dāng)于 componentDidMount 和 componentDidUpdate:
useEffect(() => {
// 使用瀏覽器的 API 更新頁面標(biāo)題
document.title = `You clicked ${count} times`;
});
return (
<div>
<p>You clicked {count} times</p>
<button onClick={() => setCount(count + 1)}>
Click me
</button>
</div>
);
}
當(dāng)你調(diào)用 useEffect 時(shí),就是在告訴 React 在完成對(duì) DOM 的更改后運(yùn)行你的“副作用”函數(shù)。由于副作用函數(shù)是在組件內(nèi)聲明的,所以它們可以訪問到組件的 props 和 state。
useEffect 會(huì)在每次渲染后都執(zhí)行嗎?
是的, 默認(rèn)情況下,React 會(huì)在每次渲染后調(diào)用副作用函數(shù) —— 包括第一次渲染的時(shí)候。 React 保證了每次運(yùn)行 effect 的時(shí),DOM 都已經(jīng)更新完畢。
傳遞給 useEffect 的函數(shù)在每次渲染中都會(huì)有所不同,這是刻意為之的。每次我們重新渲染,都會(huì)生成新的 effect,替換掉之前的。某種意義上講,effect 更像是渲染結(jié)果的一部分 —— 每個(gè) effect “屬于”一次特定的渲染。
useEffect(()=>{
... // 要做的事
return () => {} // 清除操作
}, [依賴] )
為什么要在 effect 中返回一個(gè)函數(shù)?
這是 effect 可選的清除機(jī)制。每個(gè) effect 都可以返回一個(gè)清除函數(shù)。如此可以將添加和移除訂閱的邏輯放在一起。它們都屬于 effect 的一部分。
React 何時(shí)清除 effect?
React 會(huì)在組件卸載的時(shí)候執(zhí)行清除操作。正如之前學(xué)到的,effect 在每次渲染的時(shí)候都會(huì)執(zhí)行。這就是為什么 React 會(huì)在執(zhí)行當(dāng)前 effect 之前對(duì)上一個(gè) effect 進(jìn)行清除。
通過跳過 Effect 進(jìn)行性能優(yōu)化
useEffect(() => {
document.title = `You clicked ${count} times`;
}, [count]); // 僅在 count 更改時(shí)更新
我們出入的第二個(gè)參數(shù)[count], 如果 count 的值是5, 下一次的值還是5, react會(huì)對(duì)這兩次的值進(jìn)行比較, 如果發(fā)現(xiàn)是相等的, 就會(huì)跳過這個(gè) effect, 否則就會(huì)執(zhí)行。
知識(shí)點(diǎn): 如果你傳入了一個(gè)空數(shù)組([]),effect 內(nèi)部的 props 和 state 就會(huì)一直擁有其初始值。盡管傳入 [] 作為第二個(gè)參數(shù)更接近大家更熟悉的 componentDidMount 和 componentWillUnmount 思維模式,但我們有更好的來避免過于頻繁的重復(fù)調(diào)用 effect。
自定義 Hook
通過自定義 Hook,可以將組件邏輯提取到可重用的函數(shù)中。
// 提前公用邏輯到自定義Hook中
import React, { useState, useEffect } from 'react';
function useFriendStatus(friendID) {
const [isOnline, setIsOnline] = useState(null);
useEffect(() => {
function handleStatusChange(status) {
setIsOnline(status.isOnline);
}
ChatAPI.subscribeToFriendStatus(friendID, handleStatusChange);
return () => {
ChatAPI.unsubscribeFromFriendStatus(friendID, handleStatusChange);
};
});
return isOnline;
}
// 使用自定義 Hook
function FriendStatus(props) {
const isOnline = useFriendStatus(props.friend.id);
if (isOnline === null) {
return 'Loading...';
}
return isOnline ? 'Online' : 'Offline';
}
知識(shí)點(diǎn): 自定義 Hook 是一個(gè)函數(shù),其名稱以 “use” 開頭,函數(shù)內(nèi)部可以調(diào)用其他的 Hook。
在兩個(gè)組件中使用相同的 Hook 會(huì)共享 state 嗎?
不會(huì)。自定義 Hook 是一種重用狀態(tài)邏輯的機(jī)制(例如設(shè)置為訂閱并存儲(chǔ)當(dāng)前值),所以每次使用自定義 Hook 時(shí),其中的所有 state 和副作用都是完全隔離的。
useContext
useContext(MyContext)等同于類中的靜態(tài)contextType = MyContext,或者等同于<MyContext.Consumer>
useContext(MyContext)只允許您讀取上下文并訂閱其更改。您仍然需要樹中的<MyContext.Provider>來提供此上下文的值
useReducer
useState的替代方案。接受類型為(state,action)=> newState的reducer,并返回與dispatch方法配對(duì)的當(dāng)前狀態(tài)。(參考redux)
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();
}
}
function Counter({initialState}) {
const [state, dispatch] = useReducer(reducer, initialState);
return (
<>
Count: {state.count}
<button onClick={() => dispatch({type: 'increment'})}>+</button>
<button onClick={() => dispatch({type: 'decrement'})}>-</button>
</>
);
}
初始值也可以延遲初始化, useReducer(reducer, initialCount, init), init 是一個(gè)函數(shù), 初始值將設(shè)置為init(initialArg)
如果從Reducer Hook返回與當(dāng)前狀態(tài)相同的值,則React將退出而不渲染子項(xiàng)或觸發(fā)效果。 (React使用Object.is比較算法。)
useCallback
useCallback將返回一個(gè)回調(diào)的memoized(一種優(yōu)化手段,遇到計(jì)算開銷很大的函數(shù)時(shí),會(huì)緩存其計(jì)算結(jié)果,下次同樣的輸入就可以直接返回緩存的結(jié)果)版本,該版本僅在其中一個(gè)依賴項(xiàng)發(fā)生更改時(shí)才會(huì)更改。
const memoizedCallback = useCallback(
() => {
doSomething(a, b);
},
[a, b],
);
返回的就是一個(gè)memoized版本的 callback
useMemo
useMemo只會(huì)在其中一個(gè)依賴項(xiàng)發(fā)生更改時(shí)重新計(jì)算memoized值。此優(yōu)化有助于避免在每個(gè)渲染上進(jìn)行昂貴的計(jì)算
const memoizedValue = useMemo(() => computeExpensiveValue(a, b), [a, b]);
返回的就是一個(gè)memoized版本的值
useCallback(fn, deps) 相當(dāng)于a useMemo(() => fn, deps).
請(qǐng)記住,傳遞給useMemo的函數(shù)在渲染期間運(yùn)行。不要做那些在渲染時(shí)通常不會(huì)做的事情。例如,副作用應(yīng)該用useEffect,而不是useMemo
useRef
useRef返回一個(gè)可變的ref對(duì)象,其.current屬性被初始化為傳遞的參數(shù)(initialValue)。返回的對(duì)象將持續(xù)整個(gè)組件的生命周期
const refContainer = useRef(initialValue);
useRef()比ref屬性更有用。保持任何可變值的方法類似于在類中使用實(shí)例字段的方法。
useRef()創(chuàng)建了一個(gè)普通的JavaScript對(duì)象。 useRef()與自己創(chuàng)建{current:...}對(duì)象之間的唯一區(qū)別是useRef會(huì)在每個(gè)渲染上為您提供相同的ref對(duì)象。
useImperativeHandle
useImperativeHandle用于自定義暴露給父組件的ref屬性。需要配合forwardRef一起使用
// 子組件
import React, { forwardRef, useImperativeHandle, useRef } from "react";
function FancyInput(props, ref) {
const inputRef = useRef();
useImperativeHandle(ref, () => ({
focus: () => {
inputRef.current.focus();
}
}));
return <input ref={inputRef} className="ipt" />;
}
export default FancyInput = forwardRef(FancyInput);
//父組件
import React, { Component, createRef } from "react";
import FancyInput from "./input";
export default class ImperativeApp extends Component {
constructor(props) {
super(props);
this.inputRef = createRef();
}
render() {
return (
<>
<FancyInput ref={this.inputRef} />
<button
className="btn"
onClick={() => {
this.inputRef.current.focus();
}}
>
Click
</button>
</>
);
}
}
useLayoutEffect
簽名和
useEffect相同,但所有的DOM突變后同步觸發(fā)。使用它從DOM讀取布局并同步重新渲染。在瀏覽器有機(jī)會(huì)繪制之前,將在useLayoutEffect內(nèi)部計(jì)劃的更新將同步刷新
在可能的情況下首選標(biāo)準(zhǔn)useEffect以避免阻止視覺更新
注意useLayoutEffect在與componentDidMount和componentDidUpdate相同的階段觸發(fā)
useDebugValue
useDebugValue可用于在React DevTools中顯示自定義掛鉤的標(biāo)簽