你不需要Redux

Redux 是一个知名度和使用率比较高的状态管理库,取『你不需要 Redux』这个标题应该理解为『在大部分场景下,你不需要使用 Redux 做状态管理』。所以本篇将基于 React,不用其他框架或库来实现一个简单的状态管理方案。

在没有写这篇文章之前,你问我何时应该使用 Redux,我可能会侃侃而谈,而现在我竟不知道从哪里开始回答这个问题。在之前我会把 Redux 用来做全局状态管理,当我被问道这个问题的时候,我会拿『应该在何时使用全局状态管理?』这个问题的答案来回答。而这个问题实际上应该是,在做全局状态管理的时候,什么时候应该使用 Redux?

我不能给出正确的答案,也许你可以去 Redux 的网站上去看看,我是一个比较懒的人,懒得学,看到 Redux 的文档就头疼。于是就自己去简单实现一个全局状态管理的功能(为简化写法,下文使用状态管理来指代全局状态管理),整个代码只有 30 行左右。

一个简易的状态管理

这个状态管理有一些小的缺陷:

  1. 只能在 React 中使用
  2. React 版本需要 ≥ 16.8
  3. 只有简单的状态管理功能

设计实现

Redux 状态管理到底做了什么呢?

  1. 保存状态
  2. 当状态改变时触发视图更新
  3. 状态的增删改

貌似就是这些事了吧,那就围绕这三个功能来实现。

状态保存

状态是某一时刻变量的集合,保存状态非常简单,只需要将值放在一个集合中就行,所以说,可以直接将需要管理的状态放在一个Map或者一个Object中。

状态更新视图

React 和 Vue 都是数据驱动的,通常都是通过数据去更新视图,所以一个全局状态管理是必须要能够更新视图的,在本篇文章中,直接借助 React 更新机制来更新视图
React 视图更新有 3 种方式:

  1. 组件内 State 改变
  2. 组件接收的 Prop 改变
  3. 组件消费的 Context 改变

单纯地靠 State 或者 Props 改变是不能达到改变整个 React 应用状态的的效果的,二者需要结合使用,一种常规做法是将状态存放在顶级组件的state中,通过props一层一层地向下传递,这样写显然是非常麻烦的,而且一旦组件层级发生改变,很有可能整个子组件树都要修改。
好在 Context 帮我们解决了这个问题,它提供了一种在组件之间共享此类值的方式,在 Context 下,你可以在任意组件内使用其存放的 value,不用显式地通过props传值。

状态数据更新

在任意组件中改变 Context 的 value,必须要保证这个 value 是响应式的,如果是普通的变量,在别的地方更新这个 value,Context 是不会更新的,所以将 state 作为 Context 中的 value,想要在其他组件改变 value,只需要将更新 state 的函数一起放在 Context 中即可,在其他组件中调用这个函数,state 随着 value 更新,这样就会触发 Context 更新了。

代码实现

根据以上分析,这个状态管理是基于 Context 和 State 的,所以先来创建一个组件,而且要让其他组件都被 Context 包裹起来,组件的结构大致这样:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
import React, { useState, createContext } from "react";

const Store = createContext();
Store.displayName = "Store"; // 便于在调试工具中查看

const Provider = (props) => {
// 创建一个state,将作为响应式的 context value
const [store, setStore] = useState(Object.create(null));

return (
<Store.Provider value={{ ...store, setStore }}>
// 使其子组件都被 Context 包裹
{props.children}
</Store.Provider>
);
};

核心代码就是这些了,只不过还需要一些优化。

Action&Reducer

就把需要全局使用的状态都放在 store 中,总觉得有什么东西混在一团,不便于管理,于是还需要将更新的逻辑抽象一下,变得更清晰,其实也是参照 Redux 哲学,写一个更像 Redux 的状态管理。
好在React hooks已经帮我们做了大部分工作,比如一个和 useState 相似的 useReducerhooks ,相信使用过 Redux的开发者用起来会非常趁手。
useReducer 接收三个参数,分别是一个 Reducer,一个初始化值,一个惰性初始化函数。
主要说一下 Reducer,它是一个函数,接收一个 State 和,一个 Action。Action 是一个纯对象,充当信使的作用,按照 Redux 中的说法,Action 是一个包含 type 字段的对象,在这里没有限制,但还是按照大家熟悉的 Redux 来写吧。
将 Action 定义成这样的结构:

1
2
3
4
interface Action {
type: string;
payload: any;
}

为了方便管理,将可使用的 Action 放在 actions.type.json 文件中,这里我写一个比较特殊的 Action:

1
{ "SET_DATA": "setData" }

这个 Action 对应的功能如 4~6 行所示:

1
2
3
4
5
6
7
8
9
import { SET_DATA } from "./actions.type.json";

const ACTION_REDUCER = {
[SET_DATA](state, newState) {
return { ...state, ...newState };
},
};

export default ACTION_REDUCER;

这个 Action 可以直接改变或者新增某个属性值,基本上能够在更新状态的绝大多数场景下使用。
将这些 Action 对应的函数放在一个对象中,相当于 Redux 的 combineReducers,这样写也可以避免使用 if或者 switch 语句来选择执行某个函数。
现在可以将 useState 替换成 useReducer 了:

1
2
3
4
5
6
7
8
9
10
11
12
13
// reducer
const reducer = (state, action) => {
const { type, payload } = action;
return ACTION_REDUCER[type](state, payload);
};

// useReducer
const [store, dispatch] = useReducer(reducer, Object.create(null));

render(
// Provider
<Store.Provider value={{ store, dispatch }}>{props.children}</Store.Provider>
);

如何使用

其用法和 Redux 大体相同,为了避免 Context 被重新渲染,应尽量将其作为顶层组件:

1
2
3
4
5
ReactDom.render(
<Store>
<App />
</Store>
);

读取状态

1
2
3
4
5
6
7
8
import React, { useContext } from "react";
import { store } from "./store";
// ---组件内部---
const {
store: { name },
} = useContext(store);

console.log(name);

更新状态

1
2
3
4
5
6
7
8
9
10
11
import React, {useContext} from "react"
import { store, ACTIONS} } from "./store"
// ---组件内部---
const {dispatch} = useContext(store)

dispatch({
type: ACTIONS.SET_DATA,
payload: {
data: "hello world"
}
})

最后

这个状态管理仿造 Redux 的用法简单实现出来了,用于简单的全局状态存储已经足够,不用去使用 redux 这个笨重的库,我猜绝大多数的项目都不需要去使用 Redux,如果想使用中间件,异步更新状态等特性,采用 Redux 或许是更好的选择。

本文实现的完整代码以及使用示例已经放在了此处 ,文中有错误的地方欢迎指正。

作者

KylinLee

发布于

2021-03-04

更新于

2022-02-11

许可协议

CC BY-NC-SA 4.0

评论

Your browser is out-of-date!

Update your browser to view this website correctly.&npsb;Update my browser now

×