跳至主要内容

[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 並不會每次都被叫到,像是這樣:

imgur

其他範例 如何錯誤地使用 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

如果在 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;

imgur

​ 錯誤示範

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

基本使用

// 預先定義好的 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

Action 的 Type 要描述的是「做什麼(what)」,而不是「如何做(how)」,因此,有可能有兩個 action type 做的事情是一樣的,例如,edit_messageclear_message 都可以做到把 message 清空,但還是應該要把它們分開來,以利後續增添新功能或邏輯。

不要在 reducer 中執行 side effect

另一個非常重要的概念是,reducer function 一定要是 pure 的,千萬不要在 reducer 中執行 side-effect(例如,呼叫 API),而是應該要把 side effect 放在 event handler 或其他地方。React 在 Strict Mode 中會重複執行 reducer functions,以利開發者及早發現這個可能的錯誤。

在 useEffect 中使用 dispatch

React 會確保 dispatch 方法在每次重新轉譯後仍然是相同的,因此即使沒有把 dispatch 放在 useEffectuseCallback 的 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 名稱):

Screen Shot 2021-06-05 at 6.17.23 PM

如果 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

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(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

官方文件

useDeferredValue @ react.dev

因為在 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。

提示

具體來說,React 在第一次 re-render 的時候不會更新 deferred value,而是後續才用 new value 在 background 做 re-render。(參考:How does deferring a value work under the hood?

這有點類似 debounce 的效果,例如當 switch button 在短時間內來回切換狀態時,debounce 後的 value 並不會立即更新,而是會等使用者沒有進一步動作時才開始動作;但 deferredValue 和 debounce 有很大的差異是在於:「debounce 總是會被觸發,但 useDeferredValue 則是只有在效能較差的裝置上才會有作用」。

提示

因為在 React 中是透過資料狀態的改變來觸發畫面的 re-render,而 useDeferredValue 就是透過延遲得到最新的資料狀態,來讓畫面不需要立即的被更新。當沒有其他更急迫的任務要處理時,useDeferredValue 才會回傳最新的資料,並以此觸發該元件的 re-render。

在使用 useDeferredValue 時,有幾點需要特別留意

  • 帶入 useDeferredValue 的值應該要是 primitive values(例如 string 或 number)、如果是 object 的話,要保持該 object 的 reference 而不能每次都是建立一個新的 object,否則,它每次 render 都不會是不同的物件,將會導致不必要的 background re-render。
  • useDeferredValue 能搭配 <Suspense> 使用,但當資料更新時,使用者不會看到 fallback,而是會看到 old deferred value

另外,因為 useDeferredValue 並沒有 isLoading 的狀態,如果需要的話,可以像這樣:

const App = () => {
const [query, setQuery] = useState('');
const deferredQuery = useDeferredValue(query);

const isDataStale = query !== deferredQuery;

return (/* ... */);
}