React 狀態管理庫及如何選擇?

當你開始使用 React 時,狀態的概念是比較棘手的事情之一,隨着應用程序的增長,狀態管理需求也會增加。

在這篇文章中,將爲你介紹 React 中的狀態管理選項,並幫助你決定在項目中使用哪一個。

什麼是狀態?

爲了讓我們站在同一戰線上,我們先來談談狀態。

每個交互式應用都涉及到對事件的響應,比如當用戶點擊一個按鈕時,側邊欄就會關閉。或者有人發送了一條消息,它就會出現在聊天窗口中。

隨着這些事件的發生,應用程序也會更新以反映這些事件,我們說應用的狀態已經改變。應用看起來和之前不一樣了,或者它在背後進入了一個新的模式。

比如,"側邊欄是打開還是關閉" 和 "聊天框中的消息" 都是狀態的一部分。在編程術語中,你可能會在應用的某個地方設置一個 isSidebarOpen 變量爲 true,還有一個 chatMessages 數組,裏面有你收到的消息。

廣義上講,在任何特定的時刻,你的應用的 "狀態" 是由所有這些數據決定的。所有這些單獨的變量,不管是存儲在本地組件狀態還是某個第三方狀態管理存儲中 -- 都是你的應用狀態。

這就是 "應用狀態" 的高級概念。我們還沒有談論 React 特有的東西,比如 useState 或 Context 或 Redux 或任何東西。

什麼是狀態管理?

所有那些決定你的應用處於什麼狀態的變量都必須存儲在某個地方。所以狀態管理是一個廣泛的術語,它結合瞭如何存儲狀態和如何變更狀態。

React 及其生態系統提供了很多不同的方式來存儲和管理這種狀態。當我說很多的時候,我的意思是 LOTS。

存儲數據

對於存儲,你可以...

React 不在乎你把數據放在哪裏,但是...

更新數據和重新渲染

爲了使你的應用具有交互性,你需要一種方法讓 React 知道有什麼變化,並且它應該重新渲染頁面上的一些(或所有)組件。

因爲儘管它的名字叫 React,但它並不像其他框架那樣是 "反應式"。

有些框架會 "觀察" 事物,並進行相應的更新。Angular、Svelte 和 Vue 等都做到了這一點。

但 React 沒有。它不會 "觀察變化" 並神奇地重新渲染。你 (或者其他什麼) 需要告訴它去做那件事。

哦,要完全清楚,我不建議將你的狀態全局地保存在窗口上,因爲所有通常原因,全局數據是要避免的。混亂的代碼,難以理解等等等等。我提到這一點只是想說這是可能的,想說明 React 真的不在乎它的數據來自哪裏 :)

什麼時候 useState 不夠用?

useState 鉤子非常適合少量的局部組件狀態。每個 useState 調用都可以保存一個值,雖然你可以把這個值做成一個包含一堆其他值的對象,但最好還是把它們拆開。

一旦你在一個組件中超過了 3-5 個 useState 調用,事情可能會變得很難跟蹤。尤其是當這些狀態位相互依賴的時候。對於複雜的相互依賴關係,一個合適的狀態機可能是更好的方式。

接下來,useReducer

從 useState "向上" 的下一步是 useReducer。reducer 函數爲你提供了一個集中的地方來攔截 "操作" 並相應地更新狀態。一個 useReducer 調用,就像 useState 一樣,只能容納一個值,但有了 reducer,更常見的是這個單一的值是一個包含多個值的對象。useReducer hooks 讓管理該對象變得更加容易。

避免 Prop Drilling with Context

除了 useState 和 useReducer 之外,你很可能感受到的下一個痛點就是 prop drilling。這就是當你有一個組件持有一些狀態,然後向下 5 層的子組件需要訪問它,你必須通過每一層手動 prop drilling。

這裏最簡單的解決方案是 Context API。它是內置在 React 中的。

儘管它很簡單,但 Context 有一個重要的缺點,那就是性能,除非你非常小心地使用它。

原因是當 Provider 的值發生變化時,每個調用 useContext 的組件都會重新渲染。目前看來還不錯吧?當數據改變時,組件會重新渲染?聽起來不錯!

但現在設想一下,如果這個值是一個包含 50 個不同的狀態位的對象,這些狀態位在整個應用程序中被使用,會發生什麼情況。而且它們經常變化,而且是獨立的。每當其中一個值發生變化時,使用其中任何一個值的每個組件都會重新渲染。

爲了避免這個陷阱,在每個 Context 中存儲小塊的相關數據,並在多個 Context 中拆分數據(你可以擁有任意多的數據)。或者,考慮使用第三方庫。

另一個要避免的性能問題是每次都向 Provider 的值中傳遞一個全新的對象。它看起來很無害,而且很容易被忽略。下面是一個例子。

function TheComponentWithState() {
  const [state, setState] = useState('whatever');
  return (
    <MyDataContext.Provider value={{
      state,
      setState
    }}>
      component's content goes here
      <ComponentThatNeedsData/>
    </MyDataContext.Provider>
  )
}

這裏我們傳遞的是一個包含狀態的對象及其 setter 的對象 setState。setState 永遠不會改變,state 只有在你告訴它時纔會改變。問題是包裹在它們周圍的對象,它將在每次 TheComponentWithState 被渲染時被重新創建。

你可能會注意到,我們在這裏談論的東西並不是真正的狀態管理,而只是傳遞變量。這是 Context 的主要目的。狀態本身被保存在其他地方,而 Context 只是把它傳來傳去。我推薦閱讀這篇關於 Context 與 Redux 的不同之處的文章,以瞭解更多細節。

另外,查看下面的鏈接參考資料,瞭解更多關於如何用 useCallback 修復 "新對象" 問題。

第三方狀態管理庫

讓我們來了解一下最常用的重要狀態管理工具。我已經提供了鏈接來了解每個工具的詳細信息。

Redux

在這裏提到的所有庫中,Redux 存在的時間最長。它遵循的是函數式(如函數式編程)的風格,嚴重依賴不變性。

你將創建一個單一的全局存儲來保存應用程序的所有狀態。一個 reducer 函數將接收你從組件中派發的動作,並通過返回一個新的狀態副本來響應。

因爲更改只通過動作發生,所以可以保存和重放這些動作,併到達相同的狀態。你也可以利用這一點來調試生產中的錯誤,像 LogRocket 這樣的服務存在,通過記錄服務器上的動作來實現這一目的。

優點
缺點
MobX

MobX 可能是內置 Context API 之外最流行的 Redux 替代品。Redux 是關於顯式和功能的,而 MobX 則採用了相反的方法。

MobX 是基於觀察者 / 可觀察模式的。你將創建一個可觀察的數據模型,將你的組件標記爲該數據的 "觀察者",MobX 將自動跟蹤它們訪問哪些數據,並在數據變化時重新渲染它們。

它讓你自由地定義你認爲合適的數據模型,並給你提供工具來觀察該模型的變化並對這些變化做出反應。

MobX 在幕後使用 ES6 Proxies 來檢測變化,所以更新可觀察的數據就像使用普通的 = 賦值操作符一樣簡單。

優點
缺點
MobX 狀態樹

MobX 狀態樹(或 MST)是在 MobX 之上的一層,它給你提供了一個反應式的狀態樹。你將使用 MST 的類型系統創建一個類型化的模型。模型可以有視圖(計算屬性)和動作(setter 函數)。所有的修改都要經過動作,因此 MST 可以跟蹤發生了什麼。

下面是一個模型的例子。

const TodoStore = types
  .model('TodoStore', {
    loaded: types.boolean,
    todos: types.array(Todo),
    selectedTodo: types.reference(Todo),
  })
  .views((self) => {
    return {
      get completedTodos() {
        return self.todos.filter((t) => t.done);
      },
      findTodosByUser(user) {
        return self.todos.filter((t) => t.assignee === user);
      },
    };
  })
  .actions((self) => {
    return {
      addTodo(title) {
        self.todos.push({
          id: Math.random(),
          title,
        });
      },
    };
  });

模型是可觀察的,這意味着如果一個組件被標記爲 MobX 觀察者,當模型變化時,它將自動重新渲染。你可以將 MST 與 MobX 結合起來,不需要太多的代碼就能寫出反應式組件。

MST 的一個很好的用例是存儲領域模型數據。它可以表示對象之間的關係(例如 TodoList 有很多 Todos,TodoList 屬於一個 User),並在運行時執行這些關係。

變更是以補丁流的形式創建的,你可以保存和重新加載整個狀態樹或其部分的快照。兩個用例:在頁面重載之間將狀態持久化到本地存儲,或將狀態同步到服務器。

優點
缺點
Recoil

Recoil 是這個列表中最新的庫,由 Facebook 創建。它可以讓你把數據組織成一個圖結構。它有點類似於 MobX 狀態樹,但前期沒有定義一個類型化的模型。它的 API 就像 React 的 useState 和 Context API 的組合,所以感覺和 React 很相似。

要使用它,你將你的組件樹包裹在一個 RecoilRoot 中(類似於你使用自己的 Context Provider 的方式)。然後在頂層創建狀態的 "原子",每個原子都有一個唯一的鍵。

const currentLanguage = atom({
  key: 'currentLanguage',
  default: 'en',
});

然後,組件可以使用 useRecoilState hook 來訪問這個狀態,它的工作原理與 useState 非常相似。

function LanguageSelector() {
  const [language, setLanguage] = useRecoilState(currentLanguage);
  return (
    <div>Languauge is {language}</div>
    <button onClick={() => setLanguage('es')}>
      Switch to Español
    </button>
  )
}

還有一個 "選擇器" 的概念,它可以讓你創建一個原子視圖:想一想派生狀態,比如 "TODO 的列表過濾到只剩下已完成的那些"。

通過跟蹤對 useRecoilState 的調用,Recoil 可以跟蹤哪些組件使用了哪些原子。這樣它就可以在數據發生變化時,只重新渲染那些 "訂閱" 某項數據的組件,所以這種方法在性能方面應該可以很好地擴展。

效益
缺點
React-Query

React-Query 與列表中的其他庫不同,因爲它是一個獲取數據的庫,而不是一個狀態管理庫。

我把它放在這裏,是因爲通常情況下,應用程序中的狀態管理有很大一部分是圍繞着加載數據、緩存、顯示 / 清除錯誤、在正確的時間清除緩存(或者在沒有清除的時候遇到 bug)等等...... 而 react-query 很好地解決了所有這些問題。

優勢
缺點
XState

最後一個也不是真正意義上的狀態管理庫,和這個列表中的其他庫一樣,但它非常有用!

XState 用 JavaScript(和 React,但它可以與任何框架一起使用))實現了狀態機和狀態圖。狀態機是一個 "衆所周知" 的想法(在學術文獻的意義上),已經存在了幾十年,它們在解決棘手的狀態問題方面做得非常好。

當很難推理出一個系統可以採取的所有不同組合和狀態時,狀態機是一個很好的解決方案。

舉個例子,想象一個複雜的自定義輸入,比如 Stripe 公司的那些花哨的信用卡號碼輸入 -- 這些輸入能夠精確地知道什麼時候在數字之間插入空格,以及將光標放在哪裏。

現在想想:當用戶點擊右鍵時,你應該怎麼做?嗯,這取決於光標的位置。而這取決於框中的文字是什麼 (光標是否在我們需要跳過的空格附近? 沒有?)。而且也許他們按住 Shift 鍵,你需要調整所選區域...... 有很多變量在起作用。你可以看到這將如何變得複雜。

手工管理這種事情是很棘手的,而且容易出錯,因此使用狀態機,你可以列出系統可能處於的所有狀態,以及它們之間的轉換。XState 將幫助你做到這一點。

優勢
缺點

"X 怎麼辦?"

還有很多庫我在這裏沒有篇幅介紹,比如 Zustand、easy-peasy 等。不過可以看看這些,它們也不錯:)

學習狀態管理的技巧

小例子對學習很有好處,但往往會讓一個庫顯得矯枉過正。("誰需要 Redux 來做 TODO 列表?" "爲什麼你要爲一個模態對話框使用整個狀態機?")

大的例子很適合看如何將一件事付諸實踐,但作爲介紹往往讓人不知所措。("哇,這些狀態機的東西看起來太複雜了")

就我個人而言,當我剛開始接觸一件事情的時候,我會先從那些 "愚蠢" 的小例子開始,即使我真正的目標是更大的事情。我發現現實世界的例子很容易讓人迷失在草叢中。

祝你在自己的狀態管理之路上好運:)

關於本文 譯者:@飄飄 作者:@Dave 原文:https://daveceddia.com/react-state-management/

本文由 Readfog 進行 AMP 轉碼,版權歸原作者所有。
來源https://mp.weixin.qq.com/s/jrxaa3iJBb6X9tMqbmiFNw