聊聊 React 两个状态管理库 Redux & Recoil

State Management in React Apps | WalkingTree Technologies

背景

React 是一个十分优秀的UI库, 最初的时候, React 只专注于UI层, 对全局状态管理并没有很好的解决方案, 也因此催生出类似Flux, Redux 等优秀的状态管理工具。

随着时间的演变, 又催化了一批新的状态管理工具。

简单整理了一些目前主流的状态管理工具:

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

这几个都是我接触过的,Npm 上的现状和趋势对比:

image.png

image.png

毫无疑问,ReactRedux 的组合是目前的主流。

今天5月份, 一个名叫 Recoil.js 的新成员进入了我的视野,带来了一些有趣的模型和概念,今天我们就把它和 Redux 做一个简单的对比, 希望能对大家有所启发。

正文

先看 Redux:

Redux

React-Redux 架构图:

image.png

这个模型还是比较简单的, 大家也都很熟悉。

先用一个简单的例子,回顾一下整个模型:

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

可以把 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

官方描述:

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 在一个评论中提到:

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. 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

以上是 聊聊 React 两个状态管理库 Redux &amp; Recoil 的全部内容, 来源链接: utcz.com/a/39873.html

回到顶部