使用 React Hooks 和 context 进行全局状态管理
从 prop 钻探到使用 React 的集中式全局状态管理
这一切都始于 React、Vue、Angular 等一些令人惊叹的框架,它们都提出了将应用程序数据从文档对象模型 (DOM) 中抽象出来的绝妙想法。具体来说,React 的协调算法以及即将推出的Fiber 架构,极大地提升了这些层(抽象层和 DOM)的更新速度。这样一来,我们就可以专注于组件,而不是“真正的”HTML 实现,然而,这也带来了一些新的挑战,具体来说:
这就是典型的 prop 钻取式 React 反模式,即遍历 React 组件树以便在它们之间传递属性。高阶组件或装饰器,如果你更倾向于面向对象风格,会给我们带来更大的灵活性和其他一些架构可能性。现在,我们可以提取出想要共享的功能,并装饰需要使用它的组件。
在处理组件较少、相互交互的小型应用时,这种方法还好,但是,当我们在庞大的组件生态系统中进行复杂的通信时,这种方法就会变得复杂且容易出错。因此,单向数据流应运而生:
到目前为止,没有什么新东西,但如果我们把这个概念运用到 React 上下文和 Hooks 中会怎么样?这就是你来这里的原因!
主要概念
目前的主要亮点是我们伟大的新朋友react hooks,以及您随之而来的功能性方法:
Hooks 是 React 16.8 中的新增功能。它让你无需编写类即可使用状态和其他 React 功能。
然后,中心思想是将上下文 API与useContext和useReducer钩子一起使用,以使我们的存储可供我们的组件使用。
import React, { createContext, useContext, useReducer } from 'react';
export const StateContext = createContext();
export const StoreProvider = ({ reducer, initialState, children }) => (
<StateContext.Provider
value={useReducer(reducer, initialState)}
children={children}
/>
);
export const useStore = () => useContext(StateContext);
我们从此文件源代码中导出一个StoreProvider(负责使上下文/存储在应用程序中可用),它接收:
- 带有签名的 reducer 函数(state, action) => newState;
- 应用程序初始状态;
- 以及应用程序内容(子项);
useStore钩子负责从存储/上下文中获取数据。
尽管从现在开始命名法有所不同,但我将把我们的上下文称为存储,因为概念是相同的,并且我们可以轻松地将其与我们众所周知的 redux 架构标准联系起来。
美丽就在于这种简单:
- StateContext.Provider接收一个值对象(你当前的状态);
- useReducer接收一个函数:(state, action) => newState 和一个 initialState,然后从我们的应用程序发出的任何调度都将传递到这里并更新我们应用程序的当前状态;
- useContext获取我们的商店并使其在我们的应用程序中可用!
其余的只是代码组织和微小的变化,没有什么可担心的:)
深入细节
作为概念证明,我已经完成了这个基本的待办事项列表应用程序,请在此处查看源代码和此处的实时实现,它是一个包含几个组件和当前状态树的基本界面,因此我们可以看到状态随时间的变化。
项目结构如下:
结构非常简单(操作就像我们通常在 redux 应用程序中所做的那样),我已经将 initialState 从 reducer 中移出,因为 reducer 是关于状态修改而不是定义,此外 store 文件夹包含已经讨论过的 react context / hooks 实现。
Reducer 文件的设计相当不同:
import * as todo from './todo';
import * as types from 'actions/types';
const createReducer = handlers => (state, action) => {
if (!handlers.hasOwnProperty(action.type)) {
return state;
}
return handlers[action.type](state, action);
};
export default createReducer({
[types.ADD_TODO]: todo.add,
[types.REMOVE_TODO]: todo.remove,
[types.UPDATE_TODO]: todo.update,
[types.FILTER_TODO]: todo.filter,
[types.SHOW_STATE]: todo.showState,
});
这里的重点只是为了避免通常在带有映射对象的 Reducer 函数中看到的那些巨大的 switch 语句,因此基本上对于每个新的 Reducer,我们只需在映射对象中添加一个新的入口。
但同样,这完全是一个实现的问题,这里的要求是函数需要具有(state, action) => newState接口,就像我们已经习惯 Redux 一样。
最后但同样重要的是我们的组件订阅商店:
import React from 'react';
import { useStore } from 'store';
import { addTodo, filterTodo } from 'actions';
import uuid from 'uuid/v1';
import Button from '@material-ui/core/Button';
export default props => {
const [{ filter }, dispatch] = useStore();
const onClick = () => {
dispatch(addTodo({ id: uuid(), name: filter, done: false }));
dispatch(filterTodo(''));
};
return (
<Button
{...props}
variant='contained'
onClick={onClick}
disabled={!filter}
children='Add'
/>
);
};
接下来是什么
接下来的步骤将与中间件和类型检查相关,我们该如何操作呢?从技术上讲,中间件是在调度的 action 到达 reducer 之前调用的一个函数,因此上面的createReducer函数非常适合执行该操作。那么类型检查呢?用 Typescript 来实现!期待下次再见!
干杯!
参考文献:
https://github.com/acdlite/react-fiber-architecture
https://reactjs.org/docs/reconciliation.html
https://reactjs.org/docs/hooks-intro.html
https://github.com/vanderleisilva/react-context