現(xiàn)在編寫新的組件的時候我們有兩個選擇:class組件和函數(shù)組件。那么什么是函數(shù)組件呢?
現(xiàn)在來看一段函數(shù):
function Welcome(props) {
return <h1>Hello, { props.name }</h1>;
}
可以看到杯道,函數(shù)組件是一個返回值是 DOM 結(jié)構(gòu)的函數(shù)。
函數(shù)組件與 class 組件的區(qū)別
函數(shù)式組件沒有組件實例化的過程豹爹,無實例化過程也就不需要分配多余的內(nèi)存裆悄,從而性能得到一定的提升;
函數(shù)組件本身沒有 this 的臂聋,所以使用 Ref 等模塊時與類組件也有所區(qū)別光稼;
函數(shù)式組件本身沒有內(nèi)部的 state,數(shù)據(jù)依賴于 props 的傳入孩等,所以又稱為無狀態(tài)組件艾君;
函數(shù)式組件本身是沒有訪問生命周期的方法。
如何使用函數(shù)式組件肄方,又可以使用 React 的強(qiáng)大功能呢冰垄?從 React 16.8 開始,React 新增 Hook 特性权她,使得可以在不編寫 class 的情況下使用 state 以及其他的 React 特性虹茶。這使得函數(shù)式組件也可以實現(xiàn) class 類組件的功能∈判剑現(xiàn)在,我們終于可以不用寫類蝴罪,也可以很方便地編寫復(fù)雜的功能了董济。
何時使用函數(shù)式組件
函數(shù)式組件相比類組件,擁有更好的性能和更簡單的職責(zé)要门,十分適合分割原本很寵大的組件虏肾。
什么是 Hook?
Hook 是可以在函數(shù)組件里“鉤入” React state 及生命周期等特性的函數(shù)。Hook 不能在 class 組件中使用欢搜,應(yīng)在函數(shù)式組件中使用封豪,使得不使用 class 也能使用 React。
Hook 使用規(guī)則
只在最頂層使用 Hook
只在函數(shù)最外層調(diào)用 Hook狂巢,不要在循環(huán)撑毛,條件或嵌套函數(shù)中調(diào)用 Hook,確边罅欤總是在 React 函數(shù)的最頂層調(diào)用藻雌。這樣就能確保 Hook 在每次渲染中按照同樣的順序被調(diào)用。
只在React 函數(shù)中調(diào)用 Hook
不要在普通的函數(shù)中調(diào)用 Hook斩个】韬迹可以在:
在 React 函數(shù)組件中調(diào)用 Hook
在自定義 Hook 中調(diào)用
遵循此規(guī)則,可以確保組件的狀態(tài)邏輯清晰受啥。
下面來具體介紹一下 Hook:
useState
const [state, setState] = useState(initialState);
useState 是允許在 React 函數(shù)組件中添加 state 的 Hook做个。React 會在重復(fù)渲染時保留這個 State。useState 唯一的參數(shù)是初始 State滚局,這個初始 State 參數(shù)只在第一次渲染時會被用到居暖。useState 返回一對值:當(dāng)前狀態(tài)和更新 state 的函數(shù)。
在后續(xù)的重新渲染中藤肢,useState 返回的第一個值將始終是更新后最新的 state太闺。
首先看一下state hook:
import React, { useState } from 'react';
function Example() {
// 聲明一個叫 "count" 的 state 變量,初始值為0
const [count, setCount] = useState(0);
return (
<div>
<p>You clicked {count} times</p>
<button onClick={() => setCount(count + 1)}>
Click me
</button>
</div>
);
}
再看一下等價的class:
import React 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>
);
}
}
這兩段代碼功能是一樣的嘁圈,每單擊一次按鈕將 count + 1省骂,但是函數(shù)組件相比 class 組件更簡單更直觀。
惰性初始 state
const [count, setCount] = useState(() => {
const initialState = handleComputation(props);
return initialState;
});
如果初始 state 需要通過復(fù)雜計算獲得最住,則可以傳入一個函數(shù)钞澳,在函數(shù)中計算并返回初始的 state,此函數(shù)只在初始渲染時被調(diào)用涨缚。
函數(shù)式更新 state
import React, { useState } from 'react';
function Counter({ initialCount }) {
const [count, setCount] = useState(initialCount);
return (
<>
Count: { count }
<button onClick={ () => setCount(initialCount) }>Reset</button>
<button onClick={ () => setCount(prevCount => prevCount - 1) }> - </button>
<button onClick={ () => setCount(prevCount => prevCount + 1) }> + </button>
</>
);
}
新的 state 需要通過使用先前的 state 計算得出轧粟,可以將函數(shù)傳遞給 setCount,該函數(shù)將接收先前的 state,并返回一個更新后的值逃延。
useEffect
useEffect(didUpdate, [deps]);
useEffect 給函數(shù)組件增加了操作副作用的能力览妖,也可看作是 componentDidMount,componentDidUpdate 和 componentWillUnmount 這三個函數(shù)的組合揽祥。第二個參數(shù)用來控制如何渲染讽膏,當(dāng)?shù)诙€參數(shù)存在時,只有當(dāng)該值改變時才會觸發(fā)渲染拄丰。
默認(rèn)情況下府树,effect 將在每輪渲染結(jié)束后執(zhí)行,加上第二個參數(shù)可以讓它在只有某些值改變的時候才執(zhí)行料按。
import React, { useState, useEffect } from 'react';
function Example() {
const [count, setCount] = useState(0);
// 與 componentDidMount 和 componentDidUpdate 作用相同
useEffect(() => {
// 更新 document 的 title
document.title = `You clicked ${count} times`;
}, [count]); // 僅在 count更改時更新
return (
<div>
<p>You clicked {count} times</p>
<button onClick={() => setCount(count + 1)}>
Click me
</button>
</div>
);
}
每次渲染都執(zhí)行 effect 可能會導(dǎo)致性能問題奄侠,在class組件中可以通過 componentDidUpdate 中添加 prevProps 或 prevState 的比較邏輯解決;在 useEffect 中可以通過第二個可選參數(shù)來控制渲染载矿,僅當(dāng) count 改變的時候才會執(zhí)行 effect垄潮,否則會跳過這個 effect,實現(xiàn)性能的優(yōu)化闷盔。
相同的功能使用 class 再來實現(xiàn)一下:
import React from 'react';
class Example extends React.Component {
constructor(props) {
super(props);
this.state = {
count: 0
};
}
componentDidMount() {
document.title = `You clicked ${this.state.count} times`;
}
componentDidUpdate() {
document.title = `You clicked ${this.state.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>
);
}
}
在以上的示例中弯洗,需要在兩個生命周期函數(shù)中編寫相同的代碼,但是 React 的 class 組件并沒有提供這樣的方法逢勾。
需要清除的 effect
實際項目當(dāng)中牡整,有一些副作用是需要清除的。例如訂閱外部數(shù)據(jù)源溺拱,定時器等逃贝。這種情況下,清除工作是非常重要的迫摔,可以防止引起內(nèi)存泄露沐扳。
首先看一下 class 組件:
import React from 'react';
class FriendStatusWithCounter extends React.Component {
constructor(props) {
super(props);
this.state = { count: 0, isOnline: null };
this.handleStatusChange = this.handleStatusChange.bind(this);
}
componentDidMount() {
document.title = `You clicked ${this.state.count} times`;
ChatAPI.subscribeToFriendStatus(
this.props.friend.id,
this.handleStatusChange
);
}
componentDidUpdate() {
document.title = `You clicked ${this.state.count} times`;
}
componentWillUnmount() {
ChatAPI.unsubscribeFromFriendStatus(
this.props.friend.id,
this.handleStatusChange
);
}
handleStatusChange(status) {
this.setState({
isOnline: status.isOnline
});
}
// ...
可以看到:設(shè)置 document.title 的邏輯被分割到 componentDidMount 和 componentDidUpdate 中,訂閱邏輯被分割到 componentDidMount 和 componentWillUnmount 中句占,componentDidMount 同時包含了兩個不同的功能迫皱。
現(xiàn)在再來看看 effect 的實現(xiàn):
import React, { useState, useEffect } from 'react';
function FriendStatusWithCounter(props) {
const [count, setCount] = useState(0);
useEffect(() => {
document.title = `You clicked ${count} times`;
}, [count]);
const [isOnline, setIsOnline] = useState(null);
useEffect(() => {
function handleStatusChange(status) {
setIsOnline(status.isOnline);
}
ChatAPI.subscribeToFriendStatus(props.friend.id, handleStatusChange);
return () => {
ChatAPI.unsubscribeFromFriendStatus(props.friend.id, handleStatusChange);
};
}, [props]);
// ...
}
每個 effect 都可以返回一個清除函數(shù),可以將添加和移除訂閱的邏輯放在一起辖众。在每次渲染的時候 React 會對前一個 effect 進(jìn)行清除再執(zhí)行當(dāng)前的 effect。
Hook 允許我們使用多個 Effect 實現(xiàn)關(guān)注點分離和敬。React 將按照 effect 聲明的順序依次調(diào)用組件中的 effect凹炸。
注意: 如果想執(zhí)行只運行一次的 effect(僅在組件掛載和卸載時執(zhí)行),可以傳遞一個空數(shù)組([ ])作為第二個參數(shù)昼弟,說明 effect 不依賴于 props 或 state 中的任何值啤它,它也不需要重復(fù)執(zhí)行。
useContext
const value = useContext(MyContext);
接收一個 context 對象(React.CreateContext 的返回值)并返回該 context 的當(dāng)前值。當(dāng)組件上層最近的 MyContext.Provider 更新時变骡,該 Hook 會觸發(fā)重渲染离赫,并使用最新傳遞給 MyContext provider 的 context value 值。即使祖先組件使用 React.memo 或 shouldComponentUpdate塌碌,也會在組件本身使用 useContext 時重新渲染渊胸。
調(diào)用了 useContext 的組件總會在 context 值變化時重新渲染。
const themes = {
light: {
foreground: "#000000",
background: "#eeeeee"
},
dark: {
foreground: "#ffffff",
background: "#222222"
}
};
const ThemeContext = React.createContext(themes.light);
function App() {
return (
<ThemeContext.Provider value={themes.dark}>
<Toolbar />
</ThemeContext.Provider>
);
}
function Toolbar(props) {
return (
<div>
<ThemedButton />
</div>
);
}
function ThemedButton() {
const theme = useContext(ThemeContext);
return (
<button style={{ background: theme.background, color: theme.foreground }}>
I am styled by theme context!
</button>
);
}
useCallback
useCallback(function, [deps])
useCallback 的作用是返回一個記憶化的回調(diào)函數(shù)台妆。第一個參數(shù)是一個回調(diào)函數(shù)翎猛,第二個參數(shù)是依賴項數(shù)組,在依賴項改變時會觸發(fā)更新接剩。
const memoizedCallback = useCallback(
() => {
doSomething(a, b);
},
[a, b],
);
把內(nèi)聯(lián)回調(diào)函數(shù)及依賴項數(shù)組作為參數(shù)傳入 useCallback切厘,它將返回該回調(diào)函數(shù)的記憶版本,該回調(diào)函數(shù)僅在某個依賴項改變時才會更新懊缺。
useReducer
const [state, dispatch] = useReducer(reducer, initialArg, init);
接收 (state, action) => newState 的 reducer疫稿,返回當(dāng)前的 state 以及配套的 dispatch 方法。適用于 state 邏輯較復(fù)雜且包含多個子值鹃两,或者下一個 state 依賴于之前的 state 等的場景遗座。
指定初始的 state -- 將初始 state 作為第二個參數(shù)傳入
const [state, dispatch] = useReducer(
reducer,
{count: initialCount}
);
惰性初始化 -- 將 init 函數(shù)作為第三個參數(shù)傳入
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();
}
}
function Counter({initialCount}) {
const [state, dispatch] = useReducer(reducer, initialCount, init);
return (
<>
Count: {state.count}
<button onClick={() => dispatch({type: 'reset', payload: initialCount})}>
Reset
</button>
<button onClick={() => dispatch({type: 'decrement'})}>-</button>
<button onClick={() => dispatch({type: 'increment'})}>+</button>
</>
);
}
useMemo
const memoizedValue = useMemo(() => computeExpensiveValue(a, b), [a, b]);
useMemo 的作用是返回一個記憶化的值。傳入 useMemo 的函數(shù)會在渲染期間執(zhí)行怔毛,不要在函數(shù)內(nèi)部執(zhí)行與渲染無關(guān)的操作员萍,注意:副作用的操作屬于 useEffect 的范疇。
如果沒有提供依賴項數(shù)組拣度,useMemo 會在每次渲染時計算新的值碎绎,只有將依賴項數(shù)組作為參數(shù)傳給 useMemo,它會在某個依賴項改變時才重新計算值抗果,這有助于避免每次渲染時都進(jìn)行高開銷的計算筋帖。
useLayoutEffect
useLayoutEffect(didUpdate, [deps]);
在所有的 DOM 變更之后同步調(diào)用 effect。使用它讀取 DOM 布局并同步觸發(fā)重渲染冤馏。在瀏覽器執(zhí)行繪制前日麸, useLayoutEffect 內(nèi)部的DOM 變更同步執(zhí)行,才不會感覺到視覺上的不一致逮光。
注意:盡可能使用標(biāo)準(zhǔn)的 useEffect 以避免阻塞視覺更新代箭。
useRef
const refContainer = useRef(initialValue);
useRef 返回一個可變的 ref 對象,其 .current 屬性被初始化為傳入的參數(shù)(initialValue)涕刚,返回的 ref 對象在組件的整個生命周期內(nèi)保持不變嗡综。它可以很方便地保存任何可變值,會在每次渲染時返回同一個 ref 對象杜漠。
注意:ref 對象內(nèi)容發(fā)生變化時极景,useRef 不會通知察净,變更 .current 屬性不會引發(fā)組件重新渲染。想要在 React 綁定或解綁 DOM 節(jié)點的 ref 時運行某些代碼盼樟,應(yīng)使用回調(diào) ref 實現(xiàn)氢卡。
function TextInputWithFocusButton() {
const inputEl = useRef(null);
const onButtonClick = () => {
// `current` 指向已掛載到 DOM 上的文本輸入元素
inputEl.current.focus();
};
return (
<>
<input ref={inputEl} type="text" />
<button onClick={onButtonClick}>Focus the input</button>
</>
);
}
useImperativeHandle
useImperativeHandle(ref, createHandle, [deps])
useImperativeHandle 可以在使用 ref 時自定義暴露給父組件的實例值,應(yīng)與 forwardRef 一起使用晨缴。
function FancyInput(props, ref) {
const inputRef = useRef();
useImperativeHandle(ref, () => ({
focus: () => {
inputRef.current.focus();
}
}));
return <input ref={inputRef} ... />;
}
FancyInput = forwardRef(FancyInput);
父組件使用 inputRef.current.focus() 來調(diào)用译秦。
useDebugValue
useDebugValue(value, formatFunction)
useDebugValue 用于在 React 開發(fā)者工具里顯示自定義 hook 的標(biāo)簽。
格式化 debug 值
useDebugValue(date, date => date.toDateString());
useDebugValue 接受格式化函數(shù)作為可選的第二個參數(shù)喜庞。該函數(shù)接受 debug 值作為參數(shù)诀浪,并會返回一個格式化的顯示值。
自定義 Hook
自定義 Hook 是一個函數(shù)延都,可以將組件邏輯提取到可重用的函數(shù)雷猪。名稱以" use "開頭,函數(shù)內(nèi)部可以調(diào)用其他的 Hook晰房。
現(xiàn)在來定義一個 useFriendStatus Hook:
import { 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);
};
}, [friendID]);
return isOnline;
}
這里 useFriendStatus 作用是訂閱好友的在線狀態(tài)求摇。
將 useFriendStatus 在不同的組件中使用:
function FriendStatus(props) {
const isOnline = useFriendStatus(props.friend.id);
if (isOnline === null) {
return 'Loading...';
}
return isOnline ? 'Online' : 'Offline';
}
function FriendListItem(props) {
const isOnline = useFriendStatus(props.friend.id);
return (
<li style={{ color: isOnline ? 'green' : 'black' }}>
{props.friend.name}
</li>
);
}
現(xiàn)在很好地實現(xiàn)了好友在線狀態(tài)的復(fù)用,只需 const isOnline = useFriendStatus(props.friend.id);
就可知道好友的狀態(tài)了殊者。
自定義 Hook 是一種重用狀態(tài)邏輯的機(jī)制与境,每次使用自定義 Hook 時,其中所有的 state 和副作用都是完全隔離的猖吴。