React 狀態管理庫的battle (setState/useState vs Redux vs Mobx)

2021-02-24 前端之露

原文連結:https://dev.to/mpodlasin/my-thoughts-on-endless-battle-of-react-state-management-libraries-setstate-usestate-vs-redux-vs-mobx-2kal

與我 以前的文章相比,這將是一篇更加具有爭議的文章。因此,親愛的讀者,請親噴——這只是我個人對於React 中的狀態管理問題的想法和思考

你為什麼應該聽聽我的看法?

我將 React 應用在大型商業項目中,該項目使用了三種當前最流行的狀態管理方法:


因此,在本文中,我將比較這三個選擇。

我的目標是為您提供關於這些狀態管理方法相對中立的觀點。更進一步,我將給出關於在 React 應用程式中為什麼狀態管理變得如此重要,以至於引發人們編寫無數的庫,文章和會議演講的這個話題的觀點。

讓我們開始吧!

狀態的起源

當我第一次學習前端開發時,沒有人談論「狀態管理」。沒有人真正關心狀態。

在我開發的第一個商業應用程式中,是用不朽的 jQuery 庫編寫的,人們只是將狀態存儲在一些隨機的地方(某些HTML元素的「data-*」屬性),或者根本不把它存放在任何地方。

在第二種情況下,讀取狀態意味著簡單地檢查 DOM 中當前呈現的內容。對話窗口是否打開?沒有布爾值告訴我們,所以只是檢查一下樹中是否有一個具有某些特殊類或 id 的 DOM 元素!

當然,這種方法導致了極端混亂和錯誤的代碼庫,所以 React 中,應用程式的狀態與視圖明顯分離,對我們來說是一個頓悟,也是應用狀態的概念在我們腦海中根深蒂固的時刻。

React狀態機制(經典和現代)

自從 React 將狀態當成獨立的概念提出時,它也提供了一些簡單的工具來管理狀態。

早先只是一個setState方法,這個方法允許更改我們組件中存儲的狀態,現在我們又有了一個useState的hook,有一些微小的差異,但最終出於相同的目的——定義和修改每個組件基礎的狀態。

現在最後一個關鍵點。在「React」中,每個狀態都被定義為在組件「內部」。所以假設FirstComponent將有一個獨立於SecondComponent的 state,甚至是每個實例會有FirstComponent自己的 state 實例。(跳出思維定式)這意味著React 組件之間沒有狀態共享。每個組件都有自己的 state 實例,React 創建了一個管理程序,僅此而已!

但事實卻是,我們通常希望在網站的不同位置(即在不同的組件中)顯示相同的狀態。

例如,應用程式頂部的 Facebook 標題中的新消息數量應當始終等於 messenger 窗口本身底部的未讀消息數量。

具有共享狀態(消息列表,其中一些被標記為「未讀」)來確保兩個組件始終顯示相同的信息將會輕而易舉。

Messenger組件將簡單地展示列表中的消息,用粗體標記未讀的消息。同時Header組件將計算列表上有多少消息被標記為未讀,並向用戶顯示該數字。

一種方案是,有兩個獨立的 state 副本——一個在Header組件和一個Messenger組件——有可能會導致這些狀態不同步。例如,用戶可能會看到在Header中有2條未讀,但隨後他在Messenger中卻找不到任何未讀消息。那肯定會很惱火。

那麼,我們如何只使用React而不使用任何額外的庫就實現狀態共享呢?

共享狀態的規範方法是將其存儲在組件樹中的共同父節點組件中。然後你可以簡單地把這個 state 作為 props 傳遞下去。這樣你就可以通過 props 將相同的狀態傳遞給兩個單獨的組件。砰!這兩個組件現在共享這個 state。

一開始效果很好。但是如果你用這種方式編寫你的應用程式(如果它們變得足夠複雜),隨著時間的轉移,你會很快注意到你的很多 state 會「冒泡」。

隨著越來越多的組件需要訪問相同的state,你進入狀態需要放到組件樹中越來越高的位置,直到最終到達最頂層的組件。

所以你最終會得到一個巨大的「容器」組件,它基本上存儲了你所有的state。它被幾十種方法來操縱,並通過幾十個props來將其傳遞到幾十個組件中。

這很快變得難以控制。實際上,沒有簡單的方法可以將代碼分割成更小的部分。最後你會得到一個龐大的組件文件,它通常是數千行代碼。

你最終會遇到和你之前一樣的混亂,你用 React 把 state 從 view 中分開。哎呀……

Redux 救援

發明 Redux 的原因與上文所述有所不同。實際上,它純粹是一種演示工具,旨在提供開發React應用程式時的「時間旅行」的能力。

事實證明,如果將所有狀態都放在一個位置(稱為「store」),並且總是一步一步地更新所有狀態(使用「reducer」函數),則基本上可以「時間旅行」 。由於您可以序列化保存在存儲中的state並在每次更新後保存,因此您可以保留所有過去state的歷史記錄。

然後,您可以簡單地根據命令返回到任何過去的state,將其再次加載回store。你現在是在「時間旅行」——可以看到你的應用的歷史記錄。

「時間旅行」被認為是一種有利於開發和調試React應用程式的方法。這聽起來很棒,人們立刻蜂擁而至。

但事實證明,這種能力並不像人們最初認為的那樣有用。實際上,我相信大多數當前現有的Redux應用程式都不會特別的去使用時間旅行,即使是出於調試目的。對於值得使用的場景,這簡直太過麻煩了(我仍然更加依賴console.log來調試)。

然而,我認為從一開始 Redux 就應該成為編寫複雜 React 應用程式的主要工具。

Redux 中的 state不是在任何基礎組件裡創建的,相反,是存儲在一個中央的內存資料庫中(前面提到的store)

因此,潛在的任何組件都可以訪問這個state,而無需通過props傳遞它,這太麻煩了。在Redux中,任何組件都可以直接訪問這個store,只需使用特殊的工具函數即可。

這意味著只需很少的努力,你保存在 store 中的任何數據都可以在應用程式的任何位置顯示。

由於多個組件可以同時訪問狀態而沒有任何問題,因此狀態共享也不再是一個問題。

我們的 Facebook 網站現在可以在我們想要的任何地方顯示未讀消息的數量,由保留在 store 中的消息列表提供。

將所有的state保存在一個地方聽起來有點類似於我們將state保存在一個單獨的組件中。但事實證明,由於 Redux 存儲更新是由 reducer 函數完成的,而函數非常容易組合,將我們的 Redux 代碼庫劃分為多個文件,按域或責任劃分也比管理一個大型「容器」組件容易。

所以 Redux 聽起來真的像是我們之前描述的所有問題的解決方案。看起來在React中的狀態管理問題已經解決了,現在我們可以繼續討論更有趣的問題。

然而,就像生活一樣,事實並不是那麼簡單。

還有兩個我們還沒有描述清楚的Redux問題。

儘管組件可以直接讀取 Redux store,但它們不能直接更新store。必須用「actions」來請求store自己更新。

最重要的是,Redux 被認為是一種同步機制,因此為了執行任何初始化任務(例如針對該問題的HTTP請求),您需要使用「中間件」來授予異步操作Redux的能力。

所有這些s tore,reducers,actions,middleware(以及一大堆額外的樣板代碼)使 Redux代碼極其冗長。

在Redux中更改一個簡單的功能通常會導致修改多個文件。對於新手來說,跟蹤 Redux應用程式中正在發生的事情非常困難。一開始看起來很簡單的事情——把所有的狀態存儲在一個地方——很快變成了極其複雜的架構,有人實際上需要幾周的時間來適應。

人們意識到。Redux成功後,出現了大量的各種的狀態管理庫。

這些狀態管理庫大多數都有一個共同點——他們試圖做和 Redux 完全一樣的事情,但是用較少的樣板代碼。

Mobx 成為最火的其中之一。

Mobx的魔法

與 Redux 對函數式編程的關注相反,Mobx決定擁抱老式的面向對象編程(OOP) 理念。

它保留了 Redux 的 store的概念,但是是一個具有某些屬性的類。它保留了Redux的action的概念,但是只是簡單的方法。

不再有 reducers,因為您可以像在常規類實例中一樣更新對象屬性。不再有中間件,因為 Mobx 中的方法可以是同步和異步,這使得機制更加靈活。

有趣的是,理念保持不變,但實現方式卻大不相同。至少在乍看之下,它形成了一個比Redux更輕巧的框架。

最重要的是,Mobx用的是普通軟體開發人員所熟悉的語言。幾十年來,面向對象程序設計是典型的程式設計師培訓的一部分,因此,絕大多數使用 React 的程式設計師都非常熟悉根據類,對象,方法和屬性來管理狀態。

再一次我們似乎已經解決了問題——我們現在有了一個狀態管理庫,該庫保留了Redux的思想和優點,同時又不那麼冗長,並且對新手來說不那麼陌生。

那麼問題出在哪裡呢?事實證明,儘管 Redux 複雜且冗長,但 Mobx 掩飾了其複雜性,偽裝成大多數開發人員熟悉的編程模型。

事實證明,與傳統的OOP相比,Mobx與 Rx.js 甚至 Excel 有更多的共同點。Mobx 看起來像面向對象的編程,而實際上,它的核心機制是基於完全不同的理念,與 Redux 提倡的函數式編程相比,它對常規編程器的影響更大。

Mobx不是 OOP 庫。它是一個反應式編程庫,被隱藏在類,對象和方法的語法下。

問題是,當您使用 Mobx 對象並修改其屬性時,Mobx 必須以某種方式通知 React 狀態已發生更改。為了實現這一點,Mobx 具有一種受反應式編程概念啟發的機制。當對該屬性進行更改時,Mobx 會「通知」正在使用該屬性的所有組件,並且作為響應,這些組件現在可以重新渲染。

到目前為止,可以簡單並且完美地工作,這是 Mobx 可以用很少的樣板代碼就可以實現 Redux 的大部分功能的原因之一。

但是 Mobx 的反應性並不止於此。

一些狀態值取決於其他狀態值。例如,許多未讀消息直接取決於消息列表。當新消息出現在列表中時,未讀消息的數量應隨之增加。

因此,在Mobx中,當屬性更改時,庫機制不僅會通知顯示該屬性的React組件,而且還會通知依賴於該屬性的其他屬性。

它的工作原理就像Excel,在那裡你改變一個單元格的值之後,依賴於該值的單元格也會立即更新。

此外,其中某些屬性是以異步方式計算的。例如,如果您的媒體資源是文章ID,則可能要從後端獲取該文章的標題和作者。這是兩個新屬性——title和author——直接取決於先前的屬性(文章 ID) 。但是它們不能以同步方式計算。我們需要發出一個異步HTTP請求,等待響應,處理可能發生的任何錯誤,然後我們才能更新title和author屬性。

當您開始深入挖掘時,您會發現 Mobx 具有處理這些情況的大量的機制和工具,並且 Mobx 文檔明確鼓勵這種編程風格。您開始意識到 Mobx 只是表面上看起來是 OOP,而實際上是完全不同的理念。

而且,事實證明,在足夠大的應用程式中,這種特性及其依賴關係圖很快變得異常複雜。

如果您曾經看到過一個龐大而複雜的大型Excel文件,以至於每個人都害怕對其進行任何更改——您基本上已經看到了Mobx應用程式。

但是,Mobx 反應性機制對開發人員來說是不可見的。如前所述,它是在類,方法和裝飾器的OOP語法下隱藏的。

因此,從程式設計師的角度來看,Mobx 所做的很多事情都是「神奇的」。我花了許多時間撓頭,試圖弄清在某些情況下 Mobx 的機制為什麼會(或不會)進行一些更新。有些時候我的代碼神秘地發送了多個HTTP請求而不是一個。也有一些時候我的代碼沒有發送任何請求,即使我可以發誓應該需要發送。

當然最後,錯誤總是在我這邊。Mobx 完全可以正常工作。

儘管 Redux 很複雜,因為它基本上將所有內容都交給了你,並要求你進行管理,但 Mobx 卻恰恰相反,它通過隱藏複雜之處,並假裝它只是一個「常規」的 OOP庫。

一種方法導致代碼充滿樣板,多個文件,並且難以跟蹤代碼不同部分之間的關係。

第二種方法使代碼看起來苗條而優雅,但是有時它會執行您不希望且難以分析的事情,因為您實際上不理解庫的底層功能。

狀態管理的謊言

有趣的是,整篇文章的前提是共享狀態是許多現代Web應用程式的共同要求。

但是...真的嗎?

我的意思是,當然,有時您必須在應用程式中兩個完全不同的位置顯示許多未讀消息。

但這是否足以需要創建複雜的狀態管理解決方案?

也許……也許我們真正需要的只是一種以可管理的方式在組件之間共享狀態的方法?

我正在想像有一個useSharedState鉤子,它可以像常規的 React 狀態鉤子一樣工作,但是將允許組件訪問相同的狀態實例,例如通過共享預定義的鍵:

const [count, setCount] = useSharedState(0, "UNREAD_MESSAGES_COUNT");


實際上,這個想法一點也不陌生。我至少看到了類似於此鉤子的幾種實現。

似乎人們(有意識或無意識地)都需要這種解決方案。

當然,它還不能解決所有問題。最大的問題是,異步代碼(尤其是數據獲取)在 React 中仍然比較難處理,並且以 react hooks 語法實現它幾乎就像是黑客(事實上,我可能會寫一篇有關該確切問題的後續文章)。

但是我仍然會保留我有爭議的主張,我在本文開始時就向您保證:

所有與狀態管理有關的辯論、創建的庫和編寫的文章,全部都是因為——React中沒有簡單的方法可以在組件之間共享狀態實例。

現在要記住——我從來沒有機會使用這個假想的useSharedStatehook編寫完整的商業應用程式。正如我提到的那樣,要使這樣的應用程式真正易於開發和維護還需要做一些事情。

因此,我現在所說的一切都可能完全被誤導,但無論如何我都會說:我們在React中過度設計了狀態管理。

在 React 中使用狀態已經接近一個完美的解決方案——從視圖中分離狀態是一個巨大的墊腳石——我們僅缺少一些針對非常具體問題的小的解決方案,例如共享狀態或異步獲取數據。

我們不需要狀態管理框架及庫,只需要對核心反應機制進行一些調整(或者只是外部庫中的一些小的工具)。

編寫大型網絡應用程式總是很複雜。狀態管理很難。事實上,你的應用程式越大,難度將是指數級的增長。

但是我相信,學習,調試和馴服狀態管理庫所花費的時間和努力可以轉而用於重構您的應用程式,更仔細地構建它,更好地組織代碼。

這將導致整個團隊更簡單,更容易理解和更容易管理的代碼。

我看到這是一個React社區已經在慢慢做的轉變,越來越多人覺得用 Redux 或 Mobx 編程讓人覺得沮喪。

因此,今天我用什麼?

當然,Redux 和 Mobx 仍然有他們的位置。它們是真正偉大的狀態管理庫。解決了非常具體的問題,並帶來了特定的優點(同時也帶來了特定的缺點)。

如果您想用時間旅行調試,或者需要將可序列化狀態存儲在一個地方(例如,將其保存在此處或本地存儲中),那麼Redux是適合你的。

如果您的應用程式狀態是高度互連的,並且您希望確保一個屬性的更新將導致其他屬性的立即更新,那麼Mobx模型將非常適合這個場景。

如果你沒有任何具體的要求,就從普通的React開始。

我在那篇文章中描述了一些「普通React」方法的問題,但是在實踐中自己遇到這些問題是另外一回事。有了這種經驗,您將更明智地決定選擇哪種狀態管理解決方案。

或不選擇。;)

感謝閱讀!

架構師喜歡用的架構圖工具

堪稱玄學的 Tree Shaking

資深獵頭告訴你為啥找不到好工作?

使用語雀 Webhooks 和 Netlify Functions 自動同步生成 Github Profile README

[架構師選型之路]——文件轉換開源庫

如何在公司落地業務組件

文件靈異失蹤燒腦破案

相關焦點

  • 淺談React 中的state 與useEffect
    許多非同步的操作會用redux,並不代表非同步操作一定得用redux。在有些情境之下,redux 其實是可以不需要用的。以原po 的例子來講,他就是要寫個簡單的搜尋功能,為什麼要用redux?通常會需要用redux 跟它的middleware 有幾個原因:你某些狀態必須讓很多不同的元件存取,所以要放在一個global 的地方,比較好拿某些非同步操作流程比較複雜,透過redux-saga 或是redux-observable 輔助會讓程式碼的可維護性更好
  • Vuex、Flux、Redux、Redux-saga、Dva、MobX
    不管是Vue,還是 React,都需要管理狀態(state),比如組件之間都有共享狀態的需要。什麼是共享狀態?比如一個組件需要使用另一個組件的狀態,或者一個組件需要改變另一個組件的狀態,都是共享狀態。為了簡單處理 Redux 和 React UI 的綁定,一般通過一個叫 react-redux 的庫和 React 配合使用,這個是 react 官方出的(如果不用 react-redux,那麼手動處理 Redux 和 UI 的綁定,需要寫很多重複的代碼,很容易出錯,而且有很多 UI 渲染邏輯的優化不一定能處理好)。
  • Redux數據狀態管理
    /actions';// 導出reducer和actionexport { reducer, actions };import React, { memo } from 'react';import { connect } from 'react-redux';import { setLoginToken
  • 探尋 Redux useSelector 更新機制
    但如果每次 redux store 一有變化,所有用到 react-redux 的useSelector的組件就重新 render,這就會導致一個很大的性能問題。那麼useSelector是如何做到的呢?
  • JavaScript教程:React Hook之useState和useEffect
    Hooks可以從組件中提取有狀態邏輯,以便可以獨立測試並重複使用。其允許在不更改組件層次結構的情況下重用有狀態邏輯。這樣可以輕鬆地在多個組件之間或與社區共享Hook。本篇將著重講解Hook的useState以及useEffect,Hook還有其他內置函數,後續講解。
  • React-Redux 使用 Hooks
    快速搭建 redux 環境    create-react-app redux-app    yarn  add react-redux reduxTips: 確保 react-redux 版本 在 v7.1.0 以上
  • 【React源碼筆記】setState原理解析
    React把組件看成是一個State Machines狀態機,首先定義數值的狀態state,通過用戶交互後狀態發生改變,然後更新渲染UI。也就是說更新組件的state,然後根據新的state重新渲染更新用戶的界面。而在編寫類組件時,通常分配state的地方是construtor函數。
  • 【重學React】動手實現一個react-redux
    react-redux 是什麼react-redux 是 redux 官方 React 綁定庫。它幫助我們連接UI層和數據層。本文目的不是介紹 react-redux 的使用,而是要動手實現一個簡易的 react-redux,希望能夠對你有所幫助。
  • 3-6-react-redux
    或者單獨使用了redux,接下來呢,讓我們來繼續重構之前的計數器的例子,不過這一回呢,我們要正式地協同使用react和redux了:const counter = (state = 0, action) => {  switch (action.type) {    case 'INCREMENT':      return state + 1;
  • React系列八 - 深入理解setState
    () {    this.state.message = "你好啊,李銀河";  }我們必須通過setState來更新數據:疑惑:在組件中並沒有實現setState的方法,為什麼可以調用呢?:可見setState是異步的操作,我們並不能在執行完setState之後立馬拿到最新的state的結果changeText() {  this.setState({    message: "你好啊,李銀河"  })  console.log(this.state.message); // Hello World
  • 面試題:React中setState是異步還是同步?
    在學習react的過程中幾乎所有學習材料都會反覆強調一點setState是異步的,來看一下react官網對於setState的說明。
  • React + Redux + React-router 全家桶
    是 Web 應用是一個狀態機,用來確保視圖與狀態是一一對應的(即一個 State 對應一個 View),用戶發出 Action,Reducer 函數算出新的 State,View 重新渲染。StoreStore是由redux的createStore函數生成。所有的狀態,保存在對象Store裡面,整個應用只有唯一一個Store。
  • React-redux數據傳遞是如何實現的?
    主要內容React數據傳遞reduxReact-redux其他學習目標第一節 react數據傳遞react 中組件之間數據傳遞1. 父傳子2. 子傳父(狀態提升)3.兄弟之間傳遞需要把數據上傳到共有的父級身上,然後再通過父級向下傳,傳到指定的子級上本節作業兄弟元素之間數據傳遞兩個具有共同祖先級的元素之間數據傳遞第二節 redux1. 介紹:Redux 是 JavaScript 狀態容器,提供可預測化的狀態管理。
  • 【React】853- 手摸手教你基於Hooks 的 Redux 實戰姿勢
    Redux 使您可以集中存放 JavaScript 應用程式的狀態(數據)它最常與 React 一起使用(通過 react-redux )這使您可以從樹中的任何組件訪問或更改狀態。應用的狀態被集中存放於 Redux store該 store 是使用稱為 「reducer」 的函數所創建的reducer 接受一個 state 和一個 action , 並返回相同或新的狀態
  • React Hook起飛指南
    import { useState } from 'react';const Example = () => { const [count, setCount] = useState(0); return ( <div> <p>You clicked {count} times</p>
  • state machine replication vs primary backup system
    然後zab, vr 經常提到他們的一個primary backup system, 與replicate state machine 還是有不同的. 雖然他們都有state machine, consensus  module, log.
  • React:useHooks小竅門
    import { useReducer, useCallback } from 'react';// Usagefunction App() { const { state, set, undo, redo, clear, canUndo, canRedo } = useHistory({}); return ( <div className
  • react源碼分析之-setState是異步還是同步?
    pre state', this.state.count); this.setState({ count: this.state.count + 1 }); console.log('setTimeout next state', this.state.count); }, 0); }
  • 深入React技術棧之setState詳解
    () { this.setState({value: this.state.value + 1}) console.log(this.state.value) // 第一次輸出 this.setState({value: this.state.value + 1}) console.log(this.state.value) // 第二次輸出 setTimeout(()
  • 關於react結合redux使用,或許你還應該掌握這些
    父組件的state可以作為子組件的props來傳遞數據,當state改變時,props也隨之改變,但是props本身是不可改變的。在項目當中,不同層級的頁面之間往往需要傳遞數據。當需要傳值的頁面數量變多的情況下,傳值關係可能會發生混亂。這時候要是有一個容器,能夠幫助管理react的state的狀態,那就很nice。接下來就是redux登場的時刻了!什麼是redux以及其作用?