有時候,你的 effect 可能會使用一些頻繁變化的值。你可能會忽略依賴列表中 state,但這通常會引起 Bug:
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>;
}
傳入空的依賴數組 []
,意味着該 hook 只在組件掛載時運行一次,並非重新渲染時。但如此會有問題,在 setInterval
的回調中,count
的值不會發生變化。因爲當 effect 執行時,我們會創建一個閉包,並將 count
的值被保存在該閉包當中,且初值爲 0。每隔一秒,回調就會執行 setCount(0 + 1)
,因此,count
永遠不會超過 1。
指定 [count]
作爲依賴列表就能修復這個 Bug,但會導致每次改變發生時定時器都被重置。事實上,每個 setInterval
在被清除前(類似於 setTimeout
)都會調用一次。但這並不是我們想要的。要解決這個問題,我們可以使用 setState 的函數式更新形式。它允許我們指定 state 該如何改變而不用引用 當前 state:
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>;
}
(setCount
函數的身份是被確保穩定的,所以可以放心的省略掉)
此時,setInterval
的回調依舊每秒調用一次,但每次 setCount
內部的回調取到的 count
是最新值(在回調中變量命名爲 c
)。
在一些更加複雜的場景中(比如一個 state 依賴於另一個 state),嘗試用 useReducer Hook 把 state 更新邏輯移到 effect 之外。useReducer 的 dispatch 的身份永遠是穩定的 —— 即使 reducer 函數是定義在組件內部並且依賴 props。
const initialState = {count: 0}
function reducer(state, action) {
switch (action.type) {
case 'increment':
return {count: state.count + 1};
default:
throw new Error()
}
}
function Counter() {
const [state, dispatch] = useReducer(reducer, initialState);
useEffect(() => {
const id = setInterval(() => {
dispatch({type: 'increment'})
}, 1000);
return () => setInterval(id);
}, []);
return <h1>{state.count}</h1>;
}
(如果你熟悉 Redux 的話,就已經知道它如何工作了。)
在某些場景下,useReducer
會比 useState
更適用,例如 state 邏輯較複雜且包含多個子值,或者下一個 state 依賴於之前的 state 等。並且,使用 useReducer
還能給那些會觸發深更新的組件做性能優化,因爲你可以向子組件傳遞 dispatch 而不是回調函數 。
function Child (props) {
console.log('重新渲染')
function handleClick () {
props.dispatch({type: 'increment'})
}
return (
<button onClick={handleClick}>按鈕</button>
)
}
Child = React.memo(Child)
function Counter() {
const [state, dispatch] = useReducer(reducer, initialState);
function handleClick() {
dispatch({type: 'increment'})
}
return (
<div>
<h1 onClick={handleClick}>{state.count}</h1>
<Child dispatch={dispatch} />
</div>
)
}
React 會確保 dispatch 函數的標識是穩定的,並且不會在組件重新渲染時改變。這就是爲什麼可以安全地從 useEffect 或 useCallback 的依賴列表中省略 dispatch。
function Counter() {
const [state, dispatch] = useReducer(reducer, initialState);
function handleClick() {
dispatch({type: 'increment'})
}
const callback = useCallback(() => {
dispatch({type: 'increment'})
}, [])
return (
<div>
<h1 onClick={handleClick}>{state.count}</h1>
<Child callback={callback} />
</div>
)
}
如果直接從useReducer返回操作,則其行爲與useState幾乎相同。
function App() {
const [name, setName] = useReducer((_, value) => value, '請輸入');
return (
<div className="App">
<input value={name} onChange={e => setName(e.target.value)} />
</div>
);
}