React Hooks 介紹
React 引用的 Hooks 設計,讓副作用處理和外部功能更輕鬆地進入函式元件,相比於類別元件的寫法,提供了更簡單的解決方案。舉例來說,Redux 的 useSelector
可以取代 connect
和 mapStateToProps
。接下來,我們將通過 useState
和 useEffect
兩個 Hooks 的範例,來對比其與類別寫法的不同。
React 元件的兩種形式
React 元件有兩種形式:類別(Class)和函式(Function)。這兩種形式在狀態管理上有所不同:
- Class 元件:通過
this.setState()
來直接設定元件中的狀態。 - Function 元件:使用
const [state, setState] = useState()
來回傳的setState()
函數進行狀態更新。
// 使用 connect 的寫法
const mapStateToProps = (state) => ({
counter: state.myState,
});
export default connect(mapStateToProps)(App);
// 使用 hook 的寫法
const counter = useSelector((state) => state.myState);
useState 的概念與理解
useState 是一個讓函式元件擁有狀態的 Hook。它的運作方式類似於 JavaScript 的閉包模式,讓你可以在函式中管理狀態,而不是使用類別元件中的 this.state
。
useState 純值與物件比較
狀態可以是基本型別(Primitive)或物件型別(Object):
- Primitive type:通過值傳遞(by value)。
- Object type:通過引用傳遞(by reference)。
在 React 中,使用 useState
時的考量包括:
- Primitive:比較單純,適合新手使用。
- Object:對於多層物件結構,建議將物件進行正規化 (normalize)以簡化比較和更新。
useState(Primitive) vs useState(Object)
在使用 Hook 時,可以採用以下兩種策略來處理複雜狀態:
- useState(Primitive):宣告多個 useState,每個狀態變數單獨管理。
- useState(Object):使用一個 useState,將多個狀態存儲在一個物件中。
由於 class 的狀態一定是物件的型態,對於 Object 型態的狀態會有比較好的處理,舉例來說像是物件的合併機制。
// class setState 實際上做的事情
this.setState({ ...state, value: 0 });
相對於 hook 在設定上實際上沒有針對物件做預設的物件合併機制。
// setHookState 其實就是單純設定
const [state, setHookState] = useState(0);
setHookState();
JavaScript 物件比較
由於物件型態是 by reference ,使用上會有需要考量相等的比較方式。
- 這個例子來說物件並不相等
{display: "flex"} === {display: "flex"} // false
在 JavaScrpit 中有三種比較方式
- strict equality operator ===
- loose equality operator ==
- Object.is()
在 React 中會用到的方式是 shallow object equality check
- Shallow Equality: 會迭代物件物件中的 keys 如果都是
===
就當他相同- React.PureComponent 的 shouldComponentUpdate 實作的判斷方式
物件型態是按引用比較的,即 {display: "flex"} === {display: "flex"} // false
。
React 使用淺層比較來判斷物件是否相同。
function shallowEqual(object1, object2) {
const keys1 = Object.keys(object1);
const keys2 = Object.keys(object2);
if (keys1.length !== keys2.length) {
return false;
}
for (let key of keys1) {
if (object1[key] !== object2[key]) {
return false;
}
}
return true;
}
const hero1 = {
name: "Batman",
address: {
city: "Gotham",
},
};
const hero2 = {
name: "Batman",
address: {
city: "Gotham",
},
};
// 兩層
shallowEqual(hero1, hero2); // => false
Module Pattern 與 useState 的比較
Module Pattern 使用 IIFE(Immediately Invoked Function Expression)來創建私有變數和公共函式,類似於 useState 提供的狀態管理方式。
const moduleCounter = (function () {
let count = 0;
return {
getValue: function () {
return count;
},
increment: function () {
return ++count;
},
reset: function () {
console.log("reset:" + count);
count = 0;
},
};
})();
這與 useState 類似,都是利用閉包來保存狀態和操作函式。
function Counter() {
const [count, setCount] = useState(0);
function increment() {
setCount(count + 1);
}
function reset() {
setCount(0);
}
function getValue() {
return count;
}
return (
<div>
<button type="button" onClick={increment}>
add
</button>
<button type="button" onClick={reset}>
reset
</button>
<span>{getValue()}</span>
</div>
);
}
useEffect 的概念與理解
useEffect
用來處理副作用,相當於類別元件中的 componentDidMount
、componentDidUpdate
和 componentWillUnmount
。它接受兩個參數:副作用函式和依賴陣列。
// componentDidMount
useEffect(() => {}, []);
// componentDidUpdate
useEffect(() => {
const subscription = props.source.subscribe();
}, [props.source]);
useEffect(() => {
const subscription = props.source.subscribe();
return () => {
// componentWillUnMount
subscription.unsubscribe();
};
}, [props.source]);
使用 useEffect 的實際案例
例如,從 API 取得資料:
async componentDidMount() {
const response = await axios.get(`https://jsonplaceholder.typicode.com/users`)
this.setState({ users: response.data })
};
async componentDidUpdate(prevProps) {
if (prevProps.resource !== this.props.resource) {
const response = await axios.get(`https://jsonplaceholder.typicode.com/users`)
this.setState({ users: response.data })
}
};
const fetchUsers = async () => {
const response = await axios.get(`https://jsonplaceholder.typicode.com/users`);
setUsers(response.data);
};
useEffect( () => { fetchUsers(users) }, [ users ] );
由於 React 是 component-based 的一個函式庫,所以元件本身的定義和規範就蠻重要的,其中比較特殊的是元件在實際運用上會有一些生命週期,大致上我們平常會使用到的就是 componentDidMount
及 componentDidUpdate
,剩下可能會用到但比較少的是 componentWillUnmount
。
由於是寫在 function 中,所以可以想像整個 function 的內容都是原來寫法中 render()
裡的內容,差別在把 constructor
中的狀態用其他的方法寫在這個 function 裡面,元件原本由狀態改變來驅動的特性一樣沒有改變。
更好的寫法則是再抽出來,就會變下面這樣,更清楚也更好測試,也可以重複的去使用相關邏輯。
function useCounter(initial) {
const [count, setCount] = useState(initial);
const increment = () => setCount(count + 1);
const reset = () => setCount(0);
return { reset, count, increment };
}
function Counter() {
const [reset, count, increment] = useCounter(0);
return (
<div>
<button type="button" onClick={increment}>add</button>
<button type="button" onClick={reset}>reset</button>
<span>{count)}</span>
</div>
);
}
useEffect 的常見問題與解決方案
- 依賴缺失警告 (useEffect has a missing dependency):確保所有在 useEffect 中使用的變數都包含在依賴陣列中。
- 元件卸載後更新 (update on a unmounted component):在清理函式中處理 API 請求或計時器的中止。
useEffect(() => {
fetchData();
updateCloumn();
}, []);
- useEffect 中去打 API,但是資料回來時使用者已經切換到其他畫面
useEffect(() => {
let controller = new AbortController();
(async () => {
try {
const response = await fetch(
`https://jsonplaceholder.typicode.com/posts`,
{
signal: controller.signal,
}
);
setList(await response.json());
controller = null;
} catch (e) {
// Handle the error
}
})();
// aborts the request when the component umounts
return () => controller?.abort();
});
- timer function 也是同樣的概念
useEffect(() => {
let timerId = setTimeout(() => {
timerId = null;
setList([]);
}, 1000);
return () => clearTimeout(timerId);
});
- websocket 由於跟 API 的概念不太一樣,做法就是直接關掉
useEffect(() => {
const webSocket = new WebSocket(url);
return () => webSocket.close();
});
hook 效能優化
在撰寫 hook 且狀態較複雜時,會有底下兩種策略
- useState(Primitive): 宣告多個 useState 搭配多個值
- useState(Object): 宣告一個 useState 搭配一個物件中含有多個值
在 React 18 版之前,宣告多個 State 的寫法如下:
const [isLoading, setLoading] = useState(true);
const [list, setlist] = useState([]);
useEffect(async () => {
fetch("/list")
.then((res) => res.json())
.then((data) => {
setlist(data);
setLoading(false);
});
}, []);
這樣的寫法會因為兩個 setState 發生在非同步完成之後,所以 React 並不會 batch 他們而造成兩次的畫面渲染。
所以在 React 還沒協助處理之前,最簡單的方式就是透過物件一次設定,或是使用 unstable_batchedUpdates
。
setStateOnce({
list: data,
isLoading: false,
});
有篇 Github 上的討論蠻精彩的,有興趣可以參考如下:
https://github.com/reactwg/react-18/discussions/21
內文中也有提供範例,大家可以進去試用看看兩者差異。
- ✅ Demo: React 17 batches inside event handlers. (Notice one render per click in the console.)
- 🟡 Demo: React 17 does NOT batch outside event handlers. (Notice two renders per click in the console.)
在剛開始寫 hook 的時候,可能會寫像是以下的程式碼。下面 console.log
會一直印,代表每次都重新 render,但 Increment 這個其實沒有改變,該怎麼避免?
import { useState } from "react";
const CountDisplay = ({ count }) => <h3>Count: {count}</h3>;
const CountButton = ({ updateCount }) => {
// 下面這行會一直印,代表每次都重新 render,但 Increment 這個其實沒有改變,該怎麼避免
console.log("=> CountButton render", Date.now());
return <button onClick={() => updateCount()}>Increment</button>;
};
const App = () => {
const [count, setCount] = useState(0);
const updateCount = () => setCount((c) => c + 1);
return (
<>
<CountButton updateCount={updateCount} />
<CountDisplay count={count} />
</>
);
};
export default App;
除了基本的 hook 以外,常用的還有以下:
- useState: 加入元件狀態,但這裡的 setState 不會幫我們自動合併物件型態的狀態,需要用 callback 方式寫並且自行合併
- useReducer: 可以用來處理物件型態的狀態
- useEffect: 處理 side effect,取代 componentDidMount, componentDidUpdate, componentWillUnmount
- useCallback: 當 function 需要在 useEffect 中被使用但又不想加入觸發條件
- useMemo: 把較高成本計算記起來
- useRef: 取得參考用的 object
剛剛解決方案的小提示
- useMemo: 可以記住函式運算值
- useCallBack: 可以記住函式
React 18 useState Automatic Batching
前面提到的問題將在 React 18 之後獲得解決,即使是非同步 callback 中的 setState 也會自動 batch 而不會造成兩次的渲染,還是想分開反而要自己使用 ReactDOM.flushSync
來分開。
function App() {
const [count, setCount] = useState(0);
const [flag, setFlag] = useState(false);
function handleClick() {
setCount((c) => c + 1); // Does not re-render yet
setFlag((f) => !f); // Does not re-render yet
// React will only re-render once at the end (that's batching!)
}
return (
<div>
<button onClick={handleClick}>Next</button>
<h1 style={{ color: flag ? "blue" : "black" }}>{count}</h1>
</div>
);
}
function App() {
const [count, setCount] = useState(0);
const [flag, setFlag] = useState(false);
function handleClick() {
fetchSomething().then(() => {
// React 17 and earlier does NOT batch these because
// they run *after* the event in a callback, not *during* it
setCount((c) => c + 1); // Causes a re-render
setFlag((f) => !f); // Causes a re-render
});
}
return (
<div>
<button onClick={handleClick}>Next</button>
<h1 style={{ color: flag ? "blue" : "black" }}>{count}</h1>
</div>
);
}
hooks 優點與缺點
優點
- 更接近原生 JavaScript,對於初學者友好,且不需要理解 ES6。
- 減少複雜的元件週期管理,只需控制
useEffect
。 - 提供簡化的解決方案,如 Redux 的
useSelector
。 - 更容易進行程式碼壓縮和最佳化。
缺點
useEffect
可能會將多個副作用合併,需謹慎管理依賴。- 避免在函式中使用
new
或未處理的事件監聽器,避免每次渲染時都重做一次。 - 尚未涵蓋
getSnapshotBeforeUpdate
和componentDidCatch
這兩個生命周期方法。
壓縮 (minified)
React 團隊指出,Hook 是純函式,這使得程式碼壓縮(minified)變得更簡單,相對於類別元件來說更易於進行壓縮和優化。
注意: 詳細閱讀 React Hooks 文件 以了解更多信息。
喜歡這篇文章,請幫忙拍拍手喔 🤣