Skip to main content

[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

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 內統一處理

使用 useReducer 可以將使用到的 state 和 props 從 useEffect 搬到 useReducer 內,在 useEffect 內只需要描述要進行的動作(dispatch(<action>))。

::info React 會確保 dispatch 方法在每次重新轉譯後仍然是相同的,因此即使沒有把 dispatch 放在 useEffectuseCallback 的 dependency list 中,仍不會有任何問題。 :::

範例程式碼:透過 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

因為在 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 則是只有在效能較差的裝置上才會有作用」。

tip

因為在 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 不同的地方在於,傳統這類方法,不論使用者每次都會被觸發