自 React 16.8 釋出以後,在已有專案中,把 package.json 中的 react 和 react-dom 版本一升,就可以抄起 Hooks 開幹了。筆者目前已經在專案中開始了實操,但不妨先總結下官方文件中一些值得梳理的點。
useState
為什麼 useState 不叫 createState 呢?
- 初始渲染時,useState 返回的是 initState
- 下次渲染時,useState 返回的是 curState
也就是說,create 的叫法就不太符合初始渲染之後獲取到的是「當前狀態」這麼一個事實了。
為什麼 useState 不通過 this 也知道自己是哪個 Component 的狀態?
每個元件內部都有一個「記憶體格子」的列表,他們就是一些存放資料的 JS 物件,當我們使用如 useState 的 Hooks 時,就會去讀取當前的格子(或者在初始渲染的時候進行初始化),然後將指標移動到下一個 Hooks。這就是為什麼一個元件內部的多個 useState 都能獲取到各自的區域性狀態。
但是需要注意的是,這也是為什麼官方建議我們要將 hooks 的呼叫順序保持一致。
useEffect
和過去的生命週期有什麼區別?
其一,React 會在每次渲染完成後會呼叫 useEffect,如果使用傳統的生命週期鉤子的話,當我們希望每次 render 後執行某種副作用時,我們不得不在 componentDidMount 和 componentDidUpdate 裡都塞上相同的邏輯,帶來冗餘。因此,傳統的生命週期是不能代替 useEffect 的。這一點可參考 React Class 生命週期。
當然,相比較考慮 mount 和 update,只考慮 render 是要簡單清晰不少。
其二,Hooks 讓我們可以基於邏輯而拆分程式碼,而不是基於生命週期。這一點非常重要,因為基於生命週期來拆分程式碼,勢必讓邏輯相關聯的程式碼分散各處。使用 Hooks,我們就可以按照我們指定的順序使用每一個副作用。
傳入的函式每次 render 都是新的?
是的,這是為了保證在 useEffect 中使用到的內部狀態都是最新的。這樣 useEffect 就很像是 render 的一部分了 —— 每次使用的 useEffect 都屬於其對應的的 render。
不僅如此,我們在 useEffect 中 return 的方法,也即通常用來做取消訂閱這類 cleanup 工作的,每次 render 後也都會執行一次新的副作用(準確的說會先走 return 的方法,再重新走一次 useEffect 中的方法),而絕不是 unmount 的時候才執行一次。這種模式會有更少的 bug。
什麼樣的 bug 呢?可以看官方文件的例子,大致就是說,如果我們訂閱的人的 id 變了,就需要取消訂閱然後重新訂閱新的人。這樣一來,如果在使用 class 做訂閱這類處理時,就需要在 3 個生命週期(componentDidMount、componentDidUpdate、componentWillUnmount)裡散佈邏輯,即在 componentDidUpdate 補充上取消並重新訂閱的邏輯!
如果用了 useEffect,這些東西根本不需要去考慮。整個過程如文件中給的例子一樣依次執行:
function FriendStatus(props) {
// ...
useEffect(() => {
ChatAPI.subscribeToFriendStatus(props.friend.id, handleStatusChange);
return () => {
ChatAPI.unsubscribeFromFriendStatus(props.friend.id, handleStatusChange);
};
});
// ...
}
// Mount with { friend: { id: 100 } } props
ChatAPI.subscribeToFriendStatus(100, handleStatusChange); // Run first effect
// Update with { friend: { id: 200 } } props
ChatAPI.unsubscribeFromFriendStatus(100, handleStatusChange); // Clean up previous effect
ChatAPI.subscribeToFriendStatus(200, handleStatusChange); // Run next effect
// Update with { friend: { id: 300 } } props
ChatAPI.unsubscribeFromFriendStatus(200, handleStatusChange); // Clean up previous effect
ChatAPI.subscribeToFriendStatus(300, handleStatusChange); // Run next effect
// Unmount
ChatAPI.unsubscribeFromFriendStatus(300, handleStatusChange); // Clean up last effect
複製程式碼
useEffect 第二個引數的優化作用
對 return 的 cleanup 同樣適用,不要忘了,每次 render 完就會先執行一次 cleanup,最終 unmount 的時候也會執行一次 cleanup。
useEffect(() => {
ChatAPI.subscribeToFriendStatus(props.friend.id, handleStatusChange);
return () => {
ChatAPI.unsubscribeFromFriendStatus(props.friend.id, handleStatusChange);
};
}, [props.friend.id]); // 只會在 props.friend.id 變化的時候重新訂閱
複製程式碼
如果我們不提供該引數,每次更新都會重新執行;如果只想 mount 和 unmount 的時候各執行一次,可指定 [],但這不是好的實踐方式,考慮到 useEffect 都是在 render 完後執行的,多做點工作可能會少點問題。
Hooks 使用原則
Only Call Hooks at the Top Level. Don’t call Hooks inside loops, conditions, or nested functions.
這一條的原因是,Hooks 是通過呼叫順序分配存放位置的,只有每次 run 的時候順序保持一致,才能挨個取得正確的 useState、useEffect。比方說,如果我們把 Hooks 放到條件語句裡,然後第一次 render 的時候每個都執行,第二次 render 卻有一個 Hook 不執行,那麼後面的對應就出錯了。很好理解吧。
但如果我們一定要有條件的執行 useEffect 呢?我們可以在 useEffect 內部加條件:
useEffect(function persistForm() {
// ? 這樣就不會破壞第一條原則
if (name !== '') {
localStorage.setItem('formData', name);
}
});
複製程式碼
Only Call Hooks from React Functions.
這條沒什麼說的,總之只在下面兩處用 Hooks:
- ✅ Call Hooks from React function components.
- ✅ Call Hooks from custom Hooks.