[ReactDoc] React Hooks - API Reference
Hooks API Reference @ React Docs
useState
useState @ React Docs - Hooks API Reference
⚠️ 特別留意:在 useState
的 set function 和 Class 中的 setState
不同,useState 的 set function 不會主動 merge,因此可以透過 { ...preObject }
的用法複製完整的物件。
functional updater:使用前一次的資料狀態
如果有需要的話,在 setCount
的函式中可以帶入 function,這個 function 可以取得前一次的資料狀態:
const [foo, setFoo] = useState(initialFoo);
setFoo((prevState) => /* do something with prevState */)
使用範例:
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>
</>
);
}
⚠️ 在 useEffect 或 useCallback 中善用 prevState 避免狀態依賴
若我們在 setCount
中使用的是前一次的狀態,就可以把該狀態從相依陣列(dependency array)中移除,這麼做的好處是畫面會隨時間繼續重新 render,但 useEffect 和裡面的 cleanup function 並不會每次都被叫到,像是這樣:
其他範例 如何錯誤地使用 React hooks useCallback 來保存相同的 function instance:
import React, { useState, useCallback, useRef } from 'react';
import ReactDOM from 'react-dom';
import './styles.css';
const Button = React.memo(({ handleClick }) => {
const refCount = useRef(0);
return <button onClick={handleClick}>{`button render count ${refCount.current++}`}</button>;
});
function App() {
const [isOn, setIsOn] = useState(false);
// 在 setIsOn 中帶入 prevState 可以避免把 state 或 props 放入相依陣列中
const handleClick = useCallback(() => setIsOn((isOn) => !isOn), []);
return (
<div className="App">
<h1>{isOn ? 'On' : 'Off'}</h1>
<Button handleClick={handleClick} />
</div>
);
}
const rootElement = document.getElementById('root');
const root = ReactDOM.createRoot(rootElement);
root.render(<App />);
Lazy initial state
- Lazy initial state @ React Docs - API -Reference
- How to create expensive objects lazily? @ Hooks FAQ
如果在 useState
內的預設值有些是需要經過函式處理的,則在 useState
的參數中可以帶入函式:
// 沒使用 lazy initial state: someExpensiveComputation 每次 render 都會被呼叫
const [state, setState] = useState(someExpensiveComputation(props));
// 使用 lazy initial state: someExpensiveComputation 只會被呼叫一次
const [state, setState] = useState(() => someExpensiveComputation(props));
useEffect vs. useLayoutEffect
簡單來說 useEffect
會阻塞畫面的轉譯(類似 addEventListener 中把 passive
設成 false
),需要等到 useEffect 內的 JS 執行完成後;而 useLayoutEffect
則不會阻塞畫面的轉譯(類似把 passive
設成 true
),畫面會持續轉譯,而不會被 useLayoutEffect
內的 JS 執行給阻塞。
useLayoutEffect @ React Docs
useRef
useRef @ React Docs - Hooks API Reference
由於每次畫面轉譯時的 state, props 都是獨立的,因此若我們真的有需要保留某一次轉譯時的資料狀態,則可以使用 useRef
。
const refContainer = useRef(<initialValue>);
useRef
會回傳有一個帶有 .current
屬性的物件,這個 .current
是可以被改變的(mutable),同時會在該元件的完整生命週期中被保留。透過 useRef
的參數,可以把初始值帶入 .current
屬性中。
參照到某個 DOM 元素
// https://reactjs.org/docs/hooks-reference.html#useref
import { useRef } from 'react';
const TextInputWithFocusButton = () => {
const inputRef = useRef < HTMLInputElement > null;
const handleClick = () => {
inputRef.current?.focus();
};
return (
<>
<input type="text" ref={inputRef} />
<button type="button" onClick={handleClick}>
Click To Focus
</button>
</>
);
};
export default TextInputWithFocusButton;
保存元件中會變更的資料
Is there something like instance variables? @ React Docs - Hooks FAQ
在 React 元件中,只要 state 或 props 有變化,即會導致該元件 re-render,如果希望有一些資料狀態能在元件中被保存取用,但不希望這個值改變時會導致 re-render,則可以使用 useRef
。
也就是說,透過 useRef
可以在元件中定義常數的使用。之所以:
不能直接在元件「中」直接使用
let
/const
定義變數,因為每次元件 re-render 後,該變數都會是不同的參照不能直接在元件「外」直接使用
let
/const
定義變數,因為當有多個地方使用到這個元件時,它們都會參照到「同一個」元件外的變數(Why need useRef and not mutable variable?)
但要注意的是,useRef
在內部的狀態改變的時候並不會發送通知,也就是說,當 .current
屬性改變時,並不會觸發元件重新轉譯。如果你是希望 React 附加(attach)或脫離(detach)某個 DOM 時去執行某些程式,則應該使用 callback ref 。
當我們沒有使用 useRef
時,${count}
的值會在每次該元件轉譯時就固定,因此會依序 1, 2, 3, ... 呈現;當使用 useRef
後,可以把 count
的值存在外部一個可以該元件參照的物件,因此在最後 setTimeout
呼叫時,拿到的都會是最後一次點擊 counter 的值,因此只會顯示最後的數字:
import React, { useState, useEffect, useRef } from 'react';
function Counter() {
const [count, setCount] = useState(0);
// 建立一個 ref,預設值為 count,也就是 0
const latestCount = useRef(count);
useEffect(() => {
// 改變 latestCount.current 的值
latestCount.current = count;
setTimeout(() => {
console.log(`You clicked ${count} times(count)`);
// 取得最新的值
console.log(`You clicked ${latestCount.current} times(latestCount.current)`);
}, 3000);
});
return (
<div>
<p>You click {count} times</p>
<button onClick={() => setCount(count + 1)}>Add</button>
</div>
);
}
export default Counter;
❌ 錯誤示範
Why need useRef to contain mutable variable but not define variable outside the component function? @ StackOverflow
有些時候,可以在 function component 外定義一個變數(countCache
),然後在 function component 內去參照到這個變數(countCache
),像是這樣:
import React, { useState, useEffect, useRef } from 'react';
// 在 function 外定義一個變數
let countCache = 0;
function Counter() {
const [count, setCount] = useState(0);
countCache = 0; // 給予預設值為 0
useEffect(() => {
// 每次畫面轉譯完成後賦值
countCache = count;
setTimeout(() => {
console.log(`You clicked ${count} times (count)`);
// 讀取該變數值
console.log(`You clicked ${countCache} times (countCache)`);
}, 3000);
});
return (
<div>
<p>You click {count} times</p>
<button onClick={() => setCount(count + 1)}>Add</button>
</div>
);
}
export default Counter;
如此看似可以得到一樣的效果,但當我們若會在多個地方使用到這個元件時,因為 countCache
這個變數是在 function 外部,因此在不同引用此元件的地方都會共用到這個變數,這會導致錯誤發生,參考 Why need useRef to contain mutable variable but not define variable outside the component function? @ StackOverflow 和 incorrect ref variable outside function @ code sandbox。
搭配 TypeScript 使用
如果要保存 HTMLElement 的話可以這樣寫:
// inputRef 的型別會是 RefObject<HTMLInputElement>,而它的 current 會是 readonly
const inputRef = useRef<HTMLInputElement>(null);
// ...
<input type="text" ref={inputRef} />;
但如果這個 inputRef 時會在後續才被賦值的話,其泛型需要使用 useRef<HTMLXXXElement | null>
的寫法,原因則可以從 React 對 useRef
型別的定義看出端倪(可以參考:How to Fix useRef React Hook "Cannot Assign to ... Read Only Property" TypeScript Error?)。例如:
/**
* An example of callback ref with TypeScript
**/
const App = () => {
// inputRef 的型別會是 MutableRefObject<HTMLInputElement | null>
const inputRef = useRef<HTMLInputElement | null>(null);
const setTextInput = useCallback((domElement: HTMLInputElement) => {
inputRef.current = domElement;
}, []);
const handleClick = () => {
inputRef.current?.focus();
};
return (
<>
<input type="text" ref={setTextInput} />
<button type="button" onClick={handleClick}>
Click
</button>
</>
);
};
useReducer
- 👍 Extracting State Logic into a Reducer @ React Docs Beta
- A Complete Guide to useEffect: Decoupling Updates from Actions @ Overreacted
基本使用
// 預先定義好的 initialState 和 reducer
const initialState = {
bar: 'BAR',
};
function reducer(state, action) {
const { bar } = state;
switch (action.type) {
case 'foo':
return { bar: bar };
default:
return state;
}
}
const App = () => {
// 使用 useReducer,需要將定義好的 reducer 和 initialState 帶入
const [state, dispatch] = useReducer(reducer, initialState);
// 呼叫 reducer 的方式,dispatch(<action>)
return <button onClick={() => dispatch('foo')}>Click Me</button>;
};
useReducer
使用時機是「當你需要更新一個資料,但這個資料其實是相依於另一個資料狀態時」,當你寫出 setSomething(something => ...)
時,就要考慮到可能是使用 useReducer
的時機。
透過 useReducer
,可以不再需要在 useEffect
內去讀取狀態,只需要在 effect 內去 dispatch 一個 action 來更新 state。如此,effect 不再需要更新狀態,只需要說明發生了什麼,更新的邏輯則都在 reducer 內統一處理。
如此,可以做到關注點分離(separate concerns)。在 event handler 中,透過 dispatch action,只需關注要「做什麼(what)」;在 reducer 中則決定要「如何(how)處理」這些資料。
Action 的 Type 要描述的是「做什麼(what)」,而不是「如何做(how)」,因此,有可能有兩個 action type 做的事情是一樣的,例如,edit_message
和 clear_message
都可以做到把 message 清空,但還是應該要把它們分開來,以利後續增添新功能或邏輯。
另一個非常重要的概念是,reducer function 一定要是 pure 的,千萬不要在 reducer 中執行 side-effect(例如,呼叫 API),而是應該要把 side effect 放在 event handler 或其他地方。React 在 Strict Mode 中會重複執行 reducer functions,以利開發者及早發現這個可能的錯誤。
React 會確保 dispatch
方法在每次重新轉譯後仍然是相同的,因此即使沒有把 dispatch
放在 useEffect
或 useCallback
的 dependency list 中,仍不會有任何問題。
搭配 TypeScript
只要 reducer
function 的型別有訂清楚的話,TypeScript 會自動推導 useReducer
中的型別
// 定義 reducer 中 state 的 type
type StateType = { foo: string; bar: string };
const initialState: StateType = { foo: 'foo', bar: 'bar' };
// 定義 reducer 中 action 的 type
type ActionType = {
type: 'ACTION_A' | 'ACTION_B' | 'ACTION_C';
payload: number; // 如果 payload 的型別都一樣
};
// reducer 的型別有定義好的話,useReducer 的地方就不用在定義
const reducer = (state: StateType, action: ActionType) => {
/* ... */
};
const App = () => {
const [state, dispatch] = useReducer(reducer, initialState);
return <div>{/* ... */}</div>;
};
若有需要將 dispatch
當作 props 傳給其他 component 的話,同樣將滑鼠移到 dispatch function 上就可以知道它的型別。一般來說 dispatch
的型別就是會 React.Dispatch<ReducerActionType>
(ReducerActionType
是自己取的 type 名稱):
如果 dispatch 的 action 會有不同類型 payload 的情況,可以定義多個 type 再搭配使用 |
,如此可以確保某些 type
的 action 其 payload
一定要符合特定型別:
// reducer.ts
import { ITask } from './types';
type AddedAction = {
type: 'added';
id: number;
text: string;
};
type ChangedAction = {
type: 'changed';
task: ITask;
};
type DeletedAction = {
type: 'deleted';
id: number;
};
// 如果 Action 所帶的 payload 型別不同,可以使用 union 的方式來定義 Action 的型別
type ReducerAction = AddedAction | ChangedAction | DeletedAction;
export default function tasksReducer(tasks: ITask[], action: ReducerAction) {
switch (action.type) {
case 'added': {
/* ... */
}
case 'changed': {
/* ... */
}
case 'deleted': {
/* ... */
}
default: {
// eslint-disable-next-line @typescript-eslint/restrict-template-expressions
throw Error(`Unknown action: ${action}`);
}
}
}
如果看到錯誤訊息 Argument of type 'FooBarState' is not assignable to parameter of type 'never'.
可以留意看看是不是 reducer
function 中最後沒有回傳 state(所有 case 都不符合的情況):
const reducer = (state: StateType, action: ActionType) => {
switch (action.type) {
case 'ACTION_A': {
/* ... */
}
case 'ACTION_B': {
/* ... */
}
case 'ACTION_C': {
/* ... */
}
default: {
// 最後如何果沒回傳 default case 的話,這裡會變成 never type
// return state;
}
}
};
範例程式碼:透過 reducer 讀取並更新 state
- Timer 透過 useReducer 取得並更新 state @ code sandbox
- Timer 透過 useReducer 取得 props 並更新 state @ code sandbox
import React, { useEffect, useReducer } from 'react';
const initialState = {
count: 0,
step: 1,
};
const Timer = () => {
const [state, dispatch] = useReducer(timerReducer, initialState);
const { count, step } = state;
// 使用 useReducer 可以將使用到的 state 和 props 從 useEffect 搬到 useReducer 內,
// 在 useEffect 內只需要描述要進行的動作 dispatch(action)
useEffect(() => {
const timerId = setInterval(() => {
dispatch({
type: 'tick',
});
}, 1000);
return () => {
clearInterval(timerId);
};
}, [dispatch]); // dispatch 可以省略不寫
return (
<>
<p>Current Count: {count}</p>
<div>
<input
value={step}
onChange={(e) => dispatch({ type: 'step', step: Number(e.target.value) })}
/>
</div>
</>
);
};
function timerReducer(state, action) {
const { count, step } = state;
if (action.type === 'tick') {
return {
count: count + step,
step,
};
} else if (action.type === 'step') {
return {
count: count,
step: action.step,
};
} else {
throw new Error();
}
}
export default Timer;
useCallback, useMemo
- useCallback @ React Docs - API Reference
- useCallback 的使用時機 @ Bonus: the useCallback conundrum
- useMemo @ React Docs - API Reference
- 如何錯誤地使用 React hooks useCallback 來保存相同的 function instance @ medium
// useCallback(fn, deps) is equivalent to useMemo(() => fn, deps).
const memoizedCallback = useCallback(() => {
doSomething(a, b);
}, [a, b]);
const memoizedValue = useMemo(() => computeExpensiveValue(a, b), [a, b]);
useCallback
會將原本傳入的函式保存下來(memoized),如果相依陣列(dependencies)沒有改變的話,則該函式會參照到原本的位置,因此會是同一個函式,如此可以避免畫面在不必要時重新轉譯(因為在 function component 中的函式每次都是全新的,所以每次都會觸發重新轉譯)。
useMemo
只有在 deps 有改變時才會呼叫該函式並重新回傳新的計算值,它可以用來避免每次轉譯時都要重新進行複雜的計算。
function ColorPicker() {
// Doesn't break Child's shallow equality prop check
// unless the color actually changes.
const [color, setColor] = useState('pink');
const style = useMemo(() => ({ color }), [color]);
return <Child style={style} />;
}
‼️ 注意:
React.memo()
和useMemo()
是不同的東西,前者是 HOC,後者是一個 Hooks。
useDeferredValue
- 👍 React 18 for app developers(可直接看 12 ~ 17 分的說明)@ React Conf 2021
- useDeferredValue @ react
因為在 React 中是透過資料狀態(state)的改變來觸發畫面的 re-render,因此若能延遲才拿到最新的資料狀態(deferredValue),就能避免畫面一直被 re-render。
想像有些網站上搜尋的對話框,它會在你每輸入一個字時就促使搜尋結果的畫面 re-render,在 CPU 較差的裝置上,經常會讓使用者有卡頓的感覺。
傳統上,我們可能會用 debounce 來解決這個問題,但在 React 18 中,就可以使用新的 useDeferredValue hook 來解決:
const deferredValue = useDeferredValue(value);
當 value 改變時,useDeferredValue 並不會馬上回傳最新的資料狀態,而是會等到 React 處理完其他更急迫的任務後,才會回傳最新的 value 給 deferredValue,進而促使畫面不需要在 value 改變時就立即被更新,而是在 React 沒有其他更急迫的任務時才來做 re-render。
這有點類似 debounce 的效果,例如當 switch button 在短時間內來回切換狀態時,debounce 後的 value 並不會立即更新,而是會等使用者沒有進一步動作時才開始動作;但 deferredValue 和 debounce 有很大的差異是在於:「debounce 總是會被觸發,但 useDeferredValue 則是只有在效能較差的裝置上才會有作用」。
因為在 React 中是透過資料狀態的改變來觸發畫面的 re-render,而 useDeferredValue 就是透過延遲得到最新的資料狀態,來讓畫面不需要立即的被更新。當沒有其他更急迫的任務要處理時,useDeferredValue 才會回傳最新的資料,並以此觸發該元件的 re-render。
因為在 React 中是透過資料狀態(state)的改變來觸發畫面的 re-render,因此若能延遲才拿到最新的資料狀態(deferredValue),就能避免畫面一直被 re-render。
想像有些網站上搜尋的對話框,會在你每輸入一個字時就促使搜尋結果的畫面 re-render,在效能較差的裝置上,經常會讓使用者有卡頓的感覺。
傳統上,我們可能會用 debounce 來解決這個問題,但在 React 18 中,就可以使用新的 useDeferredValue hook 來解決。
useDeferredValue 的作法就是透過延遲得到最新的資料狀態,來讓畫面不需要立即的被更新。當沒有其他更急迫的任務要處理時,useDeferredValue 才會回傳最新的資料,並以此觸發該元件的 re-render。
和 debounce 或 throttle 不同的地方在於,傳統這類方法,不論使用者每次都會被觸發