聊聊 React 兩個狀態管理庫 Redux & Recoil

State Management in React Apps | WalkingTree Technologies

背景

React 是一個十分優秀的UI庫, 最初的時候, React 只專一於UI層, 對全局狀態管理並無很好的解決方案, 也所以催生出相似Flux, Redux 等優秀的狀態管理工具。css

隨着時間的演變, 又催化了一批新的狀態管理工具。html

簡單整理了一些目前主流的狀態管理工具:前端

  1. Redux
  2. React Context & useReducer
  3. Mobx
  4. Recoil
  5. react-sweet-state
  6. hox

這幾個都是我接觸過的,Npm 上的現狀和趨勢對比react

image.png

image.png

毫無疑問,ReactRedux 的組合是目前的主流。面試

今天5月份, 一個名叫 Recoil.js 的新成員進入了個人視野,帶來了一些有趣的模型和概念,今天咱們就把它和 Redux 作一個簡單的對比, 但願能對你們有所啓發。npm

正文

先看 Redux:redux

Redux

React-Redux 架構圖:api

image.png

這個模型仍是比較簡單的, 你們也都很熟悉。架構

先用一個簡單的例子,回顧一下整個模型:app

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 }) => (
    <Provider store={store}>
       {children}
    </Provider>
);

App.js

import { TasksProvider } from './store';
import Tasks from './tasks';
const ReduxApp = () => (
    <TasksProvider>
       <Tasks />
   </TasksProvider>
);

Component

// components
import React from 'react';
import { updateListName } from './actions';
import TasksView from './TasksView';

const Tasks = (props) => {
    const { tasks } = props;
    return (
        <TasksView tasks={tasks} />
    );
};

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 (
        <TasksView tasks={tasks} />
    );
};

image.png

整個模型並不複雜,並且redux 還推出了工具集redux toolkit,使用它提供的createSlice方法去簡化一些操做, 舉個例子:

// Action
export const UPDATE_LIST_NAME = 'UPDATE_LIST_NAME';

// Action creator
export const updateListName = (name) => ({
    type: UPDATE_LIST_NAME,
    payload: { name }
});

// Reducer
const 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-name
import { 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, 能夠減小一些沒必要要的代碼, 提高開發體驗。

儘管如此, Redux 還有有一些自然的缺陷

  1. 概念比較多,心智負擔大。
  2. 屬性要一個一個 pick,計算屬性要依賴 reselect。還有魔法字符串等一系列問題,用起來很麻煩容易出錯,開發效率低。
  3. 觸發更新的效率也比較差。對於connect到store的組件,必須一個一個遍歷,組件再去作比較,攔截沒必要要的更新, 這在注重性能或者在大型應用裏, 無疑是災難。

對於這個狀況, React 自己也提供瞭解決方案, 就是咱們熟知的 Context.

Image for post

<MyContext.Provider value={/* some value */}>

<MyContext.Consumer>
  {value => /* render something based on the context value */}
</MyContext.Consumer>

給父節點加 Provider 在子節點加 Consumer,不過每多加一個 item 就要多一層 Provider, 越加越多:

Recoil - Ideal React State Management Library? - DEV

並且,使用Context 問題也很多。

對於使用 useContext 的組件,最突出的就是問題就是 re-render.

不過也有對應的優化方案: React-tracked.

稍微舉個例子:

// store.js
import 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 }) => (
    <Provider reducer={reducer} initialState={initialState}>
        {children}
    </Provider>
);

export { useTracked, useTrackedState, useUpdate };

對應的,也有 hooks 版本:

const [state, dispatch] = useTracked();
const dispatch = useUpdate();
const state = useTrackedState();

// ...

Recoil

Recoil.js 提供了另一種思路, 它的模型是這樣的:

Image for post

在 React tree 上建立另外一個正交的 tree,把每片 item 的 state 抽出來。

每一個 component 都有對應單獨的一片 state,當數據更新的時候對應的組件也會更新。

Recoil 把 這每一片的數據稱爲 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(
  <React.StrictMode>
    <RecoilRoot>
      <App />
    </RecoilRoot>
  </React.StrictMode>,
  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

假設, 如今要實現一個counter:

Image for post

先用 useState 實現:

import React, { useState } from "react";

const App = () => {
  const [count, setCount] = useState(0);
  return (
    <div className="App">
      <button onClick={() => setCount(count + 1)}>Increase</button>
      <button onClick={() => setCount(count - 1)}>Decrease</button>
      <div>Count is {count}</div>
    </div>
  );
};

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 (
    <div className="app">
      <button onClick={() => setCount(count + 1)}>Increase</button>
      <button onClick={() => setCount(count - 1)}>Decrease</button>
      <div>Count is {count}</div>
    </div>
  );
};

export default App;

看到這, 你可能對atom 有一個初步的認識了。

那 atom 具體是個什麼概念呢?

Atom

簡單理解一下,atom 是包含了一份數據的集合,這個集合是可共享,可修改的。

組件能夠訂閱atom, 能夠是一個, 也能夠是多個,當 atom 發生改變時,觸發再次渲染。

const someState = atom({
    key: 'uniqueString',
    default: [],
});

每一個atom 有兩個參數:

  • key:用於內部識別atom的字符串。相對於整個應用程序中的其餘原子和選擇器,該字符串應該是惟一的
  • default:atom的初始值。

atom 是存儲狀態的最小單位, 一種合理的設計是, 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 爲參數的純函數, 當atom 改變時, 會觸發從新計算。

selector 有以下參數:

  • key:用於內部識別 atom 的字符串。相對於整個應用程序中的其餘原子和選擇器,該字符串應該是惟一的.
  • get:做爲對象傳遞的函數{ get },其中get是從其餘案atom或selector檢索值的函數。傳遞給此函數的全部atom或selector都將隱式添加到selector的依賴項列表中。
  • set?:返回新的可寫狀態的可選函數。它做爲一個對象{ get, set }和一個新值傳遞。get是從其餘atom或selector檢索值的函數。set是設置原子值的函數,其中第一個參數是原子名稱,第二個參數是新值。

看個具體的例子:

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 (
    <div className="App">
      <button onClick={() => setCount(count + 1)}>Increase</button>
      <button onClick={() => setCount(count - 1)}>Decrease</button>
      <input type="number" value={input} onChange={(e) => setInput(Number(e.target.value))} />
      <div>Count is {count}</div>
      <div>Double count is {doubleCount}</div>
    </div>
  );
};

export default App;

比較好理解, useRecoilStateuseRecoilValue 這些基礎概念能夠參考官方文檔

另外, 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(
  <React.StrictMode>
    <RecoilRoot>
      <React.Suspense fallback={<div>Loading...</div>}>
          <App />
      </React.Suspense>
    </RecoilRoot>
  </React.StrictMode>,
  document.getElementById("root")
);

Redux vs Recoil

模型對比:

image.png

Recoil 推薦 atom 足夠小, 這樣每個葉子組件能夠單獨去訂閱, 數據變化時, 能夠達到 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數據變化時,只有訂閱了這個 atom 的組件須要re-render。

不過, 在Redux 中,咱們也能夠用selector 實現一樣的效果:

// selector
const taskSelector = (id) => state.tasks[id];

// component code
const task = useSelector(taskSelector(id));

不過這裏的一個小問題是,state變化時,taskSelector 也會從新計算, 不過咱們能夠用createSelector 去優化, 好比:

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 鼓勵把每個狀態作的足夠小, 任意組合,最小範圍的更新。

而redux, 咱們的習慣是, 把容器組件鏈接到store上, 至於子組件,哪怕往下傳一層,也沒什麼所謂。

我想,Recoil 這麼設計,多是十分注重性能問題,優化超大應用的性能表現。

目前,recoil 還處於玩具階段, 還有大量的 issues 須要處理, 不過值得繼續關注。

最後

感興趣的朋友能夠看看, 作個todo-list體驗一下。

但願這篇文章能幫到你。

才疏學淺,文中如有錯誤, 歡迎指正。


若是你以爲這篇內容對你挺有啓發,能夠:

  1. 點個「在看」,讓更多的人也能看到這篇內容。
  2. 關注公衆號「前端e進階」,掌握前端面試重難點,公衆號後臺回覆「加羣」和小夥伴們暢聊技術。

圖片

參考資料

  1. http://react.html.cn/docs/context.html#reactcreatecontext
  2. https://recoiljs.org/docs/basic-tutorial/atoms
  3. https://www.emgoto.com/react-state-management/
  4. https://medium.com/better-programming/recoil-a-new-state-management-library-moving-beyond-redux-and-the-context-api-63794c11b3a5

本文同步分享在 博客「皮小蛋」(SegmentFault)。
若有侵權,請聯繫 support@oschina.cn 刪除。
本文參與「OSC源創計劃」,歡迎正在閱讀的你也加入,一塊兒分享。

相關文章
相關標籤/搜索