前往小程序,Get更优阅读体验!
立即前往
首页
学习
活动
专区
工具
TVP
发布
社区首页 >专栏 >使用 React Hooks 代替 Redux

使用 React Hooks 代替 Redux

作者头像
Nealyang
发布2019-11-27 15:24:47
1.5K0
发布2019-11-27 15:24:47
举报
文章被收录于专栏:全栈前端精选全栈前端精选

使用 React Hooks 代替 Redux

注:此文章立场不表示 Hooks 可以完全代替 Redux。因为 Redux 还有其他适用的场景和功能,只是在大部分场景可以用 Hooks 代替。理性选择即合理。

React Hooks 面世也有很大一段时间了。我相信很多人对于 Hooks 的认知还大概处在:

  1. 更 FP「Functional Programming」 编程方式
  2. 更简洁易测的组件
  3. 不用记住繁琐的生命周期函数

上述这些特征点已经足以说服很大一部分人升级他们的 React 应用。但是总是感觉少了点什么。

我们知道 React 是一个以构建 UI 为主的的库:A JavaScript library for building user interfaces. 但是 UI 如果脱离了数据,基本上也就是耍流氓了。所以有 Redux、Mbox… 这样以数据管理为核心的库出现了。现实业务场景中,UI 与数据相辅相成。

在我最初学 React 的时候,原于成熟的方案、同事的推荐,是直接和 Redux 一起学习并且上手开发的。当时我就在想:React 为什么不能自己实现类似 Redux 那样的数据处理功能呢?对于想学习 React 的同学,无疑是增加了 Redux 的学习成本, 更加深了 React 的门槛与神秘值「这可不是一个优秀的开源库该有的特质」。

往简单了说 Redux 就是实现了全局 state 、处理全局 state 的方式和统一的数据处理中心,也就是 store、dispatch 和 reducer。虽然在 Hooks 之前我们可以通过 Context 模拟全局 state,但是我们还不能优雅的模拟 dispatch、reducer。

如果 React 官方能出一个数据处理的解决方案, 不单单是减少一个 Redux npm 包的 bundle 体积, 还降低了学习与构建 React 应用的成本, 最重要的是更统一化的数据处理思想。

年前,我在构建一个新的后台管理应用,考虑使用全新的 Hooks API。当时 React 最新的版本还是 16.7.0-alpha.2。在对于数据处理上,我尝试了新的 React Context API, 使用 Context API 提供的 ProviderConsumer 的方法,去实现代替 Redux 的数据处理方案「这也是网上大部分推荐的代替 Redux 的方案」。但是代码越写越多,数据处理量越来越大,数据分类越来越多的时候,Context 显得力不从心, 虽然能解决需求,但是代码组织方式已经乱成了一锅粥「尝试过这个方案的人,应该知道我在说什么」。

注:更不要使用 useState + context 的方式创建全局仓库来代替 Redux。

十分万幸的是,不久后 React 更新版本到 16.8.1。推出了新的 Hooks:useReducer,惊喜之外意料之中。这也就是这篇文章要讲的核心:使用 Hooks:useReducer 代替 Redux。

数据流对比

redux

redux-数据流.png

hooks

react-hooks-数据流.jpg

简单分析

redux 的数据流程图画得比较简单,理解大概意思就好,毕竟它不是我要说的重点, 和 hooks 的数据流程相比其实是大同小异。

从 hooks 数据流能大致看出来, 我们设计好 store 后,通过对应的 hooks 函数生成每个 store 的 Provider 和 Context。我们把所有的单个 Provider 糅合为一个整体的 Providers,作为所有 UI 的父组件。

在任何一个子 UI 组件内部,通过 hooks 函数得到对应 store 的 state、dispatch。UI 组件内,通过主动调用 dispatch 发送 action,然后经过 store 的数据处理中心 reducer,就能触发相应的数据改变。这个数据流程和 redux 几乎一模一样。

相同点

  1. 统一 store 数据管理
  2. 支持以发送 action 来修改数据
  3. 支持 action 处理中心:reducer

异同点

  1. hooks UI 层获取 store 和 dispatch 不需要用 HOC 依赖注入,而是用 useContext
  2. redux 在 action 之后改变视图本质上还是 state 注入的方式修改的组件内部 state,而 hooks 则是一对一的数据触发
  3. hooks 的 reducer 来自于 useReducer
  4. hooks 还没有 middleware 的解决方案

构建应用 DEMO

在构建应用之前,我们应该充分了解我们的应用,了解每一个 API 接口和返回的数据。这样不至于开发中期再来修改我们的仓库设计。需要我们设计一个本地数「全局 store」,和相应的 action 用来修改这些数据。其次就是目录设计了。接下来我们以一个 TO DO Lists 为例开发一个纯 hooks 的 SPA 吧。

本地数据库设计

  • 一个叫 list 的仓库
  • 三个 action: 增「ADD」、删「DELETE」、改「MODIFY」

目录结构

文件结构.png

这个目录是比较简单的,毕竟是个 DEMO,和 hooks 无关的没列出来。

index.js 应用入口
代码语言:javascript
复制
    ...
    
    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 应用主结构
代码语言:javascript
复制
    ...
    // 引入组合 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 数据。

store 设计

store.png

provider.js
代码语言:javascript
复制
    ...
    
    import Lists from'./lists/index';
    
    const Provider = (props) => {
    return (
        <Lists.Provider>
          {props.children}
        </Lists.Provider>
      );
    };
    
    export default Provider;

仔细观察这里的代码人应该会发现一个问题:在 store 拓展的的情况下,这个代码很可能出现 代码嵌套地狱,类似这样:

代码语言:javascript
复制
    ...
    // 多个 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
代码语言:javascript
复制
    ...
    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>
      );
    };
    
    exportdefault Provider;

上面代码灵感来自:How to combo multiple ContextProvider

providers.js
代码语言:javascript
复制
    import Lists from'./lists/index';
    
    const providers = [Lists.Provider];
    
    exportdefault providers;

即使有多个 Provider 我们也可以通过一维数组搞定啦!

数据项 && 数据处理器

在构建好基本的 Provider 后,我们需要提供基本的数据项和 reducer。

数据项
代码语言:javascript
复制
    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
代码语言:javascript
复制
    exportconst 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;
      }
    };
    
    exportdefault reducer;

能看出来,hooks reduer 和 redux reducer 基本没有区别。我们根据 action 更新 state,还是那么熟悉的味道,那么熟悉的 switch 函数。

UI 组件

代码语言:javascript
复制
   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>
     );
   };
   
   exportdefault Lists;

在 UI 组件内,使用 hooks:useContext。useContext 接受 store 导出的 Context 作为参数,得到 state、dispatch。使用 state 渲染数据,使用 dispatch 修改数据。

真实代码示例

通过上面的目录结构、store 设计、UI 组件三大步骤,我们可以使用 hooks 搭建出和 redux 一样的数据处理流程应用了。如果想进一步了解,可以参考应用:tw-agents。https://github.com/hangyangws/tw-agents

本文参与 腾讯云自媒体分享计划,分享自微信公众号。
原始发表:2019-11-23,如有侵权请联系 cloudcommunity@tencent.com 删除

本文分享自 全栈前端精选 微信公众号,前往查看

如有侵权,请联系 cloudcommunity@tencent.com 删除。

本文参与 腾讯云自媒体分享计划  ,欢迎热爱写作的你一起参与!

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
目录
  • 数据流对比
    • redux
      • hooks
        • 简单分析
          • 相同点
            • 异同点
            • 构建应用 DEMO
              • 本地数据库设计
                • 目录结构
                  • index.js 应用入口
                  • components/Layout/index 应用主结构
                • store 设计
                  • provider.js
                  • 优化后 provider.js
                  • providers.js
                • 数据项 && 数据处理器
                  • 数据项
                  • reducer
                • UI 组件
                  • 真实代码示例
                  领券
                  问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档