React Hook#
React Hook 是 React 16.8 的新增特性。它可以讓你在不編寫 class 的情況下使用 state 以及其他的 React 特性。
擁抱 React Hook#
什麼是 Hook?#
Hook 是一些可以讓你在函數組件裡 “鉤入” React state 及生命週期等特性的函數。Hook 不能在 class 組件中使用。
什麼時候使用 Hook?#
如果你在編寫函數組件並意識到需要向其添加一些 state,以前的做法是必須將其它轉化為 class,而現在你可以在現有的函數組件中使用 Hook。
State Hook#
State Hook 是允許你在 React 函數組件中添加 state 的 Hook。在 class 中,可以通過在構造函數中設置 this.state 來初始化 state,但是在函數組件中,我們沒有 this,所以不能分配或讀取 this.state,我們直接在組件中調用 useState
,舉個栗子:
import React, { useState } from 'react'
export default function Hello(prop) {
const [name, setName] = useState('chanshiyu')
const handleChange = e => setName(e.target.value)
return (
<div>
<Input placeholder="Your name" value={name} onChange={handleChange} />
</div>
)
}
useState
是 react 提供的新方法,這是一種在函數調用時保存變量的方式,它與 class 裡面的 this.state 提供的功能完全相同。一般來說,在函數退出後變量就會” 消失”,而 state 中的變量會被 React 保留。
useState
方法裡面唯一的參數就是初始 state。不同於 class 初始 state 必須是對象類型,useState
的參數可以是數字或者字符串等類型而不一定是對象。如果初始 state 需要通過複雜計算獲得,則可以傳入一個函數,在函數中計算並返回初始的 state,此函數只在初始渲染時被調用。
useState
調用後會返回當前 state 以及更新 state 的函數,可以通過數組的解構賦值來獲取。不像 class 中的 this.setState,更新 state 變量總是替換它而不是合併它。
當然,如果存在多個表單域,最好的實現方式是將 Hook 提取出復用的函數:
import React, { useState } from 'react'
export default function Hello(prop) {
const name = useFormInput('chanshiyu')
const age = useFormInput('24')
return (
<div>
<Input placeholder="Your name" value={name.value} onChange={name.onChange} />
<Input placeholder="Your age" value={age.value} onChange={age.onChange} />
</div>
)
}
function useFormInput(initialValue) {
const [value, setValue] = useState(initialValue)
const handleChange = e => setValue(e.target.value)
return {
value,
onChange: handleChange
}
}
如果計算初始值代價昂貴,可以傳入函數,這樣只會執行一次:
function Table(props) {
// ⚠️ createRows() 每次渲染都會被調用
const [rows, setRows] = useState(createRows(props.count))
// ✅ createRows() 只會被調用一次
const [rows, setRows] = useState(() => createRows(props.count))
}
Effect Hook#
Effect Hook 可以讓你在函數組件中執行副作用操作。數據獲取,設置訂閱以及手動更改 React 組件中的 DOM 都屬於副作用。React 組件中常見副作用一般分不需要清除和需要清除兩種類型。
不需要清除的 Effect#
這裡先舉個不需要清除副作用的栗子,我們根據表單輸入內容來動態改變頁面標籤標題:
import React, { useState, useEffect } from 'react'
export default function Hello(prop) {
const name = useFormInput('chanshiyu')
const title = `Hello, ${name.value}`
useDocumentTitle(title)
return (
<div>
<Input placeholder="Your name" value={name.value} onChange={name.onChange} />
</div>
)
}
function useFormInput(initialValue) {
const [value, setValue] = useState(initialValue)
const handleChange = e => setValue(e.target.value)
return {
value,
onChange: handleChange
}
}
function useDocumentTitle(title) {
useEffect(() => {
document.title = title
})
}
useEffect
可以告訴 React 組件需要在渲染後執行某些操作。React 會保存你傳遞的函數(我們將它稱之為 “effect”),並且在執行 DOM 更新之後調用它。在上面例子的 effect 中,傳遞的函數設置了 document 的 title 屬性,每次 DOM 更新後都會調用該函數。
將 useEffect
放在組件內部讓我們可以在 effect 中直接訪問 state 變量或其他 props。Hook 使用了 JavaScript 的閉包機制,將它保存在函數作用域中。。
默認情況,useEffect
會在每次渲染後執行。當然也可以通過跳過 Effect 進行性能優化,這部分接下來細說。
傳遞給 useEffect
的函數在每次渲染中都會有所不同,這是刻意為之的。每次重新渲染,都会生成新的 effect,替換掉之前的。某種意義上講,effect 更像是渲染結果的一部分 —— 每個 effect “屬於” 一次特定的渲染。
如果你熟悉 React class 的生命週期函數,你可以把 useEffect Hook 看做 componentDidMount,componentDidUpdate 和 componentWillUnmount 這三個函數的組合。 與 componentDidMount 或 componentDidUpdate 不同,使用 useEffect 調度的 effect 不會阻塞瀏覽器更新螢幕,這讓你的應用看起來響應更快。大多數情況下,effect 不需要同步地執行。
需要清除的 Effect#
上面的動態修改標籤頁標題的副作用屬於不需要清除的副作用,而事件監聽器屬於需要清除的副作用。為了防止引起內存泄露,在 class 組件中,會在 componentDidMount
添加的事件監聽,然後在 componentWillUnmount
生命週期中移除事件監聽,但這樣會讓處理同一個功能邏輯的代碼分布在兩個不同的地方,即使這兩部分代碼都作用於相同的副作用。
而在函數組件中 useEffect
的處理方式就高明許多,useEffect
設計是讓屬於同一副作用的代碼在同一個地方執行。如果你的 effect 返回一個函數,React 將會在執行清除操作時調用它。這裡再舉個栗子說明,現在我們要讓組件加載時設置監聽窗口縮放的事件,組件銷毀時移除:
import React, { useState, useEffect } from 'react'
export default function Hello(prop) {
const width = useWindowWidth()
return (
<div>
<div>Width: {width}</div>
</div>
)
}
function useWindowWidth() {
const [width, setWidth] = useState(window.innerWidth)
const handleWindowResize = () => setWidth(window.innerWidth)
useEffect(() => {
window.addEventListener('resize', handleWindowResize, false)
// 這裡返回一個函數,React 將會在執行清除操作時調用它
return () => window.removeEventListener('resize', handleWindowResize)
})
return width
}
為什麼要在 effect 中返回一個函數? 這是 effect 可選的清除機制。每個 effect 都可以返回一個清除函數。如此可以將添加和移除訂閱的邏輯放在一起,它們都屬於 effect 的一部分。
Effect 關注點#
使用 Effect Hook 其中一個目的就是要解決 class 中生命週期函數經常包含不相關的邏輯,但又把相關邏輯分離到了幾個不同方法中的問題。
Hook 允許我們按照代碼的用途分離他們,而不是像生命週期函數那樣。React 將按照 effect 聲明的順序依次調用組件中的每一個 effect。它會在調用一個新的 effect 之前對前一個 effect 進行清理。
在某些情況下,每次渲染後都執行清理或者執行 effect 可能會導致性能問題。在 class 組件中,我們可以通過在 componentDidUpdate
中添加對 prevProps
或 prevState
的比較邏輯解決。
componentDidUpdate(prevProps, prevState) {
if (prevState.name !== this.state.name) {
document.title = `Hello, ${this.state.name}`
}
}
在 Effect Hook 中,判斷是否需要重新執行的邏輯更為簡單,它被內置到了 useEffect
的 Hook API 中。只要傳遞數組作為 useEffect
的第二個可選參數,React 會判斷數組中的值在兩次渲染之間有沒有發生變化,來決定是否跳過對 effect 的調用,從而實現性能優化。如果數組中有多個元素,即使只有一個元素發生變化,React 也會執行 effect。
useEffect(() => {
document.title = `Hello, ${this.state.name}`
}, [name])
需要注意:如果要使用此優化方式,請確保數組中包含了所有外部作用域中會隨時間變化並且在 effect 中使用的變量,否則你的代碼會引用到先前渲染中的舊變量。
如果想執行只運行一次的 effect(僅在組件掛載和卸載時執行),可以傳遞一個空數組([])作為第二個參數。這就告訴 React 你的 effect 不依賴於 props 或 state 中的任何值,所以它永遠都不需要重複執行。
如果你傳入了一個空數組([]),effect 內部的 props 和 state 就會一直擁有其初始值。
React 會等待瀏覽器完成畫面渲染之後才會延遲調用 useEffect。
還有一點是 effect 的依賴頻繁變化時,在 effect 內使用 setValue,可以傳入函數而不是傳入值:
function Counter() {
const [count, setCount] = useState(0)
useEffect(() => {
const id = setInterval(() => {
setCount(count + 1) // 這個 effect 依賴於 `count` state
}, 1000)
return () => clearInterval(id)
}, []) // 🔴 Bug: `count` 沒有被指定為依賴
return <h1>{count}</h1>
}
function Counter() {
const [count, setCount] = useState(0)
useEffect(() => {
const id = setInterval(() => {
setCount(c => c + 1) // ✅ 在這不依賴於外部的 `count` 變量
}, 1000)
return () => clearInterval(id)
}, []) // ✅ 我們的 effect 不適用組件作用域中的任何變量
return <h1>{count}</h1>
}
Context Hook#
useContext
接收一個 context 對象(React.createContext 的返回值)並返回該 context 的當前值。useContext
的參數必須是 context 對象本身。
useContext(MyContext)
相當於 class 組件中的 static contextType = MyContext
或者 <MyContext.Consumer>
。
當前的 context 值由上層組件中距離當前組件最近的 <MyContext.Provider>
的 value prop 決定。調用了 useContext
的組件總會在 context 值變化時重新渲染。
import React, { useContext } from 'react'
import GlobalContext from '../../context'
export default function Hello(prop) {
const local = useContext(GlobalContext)
return (
<div>
<div>Language: {local}</div>
</div>
)
}
Reducer Hook#
在之前的 State Hook 介紹中,我們將多個表單的 useState
提取出單獨的函數來處理:
function useFormInput(initialValue) {
const [value, setValue] = useState(initialValue)
const handleChange = e => setValue(e.target.value)
return {
value,
onChange: handleChange
}
}
這是 useReducer
的雛形,React 內置了 useReducer
用來管理狀態。它接收一個形如 (state, action) => newState
的 reducer,並返回當前的 state
以及與其配套的 dispatch
方法。
當 state 邏輯較複雜且包含多個子值,或者下一個 state 依賴於之前的 state 時,可以使用 useReducer
代替 useState
。並且,使用 useReducer
還能給那些會觸發深更新的組件做性能優化。
function useReducer(reducer, initialState) {
const [state, setState] = useState(initialState)
function dispatch(action) {
const nextState = reducer(state, action)
setState(nextState)
}
return [state, dispatch]
}
調用方式:
function todosReducer(state, action) {
switch (action.type) {
case 'add':
return [
...state,
{
text: action.text,
completed: false
}
]
// ... other actions ...
default:
return state
}
}
function Todos() {
const [todos, dispatch] = useReducer(todosReducer, [])
function handleAddClick(text) {
dispatch({ type: 'add', text })
}
// ...
}
Callback Hook#
useCallback
把內聯回調函數及依賴項數組作為參數傳入 useCallback
,它將返回該回調函數的 memoized 版本,該回調函數僅在某個依賴項改變時才會更新。當你把回調函數傳遞給經過優化的並使用引用相等性去避免非必要渲染(例如 shouldComponentUpdate
)的子組件時,它將非常有用。
const memoizedCallback = useCallback(() => {
doSomething(a, b)
}, [a, b])
useCallback(fn, deps)
相當於 useMemo(() => fn, deps)
。
依賴項數組不會作為參數傳給回調函數。雖然從概念上來說它表現為:所有回調函數中引用的值都應該出現在依賴項數組中。
使用 callback ref
可以獲取 DOM:
function MeasureExample() {
const [height, setHeight] = useState(0)
const measuredRef = useCallback(node => {
if (node !== null) {
setHeight(node.getBoundingClientRect().height)
}
}, []) // [] 作為 useCallback 的依賴列表,這確保了 ref callback 不會在再次渲染時改變
return (
<>
<h1 ref={measuredRef}>Hello, world</h1>
<h2>The above header is {Math.round(height)}px tall</h2>
</>
)
}
或者可以單獨提取出可復用得 Hook:
function MeasureExample() {
const [rect, ref] = useClientRect()
return (
<>
<h1 ref={ref}>Hello, world</h1>
{/* 這裡使用短路運算 */}
{rect !== null && <h2>The above header is {Math.round(rect.height)}px tall</h2>}
</>
)
}
function useClientRect() {
const [rect, setRect] = useState(null)
const ref = useCallback(node => {
if (node !== null) {
setRect(node.getBoundingClientRect())
}
}, [])
return [rect, ref]
}
Memo Hook#
useMemo
返回一個 memoized 值,把 “創建” 函數和依賴項數組作為參數傳入 useMemo
,它僅會在某個依賴項改變時才重新計算 memoized 值。這種優化有助於避免在每次渲染時都進行高開銷的計算。如果沒有提供依賴項數組,useMemo
在每次渲染時都會計算新的值。
傳入 useMemo
的函數會在渲染期間執行。請不要在這個函數內部執行與渲染無關的操作,諸如副作用這類的操作屬於 useEffect
的適用範疇,而不是 useMemo
。
const memoizedValue = useMemo(() => computeExpensiveValue(a, b), [a, b])
Ref Hook#
useRef
返回一個可變的 ref 對象,其 .current
屬性被初始化為傳入的參數(initialValue)。返回的 ref 對象在組件的整個生命週期內保持不變。
const refContainer = useRef(initialValue)
查看官方示例:
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>
</>
)
}
useRef()
和自建一個 {current: ...}
對象的唯一区別是,useRef
會在每次渲染時返回同一個 ref 對象。
Ref Hook
不僅可以用於 DOM refs。「ref」對象是一個 current 屬性可變且可以容納任意值的通用容器,類似於一個 class 的實例屬性。
function Timer() {
const intervalRef = useRef()
useEffect(() => {
const id = setInterval(() => {
console.log('tick')
})
// 通過 .current 屬性來記錄定時器 id
intervalRef.current = id
// 回調在組件銷毀時清除
return () => {
clearInterval(intervalRef.current)
}
})
// 或者可以手動清除
function handleCancelClick() {
clearInterval(intervalRef.current)
}
}
甚至可以用它來保存上一輪的 props 或 state:
function Counter() {
const [count, setCount] = useState(0)
const prevCount = usePrevious(count)
return (
<h1>
Now: {count}, before: {prevCount}
</h1>
)
}
function usePrevious(value) {
const ref = useRef()
useEffect(() => {
ref.current = value
})
return ref.current
}
從概念上講,可以認為 refs 就像是一個 class 的實例變量。除非你正在做懶加載,否則避免在渲染期間設置 refs —— 這可能會導致意外的行為。相反的,通常你應該在事件處理器和 effects 中修改 refs。
ImperativeHandle Hook#
useImperativeHandle
可以讓你在使用 ref 時自定義暴露給父組件的實例值。useImperativeHandle
應當與 forwardRef
一起使用:
function FancyInput(props, ref) {
const inputRef = useRef()
useImperativeHandle(ref, () => ({
focus: () => {
inputRef.current.focus()
}
}))
return <input ref={inputRef} />
}
FancyInput = forwardRef(FancyInput)
LayoutEffect Hook#
useLayoutEffect
與 useEffect
相同,但它會在所有的 DOM 變更之後同步調用 effect。可以使用它來讀取 DOM 布局並同步觸發重渲染。在瀏覽器執行繪製之前,useLayoutEffect
內部的更新計劃將被同步刷新。盡可能使用標準的 useEffect
以避免阻塞視覺更新。
DebugValue Hook#
useDebugValue
可用於在 React 開發者工具中顯示自定義 hook 的標籤。
// 在開發者工具中的這個 Hook 旁邊顯示標籤
// e.g. "FriendStatus: Online"
useDebugValue(isOnline ? 'Online' : 'Offline')
Hook 規則#
Hook 本質就是 JavaScript 函數,但是在使用它時需要遵循兩條規則:
- 只在最頂層使用 Hook。不要在循環、條件或嵌套函數中調用 Hook,確保 Hook 在每一次渲染中都按照同樣的順序被調用。這讓 React 能夠在多次的
useState
和useEffect
調用之間保持 hook 狀態的正確。 - 只在 React 函數中調用 Hook。不要在普通的 JavaScript 函數中調用 Hook。
React 依靠的是 Hook 調用的順序來確定哪個 state 對應哪個 useState
,所以一定要確保每次渲染時的 Hook 順序是一致的。只有 Hook 的調用順序在每次渲染中都是相同的,React 才能正確地將內部 state 和對應的 Hook 進行關聯,它才能夠正常工作。