使用 React Hooks 代替 Redux

使用 React Hooks 代替 Redux

注: 此文章立場不表示 Hooks 能夠徹底代替 Redux。
由於 Redux 還有其餘適用的場景和功能,只是在大部分場景能夠用 Hooks 代替。
理性選擇即合理。react

React Hooks 面世也有很大一段時間了,我相信不少人對於 Hooks 的認知還大概處在:數據庫

  • 更 FP「Functional Programming」 編程方式
  • 更簡潔易測的組件
  • 不用記住繁瑣的生命週期函數
  • … 上述這些特徵點已經足以說服很大一部分人升級他們的 React 應用。 可是老是感受少了點什麼。

咱們知道 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數據庫設計

hooks

簡單分析

redux 的數據流程圖畫得比較簡單,理解大概意思就好,畢竟它不是我要說的重點, 和 hooks 的數據流程相比實際上是大同小異。

從 hooks 數據流能大體看出來, 咱們設計好 store 後,經過對應的 hooks 函數生成每一個 store 的 Provider 和 Context。 咱們把全部的單個 Provider 糅合爲一個總體的 Providers,做爲全部 UI 的父組件。

在任何一個子 UI 組件內部,經過 hooks 函數獲得對應 store 的 state、dispatch。 UI 組件內,經過主動調用 dispatch 發送 action,而後通過 store 的數據處理中心 reducer,就能觸發相應的數據改變。 這個數據流程和 redux 幾乎如出一轍。

相同點

  • 統一 store 數據管理
  • 支持以發送 action 來修改數據
  • 支持 action 處理中心:reducer

異同點

  • hooks UI 層獲取 store 和 dispatch 不須要用 HOC 依賴注入,而是用 useContext
  • redux 在 action 以後改變視圖本質上仍是 state 注入的方式修改的組件內部 state,而 hooks 則是一對一的數據觸發
  • hooks 的 reducer 來自於 useReducer
  • hooks 尚未 middleware 的解決方案

構建應用 DEMO

在構建應用以前,咱們應該充分了解咱們的應用,瞭解每個 API 接口和返回的數據。這樣不至於開發中期再來修改咱們的倉庫設計。 須要咱們設計一個本地數「全局 store」,和相應的 action 用來修改這些數據。 其次就是目錄設計了。 接下來咱們以一個 TO DO Lists 爲例開發一個純 hooks 的 SPA 吧。

本地數據庫設計

  • 一個叫 list 的倉庫
  • 三個 action: 增「ADD」、刪「DELETE」、改「MODIFY」

目錄結構

這個目錄是比較簡單的,畢竟是個 DEMO,和 hooks 無關的沒列出來。

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 數據。

store 設計

provider.js

...

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。

文章轉載地址: fed.taobao.org/blog/2019/0…
相關文章
相關標籤/搜索