Redux 包教包會(二):引入 combineReducers 拆分和組合狀態邏輯

在這一部分中,咱們將趁熱打鐵,運用上篇教程學到的 Redux 三大核心概念來將待辦事項的剩下部分重構完成,它涉及到將 TodoList 和 Footer 部分的相關代碼重構到 Redux,並使用 Redux combineReducers API 進行邏輯拆分和組合,使得咱們能夠在使用 Redux 便利的同時,又不至於讓應用的邏輯看起來臃腫不堪,複用 React 組件化的便利,咱們可讓狀態的處理也 「組件化」。前端

重構代碼:將 TodoList 部分遷移到 Redux

歡迎閱讀 Redux 包教包會系列:react

此教程屬於 React 前端工程師學習路線的一部分,點擊可查看所有內容。

在以前的幾個小節中,咱們已經把 Redux 的核心概念講完了,而且運用這些概念重構了一部分待辦事項應用,在這一小節中,咱們將趁熱打鐵,完整地運用以前學到的知識,繼續用 Redux 重構咱們的應用。git

此時若是你在瀏覽器裏面嘗試這個待辦事項小應用,你會發現它還只能夠添加新的待辦事項,對於 「完成和重作待辦事項」 以及 「過濾查看待辦事項」 這兩個功能,目前咱們尚未使用 Redux 實現。因此當你點擊單個待辦事項時,瀏覽器會報錯;當你點擊底部的三個過濾器按鈕時,瀏覽器不會有任何反應。github

在這一小節中,咱們將使用 Redux 重構 「完成和重作待辦事項」 功能,即你能夠經過點擊某個待辦事項來完成它。算法

咱們將運用 Redux 最佳實踐的開發方式來重構這一功能:編程

  • 定義 Action Creators
  • 定義 Reducers
  • connect 組件以及在組件中 dispatch Action

之後在開發 Redux 應用的時候,均可以使用這三步流程來周而復始地開發新的功能,或改進現有的功能。redux

定義 Action Creators

首先咱們要定義 「完成待辦事項」 這一功能所涉及的 Action,打開 src/actions/index.js,修改內容內容以下:數組

let nextTodoId = 0;

export const addTodo = text => ({
  type: "ADD_TODO",
  id: nextTodoId++,
  text
});

export const toggleTodo = id => ({
  type: "TOGGLE_TODO",
  id
});

能夠看到,咱們定義並導出了一個 toggleTodo 箭頭函數,它接收 id 並返回一個類型爲 "TOGGLE_TODO" 的 Action。瀏覽器

定義 Reducers

接着咱們來定義響應 dispatch(action) 的 Reducers,打開 src/index.js,修改 rootReducer 函數以下:前端工程師

import React from "react";
import ReactDOM from "react-dom";
import App, { VisibilityFilters } from "./components/App";

import { createStore } from "redux";
import { Provider } from "react-redux";

const initialState = {
  todos: [
    {
      id: 1,
      text: "你好, 圖雀",
      completed: false
    },
    {
      id: 2,
      text: "我是一隻小小小小圖雀",
      completed: false
    },
    {
      id: 3,
      text: "小若燕雀,亦可一展宏圖!",
      completed: false
    }
  ],
  filter: VisibilityFilters.SHOW_ALL
};

const rootReducer = (state, action) => {
  switch (action.type) {
    case "ADD_TODO": {
      const { todos } = state;

      return {
        ...state,
        todos: [
          ...todos,
          {
            id: action.id,
            text: action.text,
            completed: false
          }
        ]
      };
    }

    case "TOGGLE_TODO": {
      const { todos } = state;

      return {
        ...state,
        todos: todos.map(todo =>
          todo.id === action.id ? { ...todo, completed: !todo.completed } : todo
        )
      };
    }
    default:
      return state;
  }
};

const store = createStore(rootReducer, initialState);

ReactDOM.render(
  <Provider store={store}>
    <App />
  </Provider>,
  document.getElementById("root")
);

能夠看到,咱們在 switch 語句裏面添加了一個 "TOGGLE_TODO" 的判斷,並根據 action.id 來判斷對應操做的 todo,取反它目前的 completed 屬性,用來表示從完成到未完成,或從未完成到完成的操做。

connect 和 dispatch(action)

當定義了 Action,聲明瞭響應 Action 的 Reducers 以後,咱們開始定義 React 和 Redux 交流的接口:connectdispatch,前者負責將 Redux Store 的內容整合進 React,後者負責從 React 中發出操做 Redux Store 的指令。

咱們打開 src/components/TodoList.js 文件,對文件內容做出以下的修改:

import React from "react";
import PropTypes from "prop-types";
import Todo from "./Todo";

import { connect } from "react-redux";
import { toggleTodo } from "../actions";

const TodoList = ({ todos, dispatch }) => (
  <ul>
    {todos.map(todo => (
      <Todo
        key={todo.id}
        {...todo}
        onClick={() => dispatch(toggleTodo(todo.id))}
      />
    ))}
  </ul>
);

TodoList.propTypes = {
  todos: PropTypes.arrayOf(
    PropTypes.shape({
      id: PropTypes.number.isRequired,
      completed: PropTypes.bool.isRequired,
      text: PropTypes.string.isRequired
    }).isRequired
  ).isRequired
};

export default connect()(TodoList);

能夠看到,咱們對文件作出瞭如下幾步修改:

  • 首先從 react-redux 中導出 connect 函數,它負責給 TodoList 傳入 dispatch 函數,使得咱們能夠在 TodoList 組件中 dispatch Action。
  • 而後咱們導出了 toggleTodo Action Creators,並將以前從父組件接收 toggleTodo 方法並調用的方式改爲了當 Todo 被點擊以後,咱們 dispatch(toggle(todo.id))
  • 咱們刪除 propsTypes 中再也不須要的 toggleTodo

刪除無用代碼

當咱們經過以上三步整合了 Redux 的內容以後,咱們就能夠刪除原 App.js 中沒必要要的代碼了,打開 src/components/App.js 修改內容以下:

import React from "react";
import AddTodo from "./AddTodo";
import TodoList from "./TodoList";
import Footer from "./Footer";

import { connect } from "react-redux";

export const VisibilityFilters = {
  SHOW_ALL: "SHOW_ALL",
  SHOW_COMPLETED: "SHOW_COMPLETED",
  SHOW_ACTIVE: "SHOW_ACTIVE"
};

const getVisibleTodos = (todos, filter) => {
  switch (filter) {
    case VisibilityFilters.SHOW_ALL:
      return todos;
    case VisibilityFilters.SHOW_COMPLETED:
      return todos.filter(t => t.completed);
    case VisibilityFilters.SHOW_ACTIVE:
      return todos.filter(t => !t.completed);
    default:
      throw new Error("Unknown filter: " + filter);
  }
};

class App extends React.Component {
  constructor(props) {
    super(props);

    this.setVisibilityFilter = this.setVisibilityFilter.bind(this);
  }

  setVisibilityFilter(filter) {
    this.setState({
      filter: filter
    });
  }

  render() {
    const { todos, filter } = this.props;

    return (
      <div>
        <AddTodo />
        <TodoList todos={getVisibleTodos(todos, filter)} />
        <Footer
          filter={filter}
          setVisibilityFilter={this.setVisibilityFilter}
        />
      </div>
    );
  }
}

const mapStateToProps = (state, props) => ({
  todos: state.todos,
  filter: state.filter
});

export default connect(mapStateToProps)(App);

能夠看到,咱們刪除了 toggleTodo 方法,並對應刪除了定義在 constructor 中的 toggleTodo 定義以及在 render 方法中,傳給 TodoListtoggleTodo 屬性。

保存上述修改的代碼,打開瀏覽器,你應該又能夠點擊單個待辦事項來完成和重作它了:

小結

在本節中,咱們介紹了開發 Redux 應用的最佳實踐,並經過重構 "完成和重作待辦事項「 這一功能來詳細實踐了這一最佳實踐。

重構代碼:將 Footer 部分遷移到 Redux

這一節中,咱們將繼續重構剩下的部分。咱們將繼續遵循上一節提到的 Redux 開發的最佳實踐:

  • 定義 Action Creators
  • 定義 Reducers
  • connect 組件以及在組件中 dispatch Action

定義 Action Creators

打開 src/actions/index.js 文件,修改內容以下:

let nextTodoId = 0;

export const addTodo = text => ({
  type: "ADD_TODO",
  id: nextTodoId++,
  text
});

export const toggleTodo = id => ({
  type: "TOGGLE_TODO",
  id
});

export const setVisibilityFilter = filter => ({
  type: "SET_VISIBILITY_FILTER",
  filter
});

能夠看到咱們建立了一個名爲 setVisibilityFilter 的 Action Creators,它接收 filter 參數,而後返回一個類型爲 "SET_VISIBILITY_FILTER" 的 Action。

定義 Reducers

打開 src/index.js 文件,修改代碼以下:

import React from "react";
import ReactDOM from "react-dom";
import App, { VisibilityFilters } from "./components/App";

import { createStore } from "redux";
import { Provider } from "react-redux";

const initialState = {
  todos: [
    {
      id: 1,
      text: "你好, 圖雀",
      completed: false
    },
    {
      id: 2,
      text: "我是一隻小小小小圖雀",
      completed: false
    },
    {
      id: 3,
      text: "小若燕雀,亦可一展宏圖!",
      completed: false
    }
  ],
  filter: VisibilityFilters.SHOW_ALL
};

const rootReducer = (state, action) => {
  switch (action.type) {
    case "ADD_TODO": {
      const { todos } = state;

      return {
        ...state,
        todos: [
          ...todos,
          {
            id: action.id,
            text: action.text,
            completed: false
          }
        ]
      };
    }

    case "TOGGLE_TODO": {
      const { todos } = state;

      return {
        ...state,
        todos: todos.map(todo =>
          todo.id === action.id ? { ...todo, completed: !todo.completed } : todo
        )
      };
    }

    case "SET_VISIBILITY_FILTER": {
      return {
        ...state,
        filter: action.filter
      };
    }

    default:
      return state;
  }
};

const store = createStore(rootReducer, initialState);

ReactDOM.render(
  <Provider store={store}>
    <App />
  </Provider>,
  document.getElementById("root")
);

能夠看到,咱們增長了一條 case 語句,來響應 "SET_VISIBILITY_FILTER" Action,經過接收新的 filter 來更新 Store 中的狀態。

connect 和 dispatch(action)

打開 src/components/Footer.js 文件,修改內容以下:

import React from "react";
import Link from "./Link";
import { VisibilityFilters } from "./App";

import { connect } from "react-redux";
import { setVisibilityFilter } from "../actions";

const Footer = ({ filter, dispatch }) => (
  <div>
    <span>Show: </span>
    <Link
      active={VisibilityFilters.SHOW_ALL === filter}
      onClick={() => dispatch(setVisibilityFilter(VisibilityFilters.SHOW_ALL))}
    >
      All
    </Link>
    <Link
      active={VisibilityFilters.SHOW_ACTIVE === filter}
      onClick={() =>
        dispatch(setVisibilityFilter(VisibilityFilters.SHOW_ACTIVE))
      }
    >
      Active
    </Link>
    <Link
      active={VisibilityFilters.SHOW_COMPLETED === filter}
      onClick={() =>
        dispatch(setVisibilityFilter(VisibilityFilters.SHOW_COMPLETED))
      }
    >
      Completed
    </Link>
  </div>
);

export default connect()(Footer);

能夠看到,上面的文件主要作了這幾件事:

  • 首先從 react-redux 中導出 connect 函數,它負責給 Footer 傳入 dispatch 函數,使得咱們能夠在 Footer 組件中 dispatch Action。
  • 而後咱們導出了 setVisibilityFilter Action Creators,並將以前從父組件接收 setVisibilityFilter 方法並調用的方式改爲了當 Link 被點擊以後,咱們 dispatch 對應的 Action 。

刪除無用代碼

當咱們經過以上三步整合了 Redux 的內容以後,咱們就能夠刪除原 App.js 中沒必要要的代碼了,打開 src/components/App.js 修改內容以下:

import React from "react";
import AddTodo from "./AddTodo";
import TodoList from "./TodoList";
import Footer from "./Footer";

import { connect } from "react-redux";

export const VisibilityFilters = {
  SHOW_ALL: "SHOW_ALL",
  SHOW_COMPLETED: "SHOW_COMPLETED",
  SHOW_ACTIVE: "SHOW_ACTIVE"
};

const getVisibleTodos = (todos, filter) => {
  switch (filter) {
    case VisibilityFilters.SHOW_ALL:
      return todos;
    case VisibilityFilters.SHOW_COMPLETED:
      return todos.filter(t => t.completed);
    case VisibilityFilters.SHOW_ACTIVE:
      return todos.filter(t => !t.completed);
    default:
      throw new Error("Unknown filter: " + filter);
  }
};

class App extends React.Component {
  render() {
    const { todos, filter } = this.props;

    return (
      <div>
        <AddTodo />
        <TodoList todos={getVisibleTodos(todos, filter)} />
        <Footer filter={filter} />
      </div>
    );
  }
}

const mapStateToProps = (state, props) => ({
  todos: state.todos,
  filter: state.filter
});

export default connect(mapStateToProps)(App);

能夠看到,咱們刪除了 setVisibilityFilter 方法,並對應刪除了定義在 constructor 中的 setVisibilityFilter 定義以及在 render 方法中,傳給 FootersetVisibilityFilter 屬性。

由於 constructor 方法中已經不須要再定義內容了,因此咱們刪掉了它。

保存上述修改的代碼,打開瀏覽器,你應該又能夠繼續點擊底部的按鈕來過濾完成和未完成的待辦事項了:

小結

在本節中,咱們介紹了開發 Redux 應用的最佳實踐,並經過重構 "過濾查看待辦事項「 這一功能來詳細實踐了這一最佳實踐。

自此,咱們已經使用 Redux 重構了整個待辦事項小應用,可是重構完的這份代碼還顯得有點亂,不一樣類型的組件狀態混在一塊兒。當咱們的應用逐漸變得複雜時,咱們的 rootReducer 就會變得很是冗長,因此是時候考慮拆分不一樣組件的狀態了。

咱們將在下一節中講解如何將不一樣組件的狀態進行拆分,以確保咱們在編寫大型應用時也能夠顯得很從容。

combineReducers:組合拆分狀態的 Reducers

當應用邏輯逐漸複雜的時候,咱們就要考慮將巨大的 Reducer 函數拆分紅一個個獨立的單元,這在算法中被稱爲 」分而治之「。

Reducers 在 Redux 中其實是用來處理 Store 中存儲的 State 中的某個部分,一個 Reducer 和 State 對象樹中的某個屬性一一對應,一個 Reducer 負責處理 State 中對應的那個屬性。好比咱們來看一下如今咱們的 State 的結構:

const initialState = {
  todos: [
    {
      id: 1,
      text: "你好, 圖雀",
      completed: false
    },
    {
      id: 2,
      text: "我是一隻小小小小圖雀",
      completed: false
    },
    {
      id: 3,
      text: "小若燕雀,亦可一展宏圖!",
      completed: false
    }
  ],
  filter: VisibilityFilters.SHOW_ALL
};

由於 Reducer 對應着 State 相關的部分,這裏咱們的 State 有兩個部分:todosfilter,因此咱們能夠編寫兩個對應的 Reducer。

編寫 Reducer:todos

在 Redux 最佳實踐中,由於 Reducer 對應修改 State 中的相關部分,當 State 對象樹很大時,咱們的 Reducer 也會有不少,因此咱們通常會單獨建一個 reducers 文件夾來存放這些 "reducers「。

咱們在 src 目錄下新建 reducers 文件夾,而後在裏面新建一個 todos.js 文件,表示處理 State 中對應 todos 屬性的 Reducer:

const initialTodoState = [
  {
    id: 1,
    text: "你好, 圖雀",
    completed: false
  },
  {
    id: 2,
    text: "我是一隻小小小小圖雀",
    completed: false
  },
  {
    id: 3,
    text: "小若燕雀,亦可一展宏圖!",
    completed: false
  }
];

const todos = (state = initialTodoState, action) => {
  switch (action.type) {
    case "ADD_TODO": {
      return [
        ...state,
        {
          id: action.id,
          text: action.text,
          completed: false
        }
      ];
    }

    case "TOGGLE_TODO": {
      return state.map(todo =>
        todo.id === action.id ? { ...todo, completed: !todo.completed } : todo
      );
    }

    default:
      return state;
  }
};

export default todos;

能夠看到,上面的代碼作了這幾件事:

  • 首先咱們將原 initialState 裏面的 todos 部分拆分到了 src/reducers/todos.js 文件裏,咱們定義了一個 initialTodoState 表明以前的 initialStatetodos 部分,它是一個數組,並把它賦值給 todos 函數中 state 參數的默認值,即當調用此函數時,若是傳進來的 state 參數爲 undefined 或者 null 時,這個 state 就是 initialState
  • 接着咱們定義了一個 todos 箭頭函數,它的結構和 rootReducer 相似,都是接收兩個參數:stateaction,而後進入一個 switch 判斷語句,根據 action.type 判斷要相應的 Action 類型,而後對 state 執行對應的操做。
注意

咱們的 todos reducers 只負責處理原 initialStatetodos 部分,因此這裏它的 state 就是原 todos 屬性,它是一個數組,因此咱們在 switch 語句裏,進行數據改變時,要對數組進行操做,並最後返回一個新的數組。

編寫 Reducer:filter

咱們前面使用 todos reducer 解決了原 initialStatetodos 屬性操做問題,如今咱們立刻來說解剩下的 filter 屬性的操做問題。

src/reducers 文件夾下建立 filter.js 文件,在其中加入以下的內容:

import { VisibilityFilters } from "../components/App";

const filter = (state = VisibilityFilters.SHOW_ALL, action) => {
  switch (action.type) {
    case "SET_VISIBILITY_FILTER":
      return action.filter;
    default:
      return state;
  }
};

export default filter;

能夠看到咱們定義了一個 filter 箭頭函數,它接收兩個參數:stateaction,由於這個 filter reducer 只負責處理原 initialStatefilter 屬性部分,因此這裏這個 state 參數就是原 filter 屬性,這裏咱們給了它一個默認值。

注意

filter 函數的剩餘部分和 rootReducer 相似,可是注意這裏它的 state 是對 filter 屬性進行操做,因此當判斷 "SET_VISIBILITY_FILTER" action 類型時,它只是單純的返回 action.filter

組合多個 Reducer

當咱們將 rootReducer 的邏輯拆分,並對應處理 Store 中保存的 State 中的屬性以後,咱們能夠確保每一個 reducer 都很小,這個時候咱們就要考慮如何將這些小的 reducer 組合起來,構成最終的 rootReducer,這種組合就像咱們組合 React 組件同樣,最終只有一個根級組件,在咱們的待辦事項小應用裏面,這個組件就是 App.js 組件。

Redux 爲咱們提供了 combineReducers API,用來組合多個小的 reducer,咱們在 src/reducers 文件夾下建立 index.js 文件,並在裏面添加以下內容:

import { combineReducers } from "redux";

import todos from "./todos";
import filter from "./filter";

export default combineReducers({
  todos,
  filter
});

能夠看到,咱們從 redux 模塊中導出了 combineReducers 函數,而後導出了以前定義的 todosfilter reducer。

接着咱們經過對象簡潔表示法,將 todosfilter 做爲對象屬性合在一塊兒,而後傳遞給 combineReducers 函數,這裏 combineReducers 內部就會對 todosfilter 進行操做,而後生成相似咱們以前的 rootReducer 形式。最後咱們導出生成的 rootReducer

combineReducers 主要有兩個做用:

1)組合全部 reducer 的 state,最後組合成相似咱們以前定義的 initialState 對象狀態樹。

即這裏 todos reducer 的 state 爲:

state = [
  {
    id: 1,
    text: "你好, 圖雀",
    completed: false
  },
  {
    id: 2,
    text: "我是一隻小小小小圖雀",
    completed: false
  },
  {
    id: 3,
    text: "小若燕雀,亦可一展宏圖!",
    completed: false
  }
];

filter reducer 的 state 爲:

state = VisibilityFilters.SHOW_ALL

那麼經過 combineReducers 組合這兩個 reducerstate 獲得的最終結果爲:

state = {
  todos: [
    {
      id: 1,
      text: "你好, 圖雀",
      completed: false
    },
    {
      id: 2,
      text: "我是一隻小小小小圖雀",
      completed: false
    },
    {
      id: 3,
      text: "小若燕雀,亦可一展宏圖!",
      completed: false
    }
  ],
  filter: VisibilityFilters.SHOW_ALL
};

這個經過 combineReducers 組合後的最終 state 就是存儲在 Store 裏面的那棵 State JavaScript 對象狀態樹。

2)分發 dispatch 的 Action。

經過 combineReducers 組合 todosfilter reducer 以後,從 React 組件中 dispatch Action會遍歷檢查 todosfilter reducer,判斷是否存在響應對應 action.typecase 語句,若是存在,全部的這些 case 語句都會響應。

刪除沒必要要的代碼

當咱們將原 rootReducer 拆分紅了 todosfilter 兩個 reducer ,並經過 redux 提供的 combineReducers API 進行組合後,咱們以前在 src/index.js 定義的 initialStaterootReducer 就再也不須要了,因此咱們立刻來刪除它們:

import React from "react";
import ReactDOM from "react-dom";
import App, { VisibilityFilters } from "./components/App";

import { createStore } from "redux";
import { Provider } from "react-redux";
import rootReducer from "./reducers";

const store = createStore(rootReducer);

ReactDOM.render(
  <Provider store={store}>
    <App />
  </Provider>,
  document.getElementById("root")
);

能夠看到,咱們從刪除了以前在 src/index.js 定義的 rootReducer,轉而使用了從 src/reducers/index.js 導出的 rootReducer

而且咱們咱們以前講到,combineReducers 的第一個功能就是組合多個 reducer 的 state,最終合併成一個大的 JavaScript 對象狀態樹,而後自動存儲在 Redux Store 裏面,因此咱們再也不須要給 createStore 顯式的傳遞第二個 initialState 參數了。

保存修改的內容,打開瀏覽器,能夠照樣能夠操做全部的功能,你能夠加點待辦事項,點擊某個待辦事項以完成它,經過底部的三個過濾按鈕查看不一樣狀態下的待辦事項:

小結

在這一小節中,咱們講解了 redux 提供的 combineReducers API,它主要解決兩個問題:

  • 當應用逐漸複雜的時候,咱們須要對 Reducer 進行拆分,那麼咱們就須要把拆分後的 Reducer 進行組合,併合並全部的 State。
  • 對於每一個 React 組件 dispatch 的 Action,將其分發給對應的 Reducer。

當有了 combineReducers 以後,無論咱們的應用如何複雜,咱們均可以將處理應用狀態的邏輯拆分都一個一個很簡潔、易懂的小文件,而後組合這些小文件來完成複雜的應用邏輯,這和 React 組件的組合思想相似,能夠想見,組件式編程的威力是多麼巨大!

此教程屬於 React 前端工程師學習路線的一部分,點擊可查看所有內容。

想要學習更多精彩的實戰技術教程?來圖雀社區逛逛吧。

相關文章
相關標籤/搜索