背景
React 是一个十分优秀的UI库, 最初的时候, React 只专注于UI层, 对全局状态管理并没有很好的解决方案, 也因此催生出类似Flux, Redux 等优秀的状态管理工具。
随着时间的演变, 又催化了一批新的状态管理工具。
简单整理了一些目前主流的状态管理工具:
- Redux
- React Context & useReducer
- Mobx
- Recoil
- react-sweet-state
- hox
这几个都是我接触过的,Npm 上的现状和趋势对比:
毫无疑问,React
和 Redux
的组合是目前的主流。
今天5月份, 一个名叫 Recoil.js
的新成员进入了我的视野,带来了一些有趣的模型和概念,今天我们就把它和 Redux 做一个简单的对比, 希望能对大家有所启发。
正文
先看 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
// components
import 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
方法去简化一些操作, 举个例子:
// 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 还有有一些天然的缺陷
:
- 概念比较多,心智负担大。
- 属性要一个一个 pick,计算属性要依赖 reselect。还有魔法字符串等一系列问题,用起来很麻烦容易出错,开发效率低。
- 触发更新的效率也比较差。对于connect到store的组件,必须一个一个遍历,组件再去做比较,拦截不必要的更新, 这在注重性能或者在大型应用里, 无疑是灾难。
对于这个情况, React 本身也提供了解决方案, 就是我们熟知的 Context.
{value => /* render something based on the context value */}
给父节点加 Provider 在子节点加 Consumer,不过每多加一个 item 就要多一层 Provider, 越加越多:
而且,使用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 }) => (
{children}
);
export { useTracked, useTrackedState, useUpdate };
对应的,也有 hooks
版本:
const [state, dispatch] = useTracked();
const dispatch = useUpdate();
const state = useTrackedState();
// ...
Recoil
Recoil.js 提供了另外一种思路, 它的模型是这样的:
在 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(
,
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:
先用 useState 实现:
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 是包含了一份数据的集合,这个集合是可共享,可修改的。
组件可以订阅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 (
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...