iT邦幫忙

2019 iT 邦幫忙鐵人賽

DAY 14
3
Modern Web

React 30天系列 第 14

Day 14-和Redux合作重寫todos吧!

前情提要:昨天介紹完了的三個方法,我們知道如何透過getState取得state;知道如何透過dispatch執行action更新state;知道怎麼透過subscribe來監聽state。

Redux本身就和框架無關。我們可以使用在原生javascript、Angular或React。
有些綁定可以將Redux和我們所使用的framework或library綁在一起。

對React來說這個角色就是react-redux

關於react-redux

react-redux組成要點有兩個,分別為:

  1. Provider- react component,Provider讓整個react applicaiton都能取得Redux store的資料
  2. connect- function,封裝了和store交流的過程,connect可以:
    • 透過connect將來自store的state作為props在component中使用
    • 透過connect發送action到state替換state

而connect這個function有兩個參數,這兩個參數都是非必需的,分別為:

  1. mapStateToProps: 取得state資料,當store的state有更動時每次都會被調用,mapStateToProps會收到整個store的state,必須返回一個object作為props給component使用,而object的內容呢則為我們需要用到的state。
  2. mapDispatchToProps: 這個參數可能是function或object。
    • 若為function: 只在component建立時調用,會收到dispatch作為參數,然後我們需要return一個帶有function的object,使用dispatch來發送action。
    • 若為object: value帶有action creators,每個action creators都會轉變成props function,並在調用時自動發送action。
      註記::react-redux推薦使用object簡寫語法形式。

改寫todos

好吧,相信大家都還不太清楚到底發生了什麼事。
接下來我們透過改寫todos帶大家使用react-redux吧

繼續之前,我先把todos拉一個branch出來安裝react-redux和redux

yarn add redux react-redux

然後把專案目錄下的index.js的App component獨立成一個檔案放在src/components下(其實早該獨立了...),並且在專案目錄下的index.js匯入app.js,它就會變得很清爽,如下:

import React from 'react';
import ReactDOM from 'react-dom';

import App from './src/components/app';
import './src/styles/index.scss';

ReactDOM.render(<App/>, document.getElementById('app'));

前置作業完成後,接著開始進入改寫囉~

index.js

  1. 把Provider從react-redux匯入
  2. 匯入store
import React from "react";
import ReactDOM from "react-dom";

import { Provider } from "react-redux"; // 1.
import store from "./src/store";  // 2.

import App from "./src/components/app";
import "./src/styles/index.scss";

ReactDOM.render(
  <Provider store={store}>  { /* 將store作為props傳遞給其他component */ }
    <App />
  </Provider>,
  document.getElementById("app")
);

src/store/index.js

store這邊處理相對單純,生成store丟去給專案目錄下的index.js用就好了

import { createStore } from "redux";
import rootReducer from "../reducers";  // 後面沒指定檔案名,預設直接抓index.js

const store = createStore(rootReducer);

export default store;

在這個地方停下腳步稍微想一下,todos的內容到底是什麼?!
對了,我們從頭到尾操控的都是todoList array,所以我們預設todoList的初始值是個空array。

src/reducers/index.js

確定state的內容後,我們打開src/reducers/index.js,回想昨天內容:

當應用程序(application)越來越大時,reducer也會越來越胖,我們可以藉由把reducer拆分成多個function,然後透過combineReducers把它們結合。

做到這裡,覺得狀態管理就是一門分門別類的藝術,module的功能就是讓我每個動作拆分越細越好,所以我們來使用combineReducers吧!

  1. 從redux匯入combineReducers
  2. 預設combineReducers需要管理todoList
  3. 匯入todoListReducer
import { combineReducers } from 'redux';  // 1.
import todoListReducer from './todo_list_reducer';  // 3.

const rootReducer = combineReducers({
  todoList: todoListReducer   // 2.
});

export default rootReducer;

src/reducers/todo_list_reducer.js

接著處理todoListReducer

  1. export的function參數,state等於todoList的資料,預設為空array
  2. 在todoList,使用者會有三種可能行為,分別為:
    • ADD_NEW_TODO: 新增待辦事項
    • COMPLETE_TODO: 標記已完成
    • REMOVE_TODO: 移除待辦事項
      而這三個action type即之前App component內的method,現在統一透過reducer替換原本的state。
  3. 匯入action_type
    兩天前提到,reducer用type來決定怎麼計算下一個state,為了避免手殘眼花而打錯,我們先把type宣告成常數(constant)。
import { ADD_NEW_TODO, COMPLETE_TODO, REMOVE_TODO } from '../constants/action_type';
import update from 'immutability-helper';

export default function(state = [], action) {
  switch (action.type) {
    case ADD_NEW_TODO:
      return [ ...state, action.newItem ];
    case COMPLETE_TODO:
      return update(state, {
        [action.index]: {$set: action.item}
      });
    case REMOVE_TODO:
      return update(state, {
        $splice: [[action.index, 1]]
      });
    default:
      return state;
  }
};

src/constants/action_type.js

action_type的任務非常簡單,就是提供reducer和action共用的常數設定

export const ADD_NEW_TODO = "ADD_NEW_TODO";
export const COMPLETE_TODO = "COMPLETE_TODO";
export const REMOVE_TODO = "REMOVE_TODO";

src/actions/index.js

再來處理action creators,這裡使用camelCase定義action的method,並回傳一個obejct,內含type和我們自訂的data

import uuid from 'uuid/v4';
import { ADD_NEW_TODO, COMPLETE_TODO, REMOVE_TODO } from '../constants/action_type';

export const addNewTodo = text => {
  return {
    type: ADD_NEW_TODO,
    newItem: {
      text,
      key: uuid(),
      completed: false
    }
  }
}

export const completeTodo = (index, item) => {
  return {
    type: COMPLETE_TODO,
    index, item
  }
}

export const removeTodo = (index) => {
  return {
    type: REMOVE_TODO,
    index
  };
}

src/components/new_todo.js

Redux的資料都處理得差不多了,接下來調整我們的new_todo.js吧!
這邊改動的幅度相對小,主要只是把action方法匯入,並透過connect dispatch(發送) action。
因為在這裡我們不需要store的state資料,所以第一個參數給它null就可以了。

值得一提的是第二個參數mapDispatchToProps。稍早提到mapDispatchToProps可以為object,其實它的做法就是把第二個參數作為包著action的object丟出去就可以變成props給component調用,在這邊我們需要調用的action就是addNewTodo,所以把它匯出就可以了~

備註:這裡保留component內部的state設定,因為其他component並不需要輸入過程中的value,不需要放到store共用。

import React, { PureComponent } from "react";
import { connect } from "react-redux";
import { addNewTodo } from "../actions";

class NewTodo extends PureComponent {
  // .....
}

export default connect(null, { addNewTodo })(NewTodo);

src/components/todo_list.js

之前我們將ul element放在app component內,這次想調整一下todo_list的架構,將todo_list改為一整個ul element,而li的部分另外再拆一個todo_list_item渲染array內容,這樣做的原因是我想要保持component乾淨

在這裡我們透過mapStateToProps取得todoList資料;透過mapDispatchToProps取得action creators,並作為props傳給TodoListItem使用

import React from 'react';
import { connect } from 'react-redux';
import { completeTodo, removeTodo } from '../actions/';

import TodoListItem from './todo_list_item';

const TodoList = ({todo, completeTodo, removeTodo}) => {
  return todo.length ? (
    <ul className="list">
      <TodoListItem
        todo={todo}
        completeTodo={completeTodo}
        removeTodo={removeTodo}
      />
    </ul>
  ): null
};

const mapStateToProps = ({todoList}) => ({
  todo: todoList
})

const mapDispatchToProps = {
  completeTodo, removeTodo
}

export default connect(mapStateToProps, mapDispatchToProps)(TodoList);

src/components/todo_list_item.js

這邊的TodoListItem所有的資料都是從TodoList透過props送過來。

import React from 'react';

const TodoListItem = ({todo, completeTodo, removeTodo}) => (
  todo.map((item, index) => (
    <li key={item.key} className="list-item">
      <input
        type="checkbox"
        id={item.key}
        className="list-item-complete"
        onChange={() => completeTodo(index, { ...item, completed: !item.completed})}
      />
      <label htmlFor={item.key}>{item.text}</label>
      <button className="btn" onClick={() => removeTodo(index)}>移除</button>
    </li>
  ))
);

export default TodoListItem;

src/components/app.js

最後,我們可以把App component瘦身了。
儲存todoList的array可移除;新增、標記和刪除todoList的方法可移除。
接著把沒有method和local state的class component轉成function component。
調整後的程式碼如下:

import React from 'react';

import NewTodo from './new_todo';
import TodoList from './todo_list';

const App = () => {
  return (
    <div className="main">
      <h1 className="title">todos</h1>
      <NewTodo/>
      <TodoList/>
    </div>
  );
}

export default App;

github傳送門


今日總結:
今天結合redux改寫todos,不得不說即使是個渣渣todos,改寫起來也是蠻討厭的。
就像舊家搬新家一樣,房間格局不一樣又要再整理一次。如果有預期application未來會成長茁壯,一開始就使用redux會比事後調整開心許多!


上一篇
Day 13-Redux-Store的三個方法
下一篇
Day 15-[番外]關於HOC
系列文
React 30天30
圖片
  直播研討會
圖片
{{ item.channelVendor }} {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言