大家好,我是虚竹。
众所周知,React
是一个专注于UI
层的库,不同于Vue、Angular
等框架,React 的各种状态管理方案一直是在百花齐放、群魔乱舞。除了热门库Redux、Mobx、Recoil、Zustand
等之外,React 的正式版也来到了v17,useState、useReducer、useContext
等状态管理相关hook
的概念和应用也逐渐深入人心。
作为普通程序员首要考虑的是,如何快速挑选一个成熟的比较舒服的轮子解决手头项目。由于我们团队用的技术栈是 React,就花了一丢丢时间研究这个 React 的状态管理库。当然,仅限在使用层面,也就是用着舒服的角度来选择到底使用哪个状态管理库。参考在 Github 上面看看 React 社区内状态管理库的流行程度和使用程度的层面,来进行选型。可以继续往下看完这篇文章,我想你应该会有答案的。如果还是比较犹豫,可以在评论区留言,大家一起交流讨论。
下面会通过一个简易购物车实例讲解各主流状态管理库的使用,含常用 API 介绍。文末附
github 源码下载地址
,如果对大家有一丢丢帮助,还请点个赞或star
支持一下。
github 地址:https://github.com/facebookexperimental/Recoil
官方网站:https://recoiljs.org/zh-hans/
Recoil
是 Facebook 开发的状态管理库,目标是做一个高性能的状态管理库,并且可以使用React内部的调度机制,包括会支持并发模式,虽然目前还处于实验阶段,但是 Facebook 内部已经有部分在使用,因此对于前端开发者来说,尤其是使用 React 的,小项目可以尝试一下。
使用 Recoil 会为你创建一个数据流向图,从atom
(共享状态)到selector
(纯函数),再流向 React 组件。Atom 是组件可以订阅的 state 单位。selector 可以同步或异步改变此 state。
安装 Recoil
创建 React 项目最为推荐的方式是使用脚手架 Create React App,命令如下:
npx create-react-app recoil-demo
安装 Recoil 最新版本,命令如下:
npm install recoil
&
yarn add recoil
RecoilRoot 初始化
和 Redux 一样,全局数据流管理需要存在作用域 RecoilRoot
,首先是引入RecoilRoot
并将其放在根组件的位置(也可以放在其他父组件位置上),比如在 App.js 文件中引入,代码如下:
import { Routes, Route } from "react-router-dom";
import { RecoilRoot } from "recoil";
import Catalog from "./pages/cart"; // 主页
import Cart from "./pages/cart/Cart"; // 购物车列表
function App() {
return (
<div className="App">
<RecoilRoot>
<Routes>
<Route exact path="/" element={<Catalog />} />
<Route path="/cart" element={<Cart />} />
</Routes>
</RecoilRoot>
</div>
);
}
export default App;
Atom 定义状态
atom(原子)是 recoil 中最小的状态单元,atom 表示一个值可以被读、写、订阅,它必须有一个区别于其他 atom 保持唯一性和不变性
的 key。
在 src 目录下新建 store 文件夹,再创建 atoms.js 文件,代码如下:
import { atom } from "recoil";
// 购物车状态
export const cart = atom({
key: "cart", // key 值必须唯一
default: [], // 定义默认值
})
在 store 文件夹创建一个 index.js 文件,用于统一导出定义好的共享状态和方法,代码如下:
import { cart } from "./atoms";
export {
cart
}
useRecoilValue 读取数据
当一个组件需要在不写入 state 的情况下读取 state 时,推荐使用该 hook。
在购物车列表页面 src/pages/cart/Cart.js 文件中,采用Hooks
方式读取数据useRecoilValue
,代码如下:
import React from "react";
import { Table } from "antd";
import { useRecoilValue } from "recoil";
import { cart } from "../../store";
function Cart() {
const tableData = useRecoilValue(cart);
return (
<>
<Table
columns={columns}
dataSource={tableData}
size="middle"
/>
</>
);
}
export default Cart;
然后查看页面效果,因为初始值是空数组,所以暂无数据,如下图所示:
useRecoilState 读写数据
当组件同时需要读写状态时,推荐使用该 hook。
本 API 和 React 的 useState()
hook 类似,区别在于useRecoilState
的参数使用 Recoil state 代替了useState()
的默认值。它返回由 state 的当前值和 setter 函数组成的元组。Setter 函数的参数可以是新值,也可以是一个以之前的值为参数的更新器函数。
import { useRecoilState } from "recoil";
import { cart } from "./atoms";
......
// 单品加量
export const useIncrementItem = () => {
const [items, setItems] = useRecoilState(cart);
return (product) => {
const { clone, index } = cloneIndex(items, product.id);
if (index !== -1) {
clone[index].amount += 1;
setItems(clone);
} else {
setItems([...clone, { ...product, amount: 1 }]);
}
};
};
Selector 派生值
selector
代表一个派生状态,派生状态是状态的转换。你可以将派生状态视为将状态传递给以某种方式修改给定状态的纯函数的输出。
先手动添加一条假数据,代码如下:
import { atom } from "recoil";
export const cart = atom({
key: "cart", // key 值必须唯一
default: [
{
key: 1,
id: 1,
bookName: "ES6标准入门",
amount: 1,
unitPrice: 88
// 单品总价 = 单价 * 数量
}
], // 定义默认值
})
合计总金额数据是通过状态管理逻辑计算得出,包括加入购物车数量统计,在 store 文件夹新建一个 selectors.js 文件,代码实现如下:
import { selector } from "recoil";
import { cart } from "./atoms";
export const cartState = selector({
key: "cartState",
get: ({get}) => {
const totalCost = get(cart).reduce((a, b) => a + b.amount * b.unitPrice, 0); // 合计总金额
const totalCartNum = get(cart).reduce((a, b) => a + b.amount, 0); // 加入购物车总数
return {
totalCost,
totalCartNum
}
}
})
使用selector
派生状态则需要useRecoilValue
获取数据,代码如下:
import React from "react";
import { Table } from "antd";
import { useRecoilValue } from "recoil";
import { cartState } from "../../store";
......
function Cart() {
const { totalCost } = useRecoilValue(cartState);
const footerDom = () => {
return (
<>
合计:<span className="total">¥{totalCost}</span>
</>
);
};
......
}
export default Cart;
自定义 Hooks
购物车里面的单品数量增删减功能,采用自定义 Hooks
的方式实现,用到了useRecoilState
读写数据的 API。
在 store 文件夹新建一个 hooks.js 文件,代码实现如下:
import { useRecoilState } from "recoil";
import { cart } from "./atoms";
// 拷贝原数据,获取满足条件的索引值
const cloneIndex = (items, id) => ({
clone: items.map((item) => ({
...item,
})),
index: items.findIndex((item) => item.id === id),
});
// 加量
export const useIncrementItem = () => {
const [items, setItems] = useRecoilState(cart);
return (product) => {
const { clone, index } = cloneIndex(items, product.id);
if (index !== -1) {
clone[index].amount += 1;
setItems(clone);
} else {
setItems([...clone, { ...product, amount: 1 }]);
}
};
};
// 减量
export const useDecrementItem = () => {
const [items, setItems] = useRecoilState(cart);
return (product) => {
const { clone, index } = cloneIndex(items, product.id);
if (clone[index].amount !== 1) {
clone[index].amount -= 1;
setItems(clone);
}
}
}
// 删除
export const useRemoveItem = () => {
const [items, setItems] = useRecoilState(cart);
return (product) => {
setItems(items.filter(item => item.id !== product.id));
}
}
自定义好 hooks 后,在 store/index.js 文件中导入,代码如下:
import { cart } from "./atoms";
import { cartState } from "./selectors";
import { useIncrementItem, useDecrementItem, useRemoveItem } from "./hooks";
export {
cart,
cartState,
useIncrementItem,
useDecrementItem,
useRemoveItem
}
打开封装好的按钮组件,直接引入 store 文件,代码如下:
import React from "react";
import { Button } from "antd";
import PropTypes from "prop-types";
import { useIncrementItem, useDecrementItem, useRemoveItem } from "../../store";
function CartButtons(props) {
const { item } = props;
const increment = useIncrementItem();
const decrement = useDecrementItem();
const remove = useRemoveItem();
return (
<>
<Button
onClick={() => decrement(item)}
style={{ background: "#ddd", borderColor: "#ddd" }}
>
-
</Button>
<Button onClick={() => increment(item)} type="primary">
+
</Button>
<Button onClick={() => remove(item)} type="primary" danger>
x
</Button>
</>
);
}
CartButtons.propTypes = {
item: PropTypes.object, // PropTypes.object.isRequired
};
export default CartButtons;
掌握以上这些常用 API 就可以在项目中启用
Recoil
了,相比Redux
,这个库对状态的管理和组织更为灵活。Recoil
推崇状态和派生数据更细粒度控制,写法上 Demo 看起来简单,实际上代码规模大之后依然很繁琐。
如需详细了解其他 API,请移步去官方文档查看。
github 地址:https://github.com/reduxjs/redux-toolkit
官方网站:https://redux-toolkit.js.org/
Redux Toolkit 是 Redux 官方推出的基于 Redux 进行升级的工具包,它简化了 Redux 的使用流程,降低 Redux 的使用难度。Redux Toolkit 提供了强大的状态缓存与状态编辑方法,进一步强化了 Redux 中对状态进行处理的能力。Redux 官方的愿景是希望Redux Toolkit
能够成为事实上的编写 Redux 逻辑的标准方法。
安装 Redux Toolkit
# Redux + Plain JS template
npx create-react-app redux-toolkit-demo --template redux
# Redux + TypeScript template
npx create-react-app redux-toolkit-demo --template redux-typescript
启动项目
安装成功后,进入项目目录 redux-toolkit-demo,执行npm start
命令启动项目,如下图所示:
目录结构,如下图所示:
具体源代码示例自行下载安装查看。。。
安装 Redux DevTools 工具
打开 chrome 网上应用商店,搜索 Redux DevTools 开发工具,直接安装此插件,如下图所示:
刷新界面按 F12
即可看到效果,如下图所示:
npx create-react-app redux-toolkit-demo
npm install @reduxjs/toolkit react-redux
configureStore
configureStore
是对标准Redux
的createStore
函数的抽象封装,添加了默认值,方便用户获得更好的开发体验。 传统的Redux
,需要配置reducer、middleware、devTools、enhancers
等,使用configureStore
直接封装了这些默认值。
首先在 src 目录创建一个 store 文件夹,再建一个 index.js 文件,引入configureStore
创建一个空的 redux 数据,代码如下:
import { configureStore } from "@reduxjs/toolkit";
// 这个 store 已经集成了 redux-thunk 和 Redux DevTools
export const store = configureStore({
reducer: {},
});
在根目录 index.js 文件中,引入react-redux
的Provider
,并将导出的store
当作prop
传递给它。代码如下:
import React from "react";
import ReactDOM from "react-dom";
import { Provider } from "react-redux";
import { store } from "./store";
import App from "./App";
import "./index.less";
ReactDOM.render(
<Provider store={store}>
<App />
</Provider>,
document.getElementById("root")
);
createSlice
接受一个初始状态和一个带有 reducer 名称和函数的查找表,并自动生成 action creator 函数、action 类型字符串和一个 reducer 函数。
在src/store
目录创建一个cartSlice.js
文件,使用createSlice
方法创建一个redux slice reducer
。每一个slice
里面包含了reducer
和actions
,可以实现模块化的封装。所有的相关操作都独立在一个文件中完成。代码如下:
import { createSlice } from "@reduxjs/toolkit";
export const cartSlice = createSlice({
name: "CART_ACTION", // 命名空间,在调用 action 的时候会默认的设置为 action 的前缀
initialState: {
// state 数据的初始值
totalCost: 0, // 合计总金额
totalCartNum: 0, // 统计购物车数量
cartList: [], // 购物车列表数据
},
reducers: {
// 这里的属性会自动的导出为 actions,在组件中可以直接通过 dispatch 进行触发
totalCartData: (state, { payload }) => {
state.cartList = payload;
state.totalCartNum = payload.reduce((a, b) => a + b.amount, 0); // 合计总金额
state.totalCost = payload.reduce((a, b) => a + b.amount * b.unitPrice, 0); // 统计购物车数量
},
},
});
// 导出 actions
export const { totalCartData } = cartSlice.actions;
// 导出 reducer,在创建 store 时会用到
export default cartSlice.reducer;
我们需要从上面创建的空的store
导入reducer
函数并将其添加到我们的存储中,通过在reducer
参数中定义一个字段,告诉store
使用这个slice reducer
函数来处理该状态的所有更新。代码如下:
import { configureStore } from "@reduxjs/toolkit";
import cartReducer from "./cartSlice";
export const store = configureStore({
reducer: {
cart: cartReducer,
},
});
现在我们可以使用React-Redux hook
让React
组件与Redux
存储交互。我们可以使用useSelector
从存储中读取数据,并使用useDispatch
方法分派操作。Header
头部组件代码如下:
import React from "react";
import { NavLink } from "react-router-dom";
import { Button, Badge } from "antd";
import { ShoppingCartOutlined } from "@ant-design/icons";
import { useSelector } from "react-redux";
function Header() {
const { totalCartNum } = useSelector((state) => state.cart);
return (
<div className="header">
<NavLink to="/">
<Button className="title" type="link">
我的书店
</Button>
</NavLink>
<NavLink to="/cart">
<Badge count={totalCartNum}>
<Button
type="primary"
style={{ background: "#1890ff", borderColor: "#1890ff" }}
shape="round"
icon={<ShoppingCartOutlined />}
/>
</Badge>
</NavLink>
</div>
);
}
export default Header;
自定义 Hooks
购物车里面的单品数量增删减功能,采用自定义 Hooks
的方式实现。在hooks.js
文件中引用useSelector
和useDispatch
读写数据,来源react-redux hook API
。代码实现请直接查看文末 github 源码下载地址。
以下三个 API 在此 DEMO 暂未用上,大家可以自行查看官方示例。
createAction
接受一个 Action 类型字符串,并返回一个使用该类型的 Action 创建函数。
createReducer
接受初始状态值和 Action 类型的查找表到 reducer 函数,并创建一个处理所有 Action 类型的 reducer。
createAsyncThunk
createAsyncThunk 方法可以创建一个异步的 action,这个方法被执行的时候会有三个状态,如:pending(进行中) fulfilled(成功)、rejected(失败)。可以监听状态的改变执行不同的操作。官方示例中使用到了extraReducers
创建额外的action
对数据获取的状态信息进行监听。
redux-toolkit
是目前来说比较好的一个redux
使用的解决方案,通过一些内置的插件和代码封装让redux
的使用更加的方便顺手,而且条理更清晰。
github 地址:https://github.com/mobxjs/mobx
官方网站:https://zh.mobx.js.org/README.html
MobX 是一个经过战火洗礼的库,它通过透明的函数响应式编程使得状态管理变得简单和可扩展。MobX 背后的哲学很简单: 任何源自应用状态的东西都应该自动地获得。 其中包括 UI、数据序列化、服务器通讯,等等。
核心概念是:MobX 通过响应式编程实现简单高效,可扩展的状态管理库。
安装 Mobx
# 初始化项目
npx create-react-app mobx-demo
# 安装 mobx 核心工具包
# mobx-react-lite 是 mobx 和 react 配合时的包,注意它只能用于函数式组件
npm install mobx mobx-react-lite
makeAutoObservable
无需通过observable和action等修饰器,直接在构造函数中使用makeAutoObservable
来实现observable
和action
、computed
修饰器功能,使代码更加简洁。
// target: 将目标对象中的属性和方法设置为 observable state 和 action
// overrides: 覆盖默认设置, 将 target 对象中的某些属性或者方法设置为普通属性
// options: 配置对象, autoBind, 使 action 方法始终拥有正确的 this 指向
makeAutoObservable(target, overrides?, options?)
在 src 目录新建store
文件夹,里面建个modules
文件夹用来管理所有store
子模块,再建个类组件cartStore.js
,代码实现如下:
import { makeAutoObservable } from "mobx";
class Cart {
// 1.定义数据
totalCost = 0; // 合计总金额
totalCartNum = 0; // 统计购物车数量
cartList = []; // 购物车列表数据
// 2.响应式处理
constructor() {
makeAutoObservable(this);
}
// 3.定义 action 函数
totalCartData = (items) => {
this.cartList = items;
this.totalCartNum = this.cartList.reduce((a, b) => a + b.amount, 0); // 统计购物车数量
this.totalCost = this.cartList.reduce(
(a, b) => a + b.amount * b.unitPrice,
0
); // 合计总金额
};
}
// 4.实例化并导出 store
const cartStore = new Cart();
export default cartStore;
创建好cartStore
类组件后,在store
文件夹,建个index.js
作为导出所有子模块入口文件,代码实现如下:
import cartStore from "./modules/cartStore";
// 通过组件树提供了一个传递数据的方法,从而避免在每一个层级手动的传递 props 属性。
export const stores = React.createContext({
cart: cartStore,
});
// 创建一个 useStores 的 Hook,简化调用
export const useStores = () => React.useContext(stores);
自定义 Hooks
在购物车里面的单品数量增删减功能,采用自定义 Hooks
的方式实现。在hooks.js
文件中引用useStores
来读取数据。代码实现请直接查看文末 github 源码下载地址。
接下来在Header
组件中,使用useStores
,代码如下:
// observer: 监控当前组件使用到的由 MobX 跟踪的可观察的状态, 当状态发生变化时通知 React 更新视图
import { observer } from "mobx-react-lite";
import { useStores } from "@/store";
function Header() {
const store = useStores();
return (
<div className="header">
......
<NavLink to="/cart">
<Badge count={store.cart.totalCartNum}>
<Button
type="primary"
style={{ background: "#1890ff", borderColor: "#1890ff" }}
shape="round"
icon={<ShoppingCartOutlined />}
/>
</Badge>
</NavLink>
</div>
);
}
export default observer(Header);
组件用observer
包裹,useStores
引用store
,完美。
此文 DEMO 用的是最新版本
Mobx V6
,目前只用到一个 API,开发体验友好,学习和理解成本中等。如有小伙伴感兴趣,可以在中小项目去尝试使用。
经供参考:我选的是由Redux
官方提供的react-redux + redux-toolkit
组合来进行统一的状态管理。
状态管理库 | 学习成本 | 编码成本 | 项目类型 | 设计模式 | React Hooks 支持 | TS 友好 | SSR | 代码拆分 | 并发模式兼容 | 可调试性 | 生态繁荣 |
---|---|---|---|---|---|---|---|---|---|---|---|
Redux | 高 | 高 | 全能型 | 集中式 | react-redux + redux-toolkit | 一般 | 支持 | 不支持 | 支持 | 好 | 高 |
Mobx | 中 | 中 | 中小型 | 分散式 | mobx v6 + mobx-react-lite | 好 | 支持 | 支持 | 未知 | 差 | 中 |
Recoil | 低 | 低 | 中小型 | 分散式 | 天然支持 | 好 | 实践较少 | 支持 | 支持 | 好 | 低 |
本人文笔有限,时间仓促,技术才疏学浅,利用空闲时间实操+梳理,分享一下我最近学习所获的成果。本人非常看好recoil
状态库,希望未来能成为霸主地位。加油吧,FB兄弟!!!
文中如有错误,欢迎在评论区留言指正。
创作不易,如果这篇文章有一丢丢帮到你了,还望点个赞以表鼓励。
最后附上 github 源码链接:https://github.com/jackchen0120/react-share-state
关注我的公众号【懒人码农】,获取更多项目实战经验及各种源码资源。如果你也一样对技术热爱并且为之着迷,欢迎加我微信【lazycode520】,将会邀你加入我们的前端实战交流群一起学习、一起进步~~~