在离职以后,我开始静下心来,思考原来在繁重的业务开发节奏中无暇思考的一些问题,本期的主题是纯函数钩子useReducer
和共享状态钩子useContext
。前端
在react中,reducer
函数是一个很重要的概念。它表示一个接收旧状态,返回新状态的函数。vue
const nums = [1, 2, 3]
const value = nums.reduce((acc, cur) => acc + cur, 0)
在上述例子中,reduce函数的一个参数,就是一个标准的reducer
函数。react
在以前的setState
使用中,你可能会好奇setNum(prev => prev + 1)
中prev
怎么来的,让咱们深刻到最底层看看吧,实际上useState
并不是最底层的元素,它内部仍然用到了useReducer
来实现,在react源码中有个basicStateReducer
,大体结构以下:web
function basicStateReducer(state, action) {
return typeof action === 'function' ? action(state) : action;
}
因此,当咱们的setter接收的参数是一个函数时,旧的state将做为参数被该函数使用。vuex
useReducer
基本用法以下:数据库
const [state, dispatch] = useReducer(reducer, initialState, initFunc);
其中第三个参数是可选参数,咱们通常只会用到前两个。redux
一个简单的示例(实现数字+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
来隐藏修改状态的复杂细节。ide
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 的单项数据流,能够归纳为三个部分:View
,Reducers
,Store
。
View
视图层发起更新动做(dispatch
),会抵达更新函数层(Reducers
),更新函数执行并返回最新状态,抵达状态层(Store
),状态层更新后将通知视图层更新。
redux
其实我以为,不管是 vuex 也好,redux 也好,它的设计理念都是相似一个“前端数据库”。在store
层应该只存放公共状态,不建议存放其余的东西,好比公共方法,由于这与reducer
纯函数的理念是相悖的。
好了,让一切开始吧,咱们这里只定义三个组件:根组件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所引起的代码臃肿脆弱问题。
不能,我在项目中虽然已经这么用了,可是仍是发现对比redux的功能是有所欠缺的,其中最典型的就是更新公共状态后没有回调的问题。
useReducer
+ useContext
其实是制造了一个“穷人版的 redux”。并且咱们必须花费额外的心思去避免性能问题(React.memo
、useCallback
等),然而这些烦人的 dirty works 其实 React-Redux 等工具已经默默替咱们解决了。除此以外,咱们还会面临如下问题:
以上四个坑点摘抄于腾讯的这篇文章,仔细读完后发现确实写的能够:Redux with Hooks