Redux 實戰入門

前言

上一節我們了解了 Redux 基本的概念和特性後,本章我們要實際動手用 Redux、React Redux 結合 ImmutableJS 開發一個簡單的 Todo 應用。話不多說,那就讓讓我們開始吧!

以下這張圖表示了整個 React Redux App 的資料流程圖(使用者與 View 互動 => dispatch 出 Action => Reducers 依據 action tyoe 分配到對應處理方式,回傳新的 state => 透過 React Redux 傳送給 React,React 重新繪製 View):

React Redux

動手創作 React Redux ImmutableJS TodoApp

在開始創作之前我們先完成一些開發的前置作業,先透過以下指令在根目錄產生 npm 設定檔 package.json

  1. $ npm init

安裝相關套件(包含開發環境使用的套件):

  1. $ npm install --save react react-dom redux react-redux immutable redux-actions redux-immutable
  1. $ npm install --save-dev babel-core babel-eslint babel-loader babel-preset-es2015 babel-preset-react eslint eslint-config-airbnb eslint-loader eslint-plugin-import eslint-plugin-jsx-a11y eslint-plugin-react html-webpack-plugin webpack webpack-dev-server

安裝好後我們可以設計一下我們的資料夾結構,首先我們在根目錄建立 src,放置 scriptsource 。在 components 資料夾中我們會放置所有 components(個別元件資料夾中會用 index.js 輸出元件,讓引入元件更簡潔)、containers(負責和 store 互動取得 state),另外還有 actionsconstantsreducersstore,其餘設定檔則放置於根目錄下。

大致上的資料夾結構會長這樣:

React Redux

接下來我們參考上一章設定一下開發文檔(.babelrc.eslintrcwebpack.config.js)。這樣我們就完成了開發環境的設定可以開始動手實作 React Redux 應用程式了!

首先我們先用 Component 之眼感受一下我們應用程式,將它切成一個個 Component。在這邊我們設計一個主要的 Main 包含兩個子 Component:TodoHeaderTodoList

React Redux

首先設計 HTML Markup:

  1. <!DOCTYPE html>
  2. <html lang="en">
  3. <head>
  4. <meta charset="UTF-8">
  5. <title>Redux Todo</title>
  6. </head>
  7. <body>
  8. <div id="app"></div>
  9. </body>
  10. </html>

在撰寫 src/index.js 之前,我們先說明整合 react-redux 的用法。從以下這張圖可以看到 react-redux 是 React 和 Redux 間的橋樑,使用 Providerconnect 去連結 store 和 React View。

React Redux

事實上,整合了 react-redux 後,我們的 React App 就可以解決傳統跨 Component 之前傳遞 state 的問題和困難。只要透過 Provider 就可以讓每個 React App 中的 Component 取用 store 中的 state,非常方便(接下來我們也會更詳細說明 Container/Component、connect 的用法)。

React Redux

以下是 src/index.js 完整程式碼:

  1. import React from 'react';
  2. import ReactDOM from 'react-dom';
  3. import { Provider } from 'react-redux';
  4. import Main from './components/Main';
  5. import store from './store';
  6. ReactDOM.render(
  7. <Provider store={store}>
  8. <Main />
  9. </Provider>,
  10. document.getElementById('app')
  11. );

其中 src/components/Main/Main.js 是 Stateless Component,負責所有 View 的進入點。

  1. import React from 'react';
  2. import ReactDOM from 'react-dom';
  3. import TodoHeaderContainer from '../../containers/TodoHeaderContainer';
  4. import TodoListContainer from '../../containers/TodoListContainer';
  5. const Main = () => (
  6. <div>
  7. <TodoHeaderContainer />
  8. <TodoListContainer />
  9. </div>
  10. );
  11. export default Main;

接下來我們定義一下 Actions 的部份,由於是範例 App 所以相對簡單,這邊只定義一個 todoActions。在這邊我們使用了 redux-actions,它可以方便我們使用 Flux Standard Action 格式的 action。以下是 src/actions/todoActions.js 完整程式碼:

  1. import { createAction } from 'redux-actions';
  2. import {
  3. CREATE_TODO,
  4. DELETE_TODO,
  5. CHANGE_TEXT,
  6. } from '../constants/actionTypes';
  7. export const createTodo = createAction('CREATE_TODO');
  8. export const deleteTodo = createAction('DELETE_TODO');
  9. export const changeText = createAction('CHANGE_TEXT');

我們在 src/actions/index.js 將所有 actions 輸出

  1. export * from './todoActions';

另外我們把 constants 放到 components 資料夾中方便管理,以下是 src/constants/actionTypes.js 程式碼:

  1. export const CREATE_TODO = 'CREATE_TODO';
  2. export const DELETE_TODO = 'DELETE_TODO';
  3. export const CHANGE_TEXT = 'CHANGE_TEXT';
  4. /*
  5. 或是可以考慮使用 keyMirror,方便產生與 key 相同的常數
  6. import keyMirror from 'fbjs/lib/keyMirror';
  7. export default keyMirror({
  8. ADD_ITEM: null,
  9. DELETE_ITEM: null,
  10. DELETE_ALL: null,
  11. FILTER_ITEM: null
  12. });
  13. */

設定 Actions 後我們來討論一下 Reducers 的部份。在討論 Reducers 之前我們先來設定一下我們的前端的資料結構,在這邊我們把所有資料結構(initialState)放到 src/constants/models.js 中。這邊特別注意的是由於 Redux 中有一個重要特性是 State is read-only,也就是說更新當 reducers 進到 action 只會回傳新的 state 不會更改到原有的 state。因此我們會在整個 Redux App 中使用 ImmutableJS 讓整個資料流維持在 Immutable 的狀態,也可以提昇程式開發上的效能和避免不可預期的副作用。

以下是 src/constants/models.js 完整程式碼,其設定了 TodoState 的資料結構並使用 fromJS() 轉成 Immutable

  1. import Immutable from 'immutable';
  2. export const TodoState = Immutable.fromJS({
  3. 'todos': [],
  4. 'todo': {
  5. id: '',
  6. text: '',
  7. updatedAt: '',
  8. completed: false,
  9. }
  10. });

接下來我們要討論的是 Reducers 的部份,在 todoReducers 中我們會根據接收到的 action 進行 mapping 到對應的處理函式並傳入夾帶的 payload 資料(這邊我們使用 redux-actions 來進行 mapping,使用上比傳統的 switch 更為簡潔)。Reducers 接收到 action 的處理方式為 (initialState, action) => newState,最終會回傳一個新的 state,而非更改原來的 state,所以這邊我們使用 ImmutableJS

  1. import { handleActions } from 'redux-actions';
  2. import { TodoState } from '../../constants/models';
  3. import {
  4. CREATE_TODO,
  5. DELETE_TODO,
  6. CHANGE_TEXT,
  7. } from '../../constants/actionTypes';
  8. const todoReducers = handleActions({
  9. CREATE_TODO: (state) => {
  10. let todos = state.get('todos').push(state.get('todo'));
  11. return state.set('todos', todos)
  12. },
  13. DELETE_TODO: (state, { payload }) => (
  14. state.set('todos', state.get('todos').splice(payload.index, 1))
  15. ),
  16. CHANGE_TEXT: (state, { payload }) => (
  17. state.merge({ 'todo': payload })
  18. )
  19. }, TodoState);
  20. export default todoReducers;
  1. import { handleActions } from 'redux-actions';
  2. import UiState from '../../constants/models';
  3. export default handleActions({
  4. SHOW: (state, { payload }) => (
  5. state.set('todos', payload.todo)
  6. ),
  7. }, UiState);

雖然 Redux 本身僅會有一個 store,但 redux 本身有提供了 combineReducers 可以讓我們切割我們 state 方便維護和管理。實上,state 的規劃也是一們學問,通常需要不斷地實作和工作團隊討論才能找到比較好的方式。不過這邊要注意的是我們改使用了 redux-immutablecombineReducers 這樣可以確保我們的 state 維持在 Immutable 的狀態。

由於 Redux 官方也沒有特別明確或嚴謹的規範。在一般情況我會將 reducers 分為 data 和單純和 UI 有關的 ui state。但由於這邊是比較簡單的例子,我們最終只使用到 src/reducers/data/todoReducers.js

  1. import { combineReducers } from 'redux-immutable';
  2. import ui from './ui/uiReducers';// import routes from './routes';
  3. import todo from './data/todoReducers';// import routes from './routes';
  4. const rootReducer = combineReducers({
  5. todo,
  6. });
  7. export default rootReducer;

還記得我們上面說明 React Redux 之前的橋樑時有提到的 store 嗎?現在我們要更仔細地去設計 store,我們這邊使用到了 redux 其中兩個 API:applyMiddleware、createStore。分別可以產生 store 和掛載我們要使用的 middleware(這邊我們只使用到 redux-logger 方便我們除錯)。注意我們 initialState 也是維持在 Immutable 的狀態。

  1. import { createStore, applyMiddleware } from 'redux';
  2. import createLogger from 'redux-logger';
  3. import Immutable from 'immutable';
  4. import rootReducer from '../reducers';
  5. const initialState = Immutable.Map();
  6. export default createStore(
  7. rootReducer,
  8. initialState,
  9. applyMiddleware(createLogger({ stateTransformer: state => state.toJS() }))
  10. );

透過 src/store/index.js 輸出 configureStore:

  1. export { default } from './configureStore';

講解完架構層面的議題,終於我們來到了 View 的部份。加油,距離我們終點也不遠了!
在開始討論 Component 的部份之前我們先來研究一下

react-redux 所提供的 API connect 將 props 傳給 Component,其用法如下:

connect([mapStateToProps], [mapDispatchToProps], [mergeProps], [options])

在我們的範例 App 中我們只會先用到前兩個參數,第三個參數會在之後的例子裡用到。第一個參數 mapStateToProps 是一個讓開發者可以從 store 取出想要 state 並當做 props 往下傳的功能,第二個參數則是將 dispatch 行為封裝成函數順著 props 可以方便往下傳和呼叫。

以下是 src/components/TodoHeader/TodoHeader.js 的部份:

  1. import React from 'react';
  2. import ReactDOM from 'react-dom';
  3. import { connect } from 'react-redux';
  4. import TodoHeader from '../../components/TodoHeader';
  5. // 將欲使用的 actions 引入
  6. import {
  7. changeText,
  8. createTodo,
  9. } from '../../actions';
  10. const mapStateToProps = (state) => ({
  11. // 從 store 取得 todo state
  12. todo: state.getIn(['todo', 'todo'])
  13. });
  14. const mapDispatchToProps = (dispatch) => ({
  15. // 當使用者在 input 輸入資料值即會觸發這個函數,發出 changeText action 並附上使用者輸入內容 event.target.value
  16. onChangeText: (event) => (
  17. dispatch(changeText({ text: event.target.value }))
  18. ),
  19. // 當使用者按下送出時,發出 createTodo action 並清空 input
  20. onCreateTodo: () => {
  21. dispatch(createTodo());
  22. dispatch(changeText({ text: '' }));
  23. }
  24. });
  25. export default connect(
  26. mapStateToProps,
  27. mapDispatchToProps,
  28. )(TodoHeader);
  29. // 開始建設 Component 並使用 connect 進來的 props 並綁定事件(onChange、onClick)。注意我們的 state 因為是使用 `ImmutableJS` 所以要用 `get()` 取值
  30. const TodoHeader = ({
  31. onChangeText,
  32. onCreateTodo,
  33. todo,
  34. }) => (
  35. <div>
  36. <h1>TodoHeader</h1>
  37. <input type="text" value={todo.get('text')} onChange={onChangeText} />
  38. <button onClick={onCreateTodo}>送出</button>
  39. </div>
  40. );
  41. export default TodoHeader;

以下是 src/components/TodoList/TodoList.js 的部份:

  1. import React from 'react';
  2. import ReactDOM from 'react-dom';
  3. import { connect } from 'react-redux';
  4. import TodoList from '../../components/TodoList';
  5. import {
  6. deleteTodo,
  7. } from '../../actions';
  8. const mapStateToProps = (state) => ({
  9. todos: state.getIn(['todo', 'todos'])
  10. });
  11. // 由 Component 傳進欲刪除元素的 index
  12. const mapDispatchToProps = (dispatch) => ({
  13. onDeleteTodo: (index) => () => (
  14. dispatch(deleteTodo({ index }))
  15. )
  16. });
  17. export default connect(
  18. mapStateToProps,
  19. mapDispatchToProps,
  20. )(TodoList);
  21. // Component 部分值的注意的是 todos state 是透過 map function 去迭代出元素,由於要讓 React JSX 可以渲染並保持傳入觸發 event state 的 immutable,所以需使用 toJS() 轉換 component of array。
  22. const TodoList = ({
  23. todos,
  24. onDeleteTodo,
  25. }) => (
  26. <div>
  27. <ul>
  28. {
  29. todos.map((todo, index) => (
  30. <li key={index}>
  31. {todo.get('text')}
  32. <button onClick={onDeleteTodo(index)}>X</button>
  33. </li>
  34. )).toJS()
  35. }
  36. </ul>
  37. </div>
  38. );
  39. export default TodoList;

若是一切順利的話就可以在瀏覽器上看到自己努力的成果囉!(因為我們有使用 redux-logger 所以打開 console 會看到 action 和 state 的變化情形,但記得在 production 環境要拿掉)

React Redux

總結

以上就是 Redux 實戰入門,對於第一次自己動手寫 Redux 的朋友可能會需要多練習幾次,多體會整個架構。在接下來的章節我們將優化我們的 React Redux TodoApp,讓它可以有更清晰好維護的架構。

延伸閱讀

  1. Redux 官方網站

(image via JonasOhlssonlicdn

| 回首頁 | 上一章:Redux 基礎概念 | 下一章:Container 與 Presentational Components 入門 |

| 勘誤、提問或許願 |