注: 此文章立场不表示 Hooks 能够彻底代替 Redux。
由于 Redux 还有其余适用的场景和功能,只是在大部分场景能够用 Hooks 代替。
理性选择即合理。react
React Hooks 面世也有很大一段时间了,我相信不少人对于 Hooks 的认知还大概处在:数据库
咱们知道 React 是一个以构建 UI 为主的的库: A JavaScript library for building user interfaces. 可是 UI 若是脱离了数据,基本上也就是耍流氓了。 因此有 Redux、Mbox… 这样以数据管理为核心的库出现了。 现实业务场景中,UI 与数据相辅相成。npm
在我最初学 React 的时候,原于成熟的方案、同事的推荐,是直接和 Redux 一块儿学习而且上手开发的。 当时我就在想:React 为何不能本身实现相似 Redux 那样的数据处理功能呢? 对于想学习 React 的同窗,无疑是增长了 Redux 的学习成本, 更加深了 React 的门槛与神秘值「这可不是一个优秀的开源库该有的特质」。编程
往简单了说 Redux 就是实现了全局 state 、处理全局 state 的方式和统一的数据处理中心,也就是 store、dispatch 和 reducer。 虽然在 Hooks 以前咱们能够经过 Context 模拟全局 state,可是咱们还不能优雅的模拟 dispatch、reducer。redux
若是 React 官方能出一个数据处理的解决方案, 不仅仅是减小一个 Redux npm 包的 bundle 体积, 还下降了学习与构建 React 应用的成本, 最重要的是更统一化的数据处理思想。数组
年前,我在构建一个新的后台管理应用,考虑使用全新的 Hooks API。 当时 React 最新的版本仍是 16.7.0-alpha.2。 在对于数据处理上,我尝试了新的 React Context API, 使用 Context API 提供的 Provider 和 Consumer 的方法,去实现代替 Redux 的数据处理方案「这也是网上大部分推荐的代替 Redux 的方案」。 可是代码越写越多,数据处理量愈来愈大,数据分类愈来愈多的时候,Context 显得力不从心, 虽然能解决需求,可是代码组织方式已经乱成了一锅粥「尝试过这个方案的人,应该知道我在说什么」。bash
注:更不要使用 useState + context 的方式建立全局仓库来代替 Redux数据结构
十分万幸的是,不久后 React 更新版本到 16.8.1。 推出了新的 Hooks:useReducer,惊喜以外意料之中。 这也就是这篇文章要讲的核心:使用 Hooks:useReducer 代替 Redux。dom
redux数据库设计
redux 的数据流程图画得比较简单,理解大概意思就好,毕竟它不是我要说的重点, 和 hooks 的数据流程相比实际上是大同小异。
从 hooks 数据流能大体看出来, 咱们设计好 store 后,经过对应的 hooks 函数生成每一个 store 的 Provider 和 Context。 咱们把全部的单个 Provider 糅合为一个总体的 Providers,做为全部 UI 的父组件。
在任何一个子 UI 组件内部,经过 hooks 函数获得对应 store 的 state、dispatch。 UI 组件内,经过主动调用 dispatch 发送 action,而后通过 store 的数据处理中心 reducer,就能触发相应的数据改变。 这个数据流程和 redux 几乎如出一辙。
在构建应用以前,咱们应该充分了解咱们的应用,了解每个 API 接口和返回的数据。这样不至于开发中期再来修改咱们的仓库设计。 须要咱们设计一个本地数「全局 store」,和相应的 action 用来修改这些数据。 其次就是目录设计了。 接下来咱们以一个 TO DO Lists 为例开发一个纯 hooks 的 SPA 吧。
index.js 应用入口
...
import List from 'Pages/List/index';
import Layout from 'Components/Layout/index';
import history from './history';
ReactDOM.render(
<Router history={history}>
<Layout>
<Switch>
<Redirect exact from="/" to="/List" />
<Route path="/list" component={List} />
<Route component={NotFound} />
</Switch>
</Layout>
</Router>,
document.getElementById('container')
);
复制代码
components/Layout/index 应用主结构
...
// 引入组合 Provider
import Provider from 'Store/provider';
import Header from './components/Header/index';
import Footer from './components/Footer/index';
const Layout = (props) => (
<Provider>
<Header />
{props.children}
<Footer />
</Provider>
);
export default Layout;
复制代码
这里的代码很关键,在 Layout 中咱们引入「组合 Provider」, 提供「统一仓库数据提供」的能力,让子 UI 组件能获取 store 数据。
...
import Lists from './lists/index';
const Provider = (props) => {
return (
<Lists.Provider>
{props.children}
</Lists.Provider>
);
};
export default Provider;
复制代码
仔细观察这里的代码人应该会发现一个问题: 在 store 拓展的的状况下,这个代码极可能出现 代码嵌套地狱,相似这样:
...
// 多个 store 实例的状况
import One from './One/index';
import Two from './Two/index';
import Three from './Three/index';
...
const Provider = (props) => {
return (
<One.Provider>
<Two.Provider>
<Three.Provider>
...
{props.children}
...
</Three.Provider>
</Two.Provider>
</One.Provider>
);
};
export default Provider;
复制代码
因此须要 provider 组合器。
优化后 provider.js
...
import providers from './providers';
// 数据 Provider 组合器
const ProvidersComposer = (props) => (
props.providers.reduceRight((children, Parent) => (
<Parent>{children}</Parent>
), props.children)
);
const Provider = (props) => {
return (
<ProvidersComposer providers={providers}>
{props.children}
</ProvidersComposer>
);
};
export default Provider;
复制代码
providers.js
import Lists from './lists/index';
const providers = [Lists.Provider];
export default providers;
复制代码
即便有多个 Provider 咱们也能够经过一维数组搞定啦!
数据项 && 数据处理器
在构建好基本的 Provider 后,咱们须要提供基本的数据项和 reducer。
数据项
import React, { useReducer } from 'react';
import reducer, { initState } from './reducer';
const Context = React.createContext();
const Provider = (props) => {
const [state, dispatch] = useReducer(reducer, initState);
return (
<Context.Provider value={{ state, dispatch }}>
{props.children}
</Context.Provider>
);
};
export default { Context, Provider };
复制代码
首先使用 createContext 函数建立好上下 Context,而且对 Context 的 Provider 提供初始化 value,即 state、dispatch。 初始化的 state、dispatch 来自于 hooks:useReducer: 经过 useReducer 函数传入 reducer、initState,获得这样的数据结构: [state, dispatch]。
不一样的数据项的代码彻底是通用,差别点在于每一个数据项的 reducer、initState 不同。 reducer
export const initState = []; // 默认 todolist 是空数组
// 数据处理器
const reducer = (state, action) => {
const { type, payload } = action;
switch (type) {
case 'ADD':
return [...state, payload.data];
case 'MODIFY':
return state.map(
item => (item.id === payload.id ? payload.data : item);
case 'DELETE':
return state.map(
item => (item.id === payload.id ? null : item)
).filter(n => n);
default:
return state;
}
};
export default reducer;
复制代码
能看出来,hooks reduer 和 redux reducer 基本没有区别。 咱们根据 action 更新 state,仍是那么熟悉的味道,那么熟悉的 switch 函数。
UI 组件
import React from 'react';
import StoreLists from 'Store/lists/index';
const Lists = () => {
const { state: lists, dispatch: listsDispatch } = React.useContext(StoreLists.Context);
const { newList, setNewList } = React.useState('');
handleDelete = item => () => {
listsDispatch({
type: 'DELETE',
payload: item
});
};
handleSetNewList = e => {
setNewList(e.value);
};
handleNewList = () => {
listsDispatch({
type: 'ADD',
payload: {
id: Math.random() * 100,
name: newList
}
});
};
return (
<div>
<h1>TO DO list 列表</h1>
<ul>
{lists.map(item => (
<li key={item.id}>
{item.name}
<button onClick={handleDelete(item)}>删除</button>
</li>
))}
</ul>
<inpt type="text" value={newList} onChange={handleSetNewList} />
<button onChange={handleNewList}>新建列表</button>
</div>
);
};
export default Lists;
复制代码
在 UI 组件内,使用 hooks:useContext。 useContext 接受 store 导出的 Context 做为参数,获得 state、dispatch。 使用 state 渲染数据,使用 dispatch 修改数据。
经过上面的目录结构、store 设计、UI 组件三大步骤,咱们可使用 hooks 搭建出和 redux 同样的数据处理流程应用了。 若是想进一步了解,能够参考应用:tw-agents。