给前端快速上手摸鱼一把主流状态管理库,这家伙真香!!!

9,130 阅读14分钟

前言

大家好,我是虚竹。

众所周知,React是一个专注于UI层的库,不同于Vue、Angular等框架,React 的各种状态管理方案一直是在百花齐放、群魔乱舞。除了热门库Redux、Mobx、Recoil、Zustand等之外,React 的正式版也来到了v17,useState、useReducer、useContext等状态管理相关hook的概念和应用也逐渐深入人心。

image.png

作为普通程序员首要考虑的是,如何快速挑选一个成熟的比较舒服的轮子解决手头项目。由于我们团队用的技术栈是 React,就花了一丢丢时间研究这个 React 的状态管理库。当然,仅限在使用层面,也就是用着舒服的角度来选择到底使用哪个状态管理库。参考在 Github 上面看看 React 社区内状态管理库的流行程度和使用程度的层面,来进行选型。可以继续往下看完这篇文章,我想你应该会有答案的。如果还是比较犹豫,可以在评论区留言,大家一起交流讨论。

下面会通过一个简易购物车实例讲解各主流状态管理库的使用,含常用 API 介绍。文末附github 源码下载地址,如果对大家有一丢丢帮助,还请点个赞或star支持一下。

演示效果图

recoil.gif

功能清单

  • 加入购物车
  • 购物车商品数量
  • 单品选择数量
  • 删除单品
  • 统计已选商品总价

流行的状态管理库

Recoil

github 地址:github.com/facebookexp…

官方网站:recoiljs.org/zh-hans/

Recoil 是什么

Recoil是 Facebook 开发的状态管理库,目标是做一个高性能的状态管理库,并且可以使用React内部的调度机制,包括会支持并发模式,虽然目前还处于实验阶段,但是 Facebook 内部已经有部分在使用,因此对于前端开发者来说,尤其是使用 React 的,小项目可以尝试一下。

使用 Recoil 会为你创建一个数据流向图,从atom(共享状态)到selector(纯函数),再流向 React 组件。Atom 是组件可以订阅的 state 单位。selector 可以同步或异步改变此 state。

  • 分散管理原子状态的模式
  • 读写分离
  • 按需渲染
  • 派生缓存

常用 API

  • RecoilRoot 状态作用域
  • atom 分散式定义数据
  • selector 派生状态,异步获取数据
  • selectorFamily 依赖外部变量读取数据
  • useRecoilState 读写数据
  • useRecoilValue 读取数据
  • useSetRecoilValue 更新可写状态数据

目录结构

image.png

代码实现

安装 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;

然后查看页面效果,因为初始值是空数组,所以暂无数据,如下图所示:

image.png

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,请移步去官方文档查看。

Redux Toolkit(简称 RTK)

github 地址:github.com/reduxjs/red…

官方网站:redux-toolkit.js.org/

Redux Toolkit 是什么

Redux Toolkit 是 Redux 官方推出的基于 Redux 进行升级的工具包,它简化了 Redux 的使用流程,降低 Redux 的使用难度。Redux Toolkit 提供了强大的状态缓存与状态编辑方法,进一步强化了 Redux 中对状态进行处理的能力。Redux 官方的愿景是希望Redux Toolkit能够成为事实上的编写 Redux 逻辑的标准方法。

  • 简化 Redux 工作流程中过多的模板代码
  • 简化创建、配置 Store 的各种和应用逻辑无关的代码
  • 集成了常用的 Redux 中间件,不需要开发者单独下载和配置

常用 API

  • configureStore 创建 Redux store 实例
  • createSlice 创建切片处理
  • createAction 创建 actions 函数
  • createReducer 创建 reducer 函数
  • createAsyncThunk 处理异步动作

目录结构

image.png

代码实现

安装 Redux Toolkit

  1. 新项目
# 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命令启动项目,如下图所示:

image.png

目录结构,如下图所示:

image.png

具体源代码示例自行下载安装查看。。。

安装 Redux DevTools 工具

打开 chrome 网上应用商店,搜索 Redux DevTools 开发工具,直接安装此插件,如下图所示:

image.png

刷新界面按 F12 即可看到效果,如下图所示:

image.png

  1. 现有项目
npx create-react-app redux-toolkit-demo

npm install @reduxjs/toolkit react-redux

configureStore

configureStore是对标准ReduxcreateStore函数的抽象封装,添加了默认值,方便用户获得更好的开发体验。 传统的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-reduxProvider,并将导出的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里面包含了reduceractions,可以实现模块化的封装。所有的相关操作都独立在一个文件中完成。代码如下:

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 hookReact组件与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文件中引用useSelectoruseDispatch读写数据,来源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的使用更加的方便顺手,而且条理更清晰。

Mobx

github 地址:github.com/mobxjs/mobx

官方网站:zh.mobx.js.org/README.html

Mobx 是什么

MobX 是一个经过战火洗礼的库,它通过透明的函数响应式编程使得状态管理变得简单和可扩展。MobX 背后的哲学很简单: 任何源自应用状态的东西都应该自动地获得。 其中包括 UI、数据序列化、服务器通讯,等等。

核心概念是:MobX 通过响应式编程实现简单高效,可扩展的状态管理库

  • 简单编写无模板的极简代码来精准描述出你的意图
  • 轻松实现最优渲染,依赖自动追踪最小渲染优化
  • 架构自由,可移植,可测试

常用 API

  • makeAutoObservable 自动转换

目录结构

image.png

代码实现

安装 Mobx

# 初始化项目
npx create-react-app mobx-demo

# 安装 mobx 核心工具包
# mobx-react-lite 是 mobx 和 react 配合时的包,注意它只能用于函数式组件
npm install mobx mobx-react-lite

makeAutoObservable

无需通过observable和action等修饰器,直接在构造函数中使用makeAutoObservable来实现observableactioncomputed修饰器功能,使代码更加简洁。

// 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,开发体验友好,学习和理解成本中等。如有小伙伴感兴趣,可以在中小项目去尝试使用。

理由

  • 支持 TS ,使用函数式编程
  • 支持模块化,状态统一管理
  • 学习成本低,且状态管理库比较流行
  • 建议看看下面的对比,再考虑选择自己熟悉的库,还是尝试新库

经供参考:我选的是由Redux官方提供的react-redux + redux-toolkit组合来进行统一的状态管理。

主流库综合对比

状态管理库学习成本编码成本项目类型设计模式React Hooks 支持TS 友好SSR代码拆分并发模式兼容可调试性生态繁荣
Redux全能型集中式react-redux + redux-toolkit一般支持不支持支持
Mobx中小型分散式mobx v6 + mobx-react-lite支持支持未知
Recoil中小型分散式天然支持实践较少支持支持

结语

本人文笔有限,时间仓促,技术才疏学浅,利用空闲时间实操+梳理,分享一下我最近学习所获的成果。本人非常看好recoil状态库,希望未来能成为霸主地位。加油吧,FB兄弟!!!

文中如有错误,欢迎在评论区留言指正。

创作不易,如果这篇文章有一丢丢帮到你了,还望点个赞以表鼓励。

最后附上 github 源码链接:github.com/jackchen012…

关注我的公众号【懒人码农】,获取更多项目实战经验及各种源码资源。如果你也一样对技术热爱并且为之着迷,欢迎加我微信【lazycode520】,将会邀你加入我们的前端实战交流群一起学习、一起进步~~~