[React Hooks长文总结系列三]为所欲为,制作“穷人版”的redux
前言
在离职之后,我开始静下心来,思考原来在繁重的业务开发节奏中无暇思考的一些问题,本期的主题是纯函数钩子useReducer
和共享状态钩子useContext
。
什么是reducer函数?
在react中,reducer
函数是一个很重要的概念。它表示一个接收旧状态,返回新状态的函数。
const nums = [1, 2, 3]
const value = nums.reduce((acc, cur) => acc + cur, 0)
在上述例子中,reduce函数的一个参数,就是一个标准的reducer
函数。
在之前的setState
使用中,你可能会好奇setNum(prev => prev + 1)
中prev
怎么来的,让我们深入到最底层看看吧,实际上useState
并非最底层的元素,它内部仍然用到了useReducer
来实现,在react源码中有个basicStateReducer
,大致结构如下:
function basicStateReducer(state, action) {
return typeof action === 'function' ? action(state) : action;
}
所以,当我们的setter接收的参数是一个函数时,旧的state将作为参数被该函数使用。
useReducer
useReducer
基本用法如下:
const [state, dispatch] = useReducer(reducer, initialState, initFunc);
其中第三个参数是可选参数,我们一般只会用到前两个。
一个简单的示例(实现数字+1)如下:
import React, { useReducer } from "react";
const initialState = {
num: 0,
};
function reducer(state, action) {
switch (action.type) {
case "increase":
return { num: state.num + 1 };
case "decrease":
return { num: state.num - 1 };
default:
throw new Error();
}
}
const NumsDemo = () => {
const [state, dispatch] = useReducer(reducer, initialState);
return (
<>
<h1>数字为: {state.num}</h1>
<button onClick={() => dispatch({ type: "increase" })}>+</button>
<button onClick={() => dispatch({ type: "decrease" })}>-</button>
</>
);
};
export default NumsDemo;
你可能也发现了,useReducer
和 useState
非常类似:定义状态和修改状态的逻辑。useReducer
用起来更加麻烦,但是如果需要维护的状态本身比较复杂,多个状态之间相互依赖,那么 useReducer
的好处才真正显示出来了:用一个语义化的action
来隐藏修改状态的复杂细节。
useContext
useContext
则比较简单,它用于在局部组件树中共享数据,有点类似于vue中的provide/inject
,基本使用如下:
// 在模块的入口文件中定义
// Home.tsx
export const MyContext = React.createContext({num: 24});
// 在模块内部的某个组件中获取
// Sub.tsx
import MyContext from '../Home';
const Sub = () => {
const state = useContext(MyContext);
// ...
}
redux的基本理念以及解决了什么问题
好了,现在我们已经确定要弄一个小型redux了,不过在这之前我们还是需要回顾一下redux的基本理念。
在对于小型页面共享数据时,我们一般会有诸如“状态提升”这样的开发约定,也就是说,我们会将共享的状态放到上层最近的公共父级。但是当页面数量一多,组件拆分粒度变细,这种“共享状态”的机制变得很脆弱,很冗余。
redux 的机制就是为了解决这个问题,redux 有几个非常明显的特点:
- 数据的唯一真相来源;
- reducer 纯函数;
- 单项数据流。
redux 的单项数据流,可以概括为三个部分:View
,Reducers
,Store
。
View
视图层发起更新动作(dispatch
),会抵达更新函数层(Reducers
),更新函数执行并返回最新状态,抵达状态层(Store
),状态层更新后将通知视图层更新。
redux
其实我觉得,无论是 vuex 也好,redux 也好,它的设计理念都是类似一个“前端数据库”。在store
层应该只存放公共状态,不建议存放其他的东西,比如公共方法,因为这与reducer
纯函数的理念是相悖的。
实现一个简单的小型redux
好了,让一切开始吧,我们这里只定义三个组件:根组件App
,第一个子组件Sub1
,第二个子组件Sub2
。实现一个非常简单的数字加减功能,如下:
// 说明:为了代码更加易读,已经将ts的类型定义做了删除操作
// App.tsx
import React, { useReducer } from "react";
import Sub1 from "./Sub1";
import Sub2 from "./Sub2";
const INITIAL_STATE = {
name: "zhang",
age: 24,
};
// 定义改变状态的几种操作
function reducer(state, action) {
switch (action.type) {
case "increaseAge":
return { ...state, age: state.age + 1 };
case "decreaseAge":
return { ...state, age: state.age - 1 };
case "changeName":
return { ...state, name: action.payload };
default:
return state;
}
}
// 选择性导出该context
export const AppContext = React.createContext(null);
function App() {
const [state, dispatch] = useReducer(reducer, INITIAL_STATE);
return (
<AppContext.Provider value={{ state, dispatch }}>
<Sub1 />
<Sub2 />
</AppContext.Provider>
);
}
export default App;
// 在Sub1.tsx中
import React, { useContext } from "react";
import { AppContext } from "./App";
const Sub1 = () => {
const { state, dispatch } = useContext(AppContext);
return (
<>
<h1>Sub1年龄为: {state.age}, 名字为:{state.name}</h1>
<button onClick={() => dispatch({ type: "increaseAge" })}>+</button>
</>
);
};
export default Sub1;
// 在Sub2.tsx中
import React, { useContext } from "react";
import { AppContext } from "./App";
const Sub2 = () => {
const { state, dispatch } = useContext(AppContext);
return (
<>
<h1>Sub2年龄为: {state.age}, 名字为:{state.name}</h1>
<button onClick={() => dispatch({ type: "decreaseAge" })}>-</button>
</>
);
};
export default Sub2;
运行以上示例,可以发现在一处子组件更改公共状态,其他消费到该公共状态的组件(Consumer
)都会更新。这有效避免了隔代传props所引发的代码臃肿脆弱问题。
useReducer + useContext 能否代替 redux?
不能,我在项目中虽然已经这么用了,但是还是发现对比redux的功能是有所欠缺的,其中最典型的就是更新公共状态后没有回调的问题。
useReducer
+ useContext
实际上是制造了一个“穷人版的 redux”。而且我们必须花费额外的心思去避免性能问题(React.memo
、useCallback
等),然而这些烦人的 dirty works 其实 React-Redux 等工具已经默默替我们解决了。除此之外,我们还会面临以下问题:
- 需要自行实现 combineReducers 等辅助功能
- 失去 Redux 生态的中间件支持
- 失去 Redux DevTools 等调试工具
- 出了坑不利于求助……
以上四个坑点摘抄于腾讯的这篇文章,仔细读完后发现确实写的可以:Redux with Hooks