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');
ReactDOM.render(<App />, rootElement);

Lazy initial state#

Lazy initial state @ React Docs - API Reference

如果在 useState 內的預設值有些是需要經過函式處理的,則在 useState 的參數中可以帶入函式:

const [state, setState] = useState(() => {
const initialState = someExpensiveComputation(props);
return initialState;
});

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 React, { useRef } from 'react';
const TextInputWithFocusButton = () => {
const inputRef = useRef(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

因為在 Hooks 中,每次元件轉譯都是新的 state 和 props,若想要把留某次的資料狀態,則可以使用 useRef。之所以透過 useRef() 可以保留資料狀態是因為它會產生一個可以在每次轉譯時都參照到的同一個 JavaScript 物件,因此該物件可以在該元件的整個生命週期中都被取用

但要注意的是,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。

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。

Last updated on