React 狀態管理庫及如何選擇?
當你開始使用 React 時,狀態的概念是比較棘手的事情之一,隨着應用程序的增長,狀態管理需求也會增加。
在這篇文章中,將爲你介紹 React 中的狀態管理選項,並幫助你決定在項目中使用哪一個。
什麼是狀態?
爲了讓我們站在同一戰線上,我們先來談談狀態。
每個交互式應用都涉及到對事件的響應,比如當用戶點擊一個按鈕時,側邊欄就會關閉。或者有人發送了一條消息,它就會出現在聊天窗口中。
隨着這些事件的發生,應用程序也會更新以反映這些事件,我們說應用的狀態已經改變。應用看起來和之前不一樣了,或者它在背後進入了一個新的模式。
比如,"側邊欄是打開還是關閉" 和 "聊天框中的消息" 都是狀態的一部分。在編程術語中,你可能會在應用的某個地方設置一個 isSidebarOpen 變量爲 true,還有一個 chatMessages 數組,裏面有你收到的消息。
廣義上講,在任何特定的時刻,你的應用的 "狀態" 是由所有這些數據決定的。所有這些單獨的變量,不管是存儲在本地組件狀態還是某個第三方狀態管理存儲中 -- 都是你的應用狀態。
這就是 "應用狀態" 的高級概念。我們還沒有談論 React 特有的東西,比如 useState 或 Context 或 Redux 或任何東西。
什麼是狀態管理?
所有那些決定你的應用處於什麼狀態的變量都必須存儲在某個地方。所以狀態管理是一個廣泛的術語,它結合瞭如何存儲狀態和如何變更狀態。
React 及其生態系統提供了很多不同的方式來存儲和管理這種狀態。當我說很多的時候,我的意思是 LOTS。
存儲數據
對於存儲,你可以...
-
將這些變量保存在本地組件狀態中 -- 無論是使用 hooks(useState 或 useReducer)還是類(this.state 和 this.setState)。
-
將數據保存在存儲中,使用第三方庫,如 Redux、MobX、Recoil 或 Zustand。
-
甚至可以全局地將它們保留在窗口對象上。
React 不在乎你把數據放在哪裏,但是...
更新數據和重新渲染
爲了使你的應用具有交互性,你需要一種方法讓 React 知道有什麼變化,並且它應該重新渲染頁面上的一些(或所有)組件。
因爲儘管它的名字叫 React,但它並不像其他框架那樣是 "反應式"。
有些框架會 "觀察" 事物,並進行相應的更新。Angular、Svelte 和 Vue 等都做到了這一點。
但 React 沒有。它不會 "觀察變化" 並神奇地重新渲染。你 (或者其他什麼) 需要告訴它去做那件事。
-
使用 useState、useReducer 或 this.setState(類),當你調用其中一個 setter 函數時,React 會重新渲染。
-
如果你將數據保存在 Redux、MobX、Recoil 或其他存儲中,那麼當一些東西發生變化時,該存儲將告訴 React,併爲你觸發重渲染。
-
如果你選擇在窗口上全局保留數據,你需要告訴 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 這樣的服務存在,通過記錄服務器上的動作來實現這一目的。
優點
-
自 2015 年以來一直處於試驗階段
-
官方的 Redux Toolkit 庫減少了模板代碼。
-
優秀的開發工具讓調試變得簡單
-
Time travel 調試
-
bundle size 小(redux+react-redux 約爲 3kb)。
-
功能性的風格意味着很少有幕後隱藏的東西
-
有自己的庫生態系統,用於做一些事情,如同步到 localStorage,管理 API 請求,以及更多。
缺點
-
心智模型需要一些時間來理解,特別是當你不熟悉函數式編程的時候
-
對不可變性的嚴重依賴會使編寫 reducer 變得很麻煩 (通過添加 Immer 庫,或使用包含 Immer 的 Redux Toolkit 來緩解這一問題)
-
要求你對所有的事情都要明確(這可能是贊成或反對,取決於你喜歡什麼)。
MobX
MobX 可能是內置 Context API 之外最流行的 Redux 替代品。Redux 是關於顯式和功能的,而 MobX 則採用了相反的方法。
MobX 是基於觀察者 / 可觀察模式的。你將創建一個可觀察的數據模型,將你的組件標記爲該數據的 "觀察者",MobX 將自動跟蹤它們訪問哪些數據,並在數據變化時重新渲染它們。
它讓你自由地定義你認爲合適的數據模型,並給你提供工具來觀察該模型的變化並對這些變化做出反應。
MobX 在幕後使用 ES6 Proxies 來檢測變化,所以更新可觀察的數據就像使用普通的 = 賦值操作符一樣簡單。
優點
-
以真正的 "反應式" 方式管理狀態,因此當你修改一個值時,任何使用該值的組件都會自動重新渲染。
-
不需要任何動作或者 reducers,只需修改你的狀態,應用程序就會反映出來。
-
神奇的反應性意味着要寫更少的代碼。
-
你可以編寫常規的可變性代碼。不需要特殊的 setter 函數或不可變性。
缺點
-
不像 Redux 那樣廣泛使用,所以社區支持較少(教程等),但在用戶中深受喜愛
-
神奇的反應性意味着更少的明文代碼。(這可能是一個優點或缺點,取決於你對自動更新 "魔法" 的感覺)
-
要求使用 ES6 代理,意味着不支持 IE11 及以下版本。(如果你的應用需要支持 IE,那麼舊版本的 MobX 可以不需要代理服務器)
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),並在運行時執行這些關係。
變更是以補丁流的形式創建的,你可以保存和重新加載整個狀態樹或其部分的快照。兩個用例:在頁面重載之間將狀態持久化到本地存儲,或將狀態同步到服務器。
優點
-
類型系統保證了你的數據將是一個一致的形狀。
-
自動跟蹤依賴關係意味着 MST 可以智能地只重新渲染需要的組件。
-
變更是以顆粒狀補丁流的形式創建的。
-
簡單地對整個或部分狀態進行可序列化的 JSON 快照。
缺點
-
你需要學習 MST 的類型系統。
-
魔幻與顯性的權衡
-
補丁、快照和動作的一些性能開銷。如果你的數據變化非常快,MST 可能不是最合適的。
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 非常相似的簡單 API
-
它被 Facebook 用在他們的一些內部工具中。
-
爲性能而設計
-
可與 React Suspense 一起工作,也可不與 React Suspense 一起工作(在撰寫本文時,React Suspense 仍在試驗階段)。
缺點
- 這個庫成立才幾個月,所以社區資源和最佳實踐還沒有其他庫那麼強大。
React-Query
React-Query 與列表中的其他庫不同,因爲它是一個獲取數據的庫,而不是一個狀態管理庫。
我把它放在這裏,是因爲通常情況下,應用程序中的狀態管理有很大一部分是圍繞着加載數據、緩存、顯示 / 清除錯誤、在正確的時間清除緩存(或者在沒有清除的時候遇到 bug)等等...... 而 react-query 很好地解決了所有這些問題。
優勢
-
將數據保存在每個組件都能訪問的緩存中。
-
可以自動重新獲取 (停滯 - 同時 - 驗證、窗口重新聚焦、輪詢 / 實時)
-
支持獲取分頁數據
-
支持 "加載更多" 和無限滾動數據,包括滾動位置恢復。
-
你可以使用任何 HTTP 庫(fetch,axios 等)或後端(REST,GraphQL)。
-
支持 React Suspense,但不要求它。
-
並行 + 依賴性查詢
-
突變 + 反應式重取("在我更新這個項目後,重取整個列表")。
-
支持取消請求
-
用自己的 React Query Devtools 進行良好的調試。
-
bundle 尺寸小(6.5k minified + gzipped)。
缺點
- 如果你的要求很簡單,可能會矯枉過正。
XState
最後一個也不是真正意義上的狀態管理庫,和這個列表中的其他庫一樣,但它非常有用!
XState 用 JavaScript(和 React,但它可以與任何框架一起使用))實現了狀態機和狀態圖。狀態機是一個 "衆所周知" 的想法(在學術文獻的意義上),已經存在了幾十年,它們在解決棘手的狀態問題方面做得非常好。
當很難推理出一個系統可以採取的所有不同組合和狀態時,狀態機是一個很好的解決方案。
舉個例子,想象一個複雜的自定義輸入,比如 Stripe 公司的那些花哨的信用卡號碼輸入 -- 這些輸入能夠精確地知道什麼時候在數字之間插入空格,以及將光標放在哪裏。
現在想想:當用戶點擊右鍵時,你應該怎麼做?嗯,這取決於光標的位置。而這取決於框中的文字是什麼 (光標是否在我們需要跳過的空格附近? 沒有?)。而且也許他們按住 Shift 鍵,你需要調整所選區域...... 有很多變量在起作用。你可以看到這將如何變得複雜。
手工管理這種事情是很棘手的,而且容易出錯,因此使用狀態機,你可以列出系統可能處於的所有狀態,以及它們之間的轉換。XState 將幫助你做到這一點。
優勢
-
簡單的基於對象的 API 來表示狀態和它們的轉換。
-
可以處理複雜的情況,如平行狀態
-
XState Visualizer 對於調試和步入狀態機真的很不錯。
-
狀態機可以大幅簡化複雜的問題。
缺點
-
用狀態機思考 " 需要適應一下
-
狀態機描述對象可能會變得相當囉嗦(但是,想象一下,用手寫它
"X 怎麼辦?"
還有很多庫我在這裏沒有篇幅介紹,比如 Zustand、easy-peasy 等。不過可以看看這些,它們也不錯:)
學習狀態管理的技巧
小例子對學習很有好處,但往往會讓一個庫顯得矯枉過正。("誰需要 Redux 來做 TODO 列表?" "爲什麼你要爲一個模態對話框使用整個狀態機?")
大的例子很適合看如何將一件事付諸實踐,但作爲介紹往往讓人不知所措。("哇,這些狀態機的東西看起來太複雜了")
就我個人而言,當我剛開始接觸一件事情的時候,我會先從那些 "愚蠢" 的小例子開始,即使我真正的目標是更大的事情。我發現現實世界的例子很容易讓人迷失在草叢中。
祝你在自己的狀態管理之路上好運:)
關於本文 譯者:@飄飄 作者:@Dave 原文:https://daveceddia.com/react-state-management/
本文由 Readfog 進行 AMP 轉碼,版權歸原作者所有。
來源:https://mp.weixin.qq.com/s/jrxaa3iJBb6X9tMqbmiFNw