聊聊兩個狀態(tài)管理庫 Redux & Recoil

背景
React 是一個十分優(yōu)秀的UI庫, 最初的時候, React 只專注于UI層, 對全局狀態(tài)管理并沒有很好的解決方案, 也因此催生出類似Redux 這樣優(yōu)秀的狀態(tài)管理工庫。
隨著時間的演變, 又催化了一批新的狀態(tài)管理工具。
我簡單整理了一些目前主流的:
ReduxReact Context & useReducerMobxRecoilreact-sweet-statehox
這幾個都是我接觸過的,Npm 上的現(xiàn)狀和趨勢對比:


毫無疑問,React?和?Redux?的組合是目前的主流。
今天5月份, 一個名叫?Recoil.js?的新成員進入了我的視野,帶來了一些有趣的模型和概念,今天我們就把它和 Redux 做一個簡單的對比, 希望能對大家有所啟發(fā)。
正文
先看 Redux:
Redux
React-Redux 架構圖:

這個模型還是比較簡單的, 大家也都很熟悉。
先用一個簡單的例子,回顧一下整個模型:
actions.js
export const UPDATE_LIST_NAME = 'UPDATE_NAME';reducers.js
export const reducer = (state = initialState, action) => {const { listName, tasks } = state;switch (action.type) {case 'UPDATE_NAME': {// ...}default: {return state;}}};
store.js
import reducers from '../reducers';import { createStore } from 'redux';const store = createStore(reducers);export const TasksProvider = ({ children }) => ({children});
App.js
import { TasksProvider } from './store';import Tasks from './tasks';const ReduxApp = () => ();
Component
// componentsimport React from 'react';import { updateListName } from './actions';import TasksView from './TasksView';const Tasks = (props) => {const { tasks } = props;return ();};const mapStateToProps = (state) => ({tasks: state.tasks});const mapDispatchToProps = (dispatch) => ({updateTasks: (tasks) => dispatch(updateTasks(tasks))});export default connect(mapStateToProps, mapDispatchToProps)(Tasks);
當然也可以不用connect,?react-redux?提供了?useDispatch, useSelector?兩個hook, 也很方便。
import { useDispatch, useSelector } from 'react-redux';const Tasks = () => {const dispatch = useDispatch();const name = useSelector(state => state.name);const setName = (name) => dispatch({ type: 'updateName', payload: { name } });return ();};

整個模型并不復雜,而且redux?還推出了工具集redux toolkit,使用它提供的createSlice方法去簡化一些操作, 舉個例子:
// Actionexport const UPDATE_LIST_NAME = 'UPDATE_LIST_NAME';// Action creatorexport const updateListName = (name) => ({type: UPDATE_LIST_NAME,payload: { name }});// Reducerconst reducer = (state = 'My to-do list', action) => {switch (action.type) {case UPDATE_LIST_NAME: {const { name } = action.payload;return name;}default: {return state;}}};export default reducer;
使用?createSlice:
// src/redux-toolkit/state/reducers/list-nameimport { createSlice } from '@reduxjs/toolkit';const listNameSlice = createSlice({name: 'listName',initialState: 'todo-list',reducers: {updateListName: (state, action) => {const { name } = action.payload;return name;}}});export const {actions: { updateListName },} = listNameSlice;export default listNameSlice.reducer;
通過createSlice, 可以減少一些不必要的代碼, 提升開發(fā)體驗。
盡管如此, Redux 還有有一些天然的缺陷:
概念比較多,心智負擔大。 屬性要一個一個 pick,計算屬性要依賴 reselect。還有魔法字符串等一系列問題,用起來很麻煩容易出錯,開發(fā)效率低。 觸發(fā)更新的效率也比較差。對于connect到store的組件,必須一個一個遍歷,組件再去做比較,攔截不必要的更新, 這在注重性能或者在大型應用里, 無疑是災難。
對于這個情況, React 本身也提供了解決方案, 就是我們熟知的?Context.

{value => /* render something based on the context value */}
給父節(jié)點加 Provider 在子節(jié)點加 Consumer,不過每多加一個 item 就要多一層 Provider, 越加越多:

而且,使用Context?問題也不少。
對于使用?useContext?的組件,最突出的就是問題就是?re-render.
不過也有對應的優(yōu)化方案:?React-tracked.
稍微舉個例子:
// store.jsimport React, { useReducer } from 'react';import { createContainer } from 'react-tracked';import { reducers } from './reducers';const useValue = ({ reducers, initialState }) => useReducer(reducer, initialState);const { Provider, useTracked, useTrackedState, useUpdate } = createContainer(useValue);export const TasksProvider = ({ children, initialState }) => ({children});export { useTracked, useTrackedState, useUpdate };
對應的,也有?hooks?版本:
const [state, dispatch] = useTracked();const dispatch = useUpdate();const state = useTrackedState();// ...
Recoil
Recoil.js 提供了另外一種思路, 它的模型是這樣的:

在 React tree 上創(chuàng)建另一個正交的 tree,把每片 item 的 state 抽出來。
每個 component 都有對應單獨的一片 state,當數(shù)據(jù)更新的時候對應的組件也會更新。
Recoil 把 這每一片的數(shù)據(jù)稱為 Atom,Atom 是可訂閱可變的 state 單元。
這么說可能有點抽象, 看個簡單的例子吧:
// index.js
import React from "react";import ReactDOM from "react-dom";import { RecoilRoot } from "recoil";import "./index.css";import App from "./App";import * as serviceWorker from "./serviceWorker";ReactDOM.render(,document.getElementById("root"));
Recoil Root
Provides the context in which atoms have values. Must be an ancestor of any component that uses any Recoil hooks. Multiple roots may co-exist; atoms will have distinct values within each root. If they are nested, the innermost root will completely mask any outer roots.
可以把 RecoilRoot 看成頂層的 Provider.
Atoms
假設, 現(xiàn)在要實現(xiàn)一個counter:

先用 useState 實現(xiàn):
import React, { useState } from "react";const App = () => {const [count, setCount] = useState(0);return (Count is {count});};export default App;
再用 atom 改寫一下:
import React from "react";import { atom, useRecoilState } from "recoil";const countState = atom({key: "counter",default: 0,});const App = () => {const [count, setCount] = useRecoilState(countState);return (Count is {count});};export default App;
看到這, 你可能對atom 有一個初步的認識了。
那 atom 具體是個什么概念呢?
Atom
簡單理解一下,atom 是包含了一份數(shù)據(jù)的集合,這個集合是可共享,可修改的。
組件可以訂閱atom, 可以是一個, 也可以是多個,當 atom 發(fā)生改變時,觸發(fā)再次渲染。
const someState = atom({key: 'uniqueString',default: [],});
每個atom 有兩個參數(shù):
key:用于內(nèi)部識別atom的字符串。相對于整個應用程序中的其他原子和選擇器,該字符串應該是唯一的。default:atom的初始值。
atom 是存儲狀態(tài)的最小單位, 一種合理的設計是, atom 盡量小, 保持最大的靈活性。
Recoil 的作者, 在 ReactEurope video 中也介紹了以后一種封裝定atom 的方法:
export const itemWithId =memoize(id => atom({key: `item${id}`,default: {...},}));
Selectors
官方描述:
“A selector is a pure function that accepts atoms or other selectors as input. When these upstream atoms or selectors are updated, the selector function will be re-evaluated.”
selector 是以 atom 為參數(shù)的純函數(shù), 當atom 改變時, 會觸發(fā)重新計算。
selector 有如下參數(shù):
key:用于內(nèi)部識別 atom 的字符串。相對于整個應用程序中的其他原子和選擇器,該字符串應該是唯一的.get:作為對象傳遞的函數(shù){ get },其中get是從其他案atom或selector檢索值的函數(shù)。傳遞給此函數(shù)的所有atom或selector都將隱式添加到selector的依賴項列表中。set?:返回新的可寫狀態(tài)的可選函數(shù)。它作為一個對象{ get, set }和一個新值傳遞。get是從其他atom或selector檢索值的函數(shù)。set是設置原子值的函數(shù),其中第一個參數(shù)是原子名稱,第二個參數(shù)是新值。
看個具體的例子:
import React from "react";import { atom, selector, useRecoilState, useRecoilValue } from "recoil";const countState = atom({key: "myCount",default: 0,});const doubleCountState = selector({key: "myDoubleCount",get: ({ get }) => get(countState) * 2,});const inputState = selector({key: "inputCount",get: ({ get }) => get(doubleCountState),set: ({ set }, newValue) => set(countState, newValue),});const App = () => {const [count, setCount] = useRecoilState(countState);const doubleCount = useRecoilValue(doubleCountState);const [input, setInput] = useRecoilState(inputState);return (setInput(Number(e.target.value))} />Count is {count}Double count is {doubleCount});};export default App;
比較好理解,?useRecoilState,?useRecoilValue?這些基礎概念可以參考官方文檔。
另外, selector 還可以做異步, 比如:
get: async ({ get }) => {const countStateValue = get(countState);const response = await new Promise((resolve) => setTimeout(() => resolve(countStateValue * 2)),1000);return response;}
不過對于異步的selector, 需要在RecoilRoot加一層Suspense:
ReactDOM.render(Loading...
, document.getElementById("root"));Redux vs Recoil
模型對比:

Recoil 推薦 atom 足夠小, 這樣每一個葉子組件可以單獨去訂閱, 數(shù)據(jù)變化時, 可以達到 O(1)級別的更新.
Recoil 作者?Dave McCabe?在一個評論中提到:
Well, I know that on one tool we saw a 20x or so speedup compared to using Redux. This is because Redux is O(n) in that it has to ask each connected component whether it needs to re-render, whereas we can be O(1).
useReducer is equivalent to useState in that it works on a particular component and all of its descendants, rather than being orthogonal to the React tree.
Rocil 可以做到 O(1) 的更新是因為,當atom數(shù)據(jù)變化時,只有訂閱了這個 atom 的組件需要re-render。
不過, 在Redux 中,我們也可以用selector 實現(xiàn)同樣的效果:
// selectorconst taskSelector = (id) => state.tasks[id];// component codeconst task = useSelector(taskSelector(id));
不過這里的一個小問題是,state變化時,taskSelector 也會重新計算, 不過我們可以用createSelector?去優(yōu)化, 比如:
import { createSelector } from 'reselect';const shopItemsSelector = state => state.shop.items;const subtotalSelector = createSelector(shopItemsSelector,items => items.reduce((acc, item) => acc + item.value, 0))
寫到這里, 是不是想說,就這?扯了這么多, Rocoil 能做的, Redux 也能做, 那要你何用?
哈哈, 這個確實有點尷尬。
不過我認為,這是一種模式上的改變,recoil 鼓勵把每一個狀態(tài)做的足夠小, 任意組合,最小范圍的更新。
而redux, 我們的習慣是, 把容器組件連接到store上, 至于子組件,哪怕往下傳一層,也沒什么所謂。
我想,Recoil 這么設計,可能是十分注重性能問題,優(yōu)化超大應用的性能表現(xiàn)。
目前,recoil 還處于玩具階段, 還有大量的 issues 需要處理, 不過值得繼續(xù)關注。
最后
感興趣的朋友可以看看, 做個todo-list體驗一下。
希望這篇文章能幫到你。
才疏學淺,文中若有錯誤, 歡迎指正。
