React Hooks 梳理

自 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.
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章