ReactJS - Redux



React Redux 是一個用於 React 的高階狀態管理庫。正如我們之前學到的,React 只支援元件級別狀態管理。在一個大型複雜的應用程式中,使用了大量的元件。React 建議將狀態移到頂級元件,並使用屬性將狀態傳遞給巢狀元件。這在某種程度上有所幫助,但當元件數量增加時,它會變得複雜。

React Redux 介入並幫助在應用程式級別維護狀態。React Redux 允許任何元件隨時訪問狀態。此外,它還允許任何元件隨時更改應用程式的狀態。

讓我們學習如何在本章中使用 React Redux 來編寫 React 應用程式。

概念

React Redux 將應用程式的狀態儲存在一個名為 Redux store 的單一位置。React 元件可以從 store 獲取最新狀態,也可以隨時更改狀態。Redux 提供了一個簡單的流程來獲取和設定應用程式的當前狀態,並涉及以下概念。

Store - 儲存應用程式狀態的中心位置。

Actions - Action 是一個簡單的物件,包含要執行的動作型別和執行動作所需的輸入(稱為 payload)。例如,在 store 中新增專案的 action 包含型別為 ADD_ITEM 和一個包含專案詳細資訊的物件作為 payload。Action 可以表示為:

{ 
   type: 'ADD_ITEM', 
   payload: { name: '..', ... }
}

Reducers - Reducers 是純函式,用於根據現有狀態和當前 action 建立新狀態。它返回新建立的狀態。例如,在新增專案的情況下,它建立一個新的專案列表,並將狀態中的專案和新專案合併,然後返回新建立的列表。

Action creators - Action creator 建立具有適當動作型別和動作所需資料的 action 並返回該 action。例如,addItem action creator 返回以下物件:

{ 
   type: 'ADD_ITEM', 
   payload: { name: '..', ... }
}

Component - 元件可以連線到 store 以獲取當前狀態,並向 store 分派 action,以便 store 執行 action 並更新其當前狀態。

典型 Redux store 的工作流程如下圖所示。

Redux Store
  • React 元件訂閱 store,並在應用程式初始化期間獲取最新狀態。
  • 要更改狀態,React 元件建立必要的 action 並分派該 action。
  • Reducer 根據 action 建立新狀態並返回它。Store 使用新狀態更新自身。
  • 狀態更改後,store 將更新後的狀態傳送到所有已訂閱的元件。

Redux API

Redux 提供單個 API,connect,它將元件連線到 store,並允許元件獲取和設定 store 的狀態。

connect API 的簽名是:

function connect(mapStateToProps?, mapDispatchToProps?, mergeProps?, options?)

所有引數都是可選的,它返回一個 HOC(高階元件)。高階元件是一個包裝元件並返回新元件的函式。

let hoc = connect(mapStateToProps, mapDispatchToProps) 
let connectedComponent = hoc(component)

讓我們看看前兩個引數,對於大多數情況來說,它們就足夠了。

  • mapStateToProps - 接受具有以下簽名的函式。

(state, ownProps?) => Object

這裡,state 指的是 store 的當前狀態,Object 指的是元件的新 props。每當 store 的狀態更新時,它都會被呼叫。

(state) => { prop1: this.state.anyvalue }
  • mapDispatchToProps - 接受具有以下簽名的函式。

Object | (dispatch, ownProps?) => Object

這裡,dispatch 指的是用於在 redux store 中分派 action 的 dispatch 物件,Object 指的是元件的一個或多個 dispatch 函式作為 props。

(dispatch) => {
   addDispatcher: (dispatch) => dispatch({ type: 'ADD_ITEM', payload: { } }),
   removeispatcher: (dispatch) => dispatch({ type: 'REMOVE_ITEM', payload: { } }),
}

Provider 元件

React Redux 提供了一個 Provider 元件,其唯一目的是使 Redux store 可用於所有使用 connect API 連線到 store 的巢狀元件。示例程式碼如下:

import React from 'react'
import ReactDOM from 'react-dom'
import { Provider } from 'react-redux'
import { App } from './App'
import createStore from './createReduxStore'

const store = createStore()

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

現在,App 元件內的所有元件都可以使用 connect API 訪問 Redux store。

工作示例

讓我們重新建立我們的 expense manager 應用程式,並使用 React Redux 概念來維護應用程式的狀態。

首先,使用 Create React App 或 Rollup bundler 建立一個新的 React 應用程式,react-message-app,方法是按照“建立 React 應用程式”一章中的說明進行操作。

接下來,安裝 Redux 和 React Redux 庫。

npm install redux react-redux --save

接下來,安裝 uuid 庫以生成新費用的唯一識別符號。

npm install uuid --save

接下來,在您最喜歡的編輯器中開啟應用程式。

接下來,在應用程式的根目錄下建立 src 資料夾。

接下來,在 src 資料夾下建立 actions 資料夾。

接下來,在 src/actions 資料夾下建立一個檔案,types.js,並開始編輯。

接下來,新增兩種 action 型別,一種用於新增支出,另一種用於刪除支出。

export const ADD_EXPENSE = 'ADD_EXPENSE'; 
export const DELETE_EXPENSE = 'DELETE_EXPENSE';

接下來,在 src/actions 資料夾下建立一個檔案,index.js,以新增 action 並開始編輯。

接下來,匯入 uuid 來建立唯一識別符號。

import { v4 as uuidv4 } from 'uuid';

接下來,匯入 action 型別。

import { ADD_EXPENSE, DELETE_EXPENSE } from './types';

接下來,新增一個新函式以返回用於新增支出的 action 型別並將其匯出。

export const addExpense = ({ name, amount, spendDate, category }) => ({
   type: ADD_EXPENSE,
   payload: {
      id: uuidv4(),
      name,
      amount,
      spendDate,
      category
   }
});

這裡,該函式期望 expense 物件並返回型別為 ADD_EXPENSE 的 action 型別以及 expense 資訊的 payload。

接下來,新增一個新函式以返回用於刪除支出的 action 型別並將其匯出。

export const deleteExpense = id => ({
   type: DELETE_EXPENSE,
   payload: {
      id
   }
});

這裡,該函式期望要刪除的支出專案的 id,並返回型別為 'DELETE_EXPENSE' 的 action 型別以及支出 id 的 payload。

action 的完整原始碼如下:

import { v4 as uuidv4 } from 'uuid';
import { ADD_EXPENSE, DELETE_EXPENSE } from './types';

export const addExpense = ({ name, amount, spendDate, category }) => ({
   type: ADD_EXPENSE,
   payload: {
      id: uuidv4(),
      name,
      amount,
      spendDate,
      category
   }
});
export const deleteExpense = id => ({
   type: DELETE_EXPENSE,
   payload: {
      id
   }
});

接下來,在 src 資料夾下建立一個名為 reducers 的新資料夾。

接下來,在 src/reducers 資料夾下建立一個檔案,index.js,以編寫 reducer 函式並開始編輯。

接下來,匯入 action 型別。

import { ADD_EXPENSE, DELETE_EXPENSE } from '../actions/types';

接下來,新增一個函式,expensesReducer,以執行在 redux store 中新增和更新支出的實際功能。

export default function expensesReducer(state = [], action) {
   switch (action.type) {
      case ADD_EXPENSE:
         return [...state, action.payload];
      case DELETE_EXPENSE:
         return state.filter(expense => expense.id !== action.payload.id);
      default:
         return state;
   }
}

reducer 的完整原始碼如下:

import { ADD_EXPENSE, DELETE_EXPENSE } from '../actions/types';

export default function expensesReducer(state = [], action) {
   switch (action.type) {
      case ADD_EXPENSE:
         return [...state, action.payload];
      case DELETE_EXPENSE:
         return state.filter(expense => expense.id !== action.payload.id);
      default:
         return state;
   }
}

這裡,reducer 檢查 action 型別並執行相關程式碼。

接下來,在 src 資料夾下建立 components 資料夾。

接下來,在 src/components 資料夾下建立一個檔案,ExpenseEntryItemList.css,併為 html 表格新增通用樣式。

html {
   font-family: sans-serif;
}
table {
   border-collapse: collapse;
   border: 2px solid rgb(200,200,200);
   letter-spacing: 1px;
   font-size: 0.8rem;
}
td, th {
   border: 1px solid rgb(190,190,190);
   padding: 10px 20px;
}
th {
   background-color: rgb(235,235,235);
}
td, th {
   text-align: left;
}
tr:nth-child(even) td {
   background-color: rgb(250,250,250);
}
tr:nth-child(odd) td {
   background-color: rgb(245,245,245);
}
caption {
   padding: 10px;
}
tr.highlight td { 
   background-color: #a6a8bd;
}

接下來,在 src/components 資料夾下建立一個檔案,ExpenseEntryItemList.js,並開始編輯。

接下來,匯入 React 和 React Redux 庫。

import React from 'react'; 
import { connect } from 'react-redux';

接下來,匯入 ExpenseEntryItemList.css 檔案。

import './ExpenseEntryItemList.css';

接下來,匯入 action creators。

import { deleteExpense } from '../actions'; 
import { addExpense } from '../actions';

接下來,建立一個類,ExpenseEntryItemList,並使用 props 呼叫建構函式。

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

接下來,建立 mapStateToProps 函式。

const mapStateToProps = state => {
   return {
      expenses: state
   };
};

在這裡,我們將輸入狀態複製到元件的 expenses props 中。

接下來,建立 mapDispatchToProps 函式。

const mapDispatchToProps = dispatch => {
   return {
      onAddExpense: expense => {
         dispatch(addExpense(expense));
      },
      onDelete: id => {
         dispatch(deleteExpense(id));
      }
   };
};

在這裡,我們建立了兩個函式,一個用於分派 add expense (addExpense) 函式,另一個用於分派 delete expense (deleteExpense) 函式,並將這些函式對映到元件的 props 中。

接下來,使用 connect api 匯出元件。

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

現在,元件獲得三個新的屬性,如下所示:

  • expenses - 支出列表

  • onAddExpense - 用於分派 addExpense 函式的函式

  • onDelete - 用於分派 deleteExpense 函式的函式

接下來,使用 onAddExpense 屬性在建構函式中向 redux store 新增一些支出。

if (this.props.expenses.length == 0)
{
   const items = [
      { id: 1, name: "Pizza", amount: 80, spendDate: "2020-10-10", category: "Food" },
      { id: 2, name: "Grape Juice", amount: 30, spendDate: "2020-10-12", category: "Food" },
      { id: 3, name: "Cinema", amount: 210, spendDate: "2020-10-16", category: "Entertainment" },
      { id: 4, name: "Java Programming book", amount: 242, spendDate: "2020-10-15", category: "Academic" },
      { id: 5, name: "Mango Juice", amount: 35, spendDate: "2020-10-16", category: "Food" },
      { id: 6, name: "Dress", amount: 2000, spendDate: "2020-10-25", category: "Cloth" },
      { id: 7, name: "Tour", amount: 2555, spendDate: "2020-10-29", category: "Entertainment" },
      { id: 8, name: "Meals", amount: 300, spendDate: "2020-10-30", category: "Food" },
      { id: 9, name: "Mobile", amount: 3500, spendDate: "2020-11-02", category: "Gadgets" },
      { id: 10, name: "Exam Fees", amount: 1245, spendDate: "2020-11-04", category: "Academic" }
   ]
   items.forEach((item) => {
      this.props.onAddExpense(
         { 
            name: item.name, 
            amount: item.amount, 
            spendDate: item.spendDate, 
            category: item.category 
         }
      );
   })
}

接下來,新增一個事件處理程式以使用支出 id 刪除支出專案。

handleDelete = (id,e) => {
   e.preventDefault();
   this.props.onDelete(id);
}

在這裡,事件處理程式呼叫 onDelete 分派器,它與支出 id 一起呼叫 deleteExpense

接下來,新增一個方法來計算所有支出的總金額。

getTotal() {
   let total = 0;
   for (var i = 0; i < this.props.expenses.length; i++) {
      total += this.props.expenses[i].amount
   }
   return total;
}

接下來,新增 render() 方法,並以表格格式列出支出專案。

render() {
   const lists = this.props.expenses.map(
      (item) =>
      <tr key={item.id}>
         <td>{item.name}</td>
         <td>{item.amount}</td>
         <td>{new Date(item.spendDate).toDateString()}</td>
         <td>{item.category}</td>
         <td><a href="#"
            onClick={(e) => this.handleDelete(item.id, e)}>Remove</a></td>
      </tr>
   );
   return (
      <div>
         <table>
            <thead>
               <tr>
                  <th>Item</th>
                  <th>Amount</th>
                  <th>Date</th>
                  <th>Category</th>
                  <th>Remove</th>
               </tr>
            </thead>
            <tbody>
               {lists}
               <tr>
                  <td colSpan="1" style={{ textAlign: "right" }}>Total Amount</td>
                  <td colSpan="4" style={{ textAlign: "left" }}>
                     {this.getTotal()}
                  </td>
               </tr>
            </tbody>
         </table>
      </div>
   );
}

這裡,我們將事件處理程式handleDelete設定為從儲存中移除費用。

下面給出了ExpenseEntryItemList元件的完整原始碼:

import React from 'react';
import { connect } from 'react-redux';
import './ExpenseEntryItemList.css';
import { deleteExpense } from '../actions';
import { addExpense } from '../actions';

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

      if (this.props.expenses.length == 0){
         const items = [
            { id: 1, name: "Pizza", amount: 80, spendDate: "2020-10-10", category: "Food" },
            { id: 2, name: "Grape Juice", amount: 30, spendDate: "2020-10-12", category: "Food" },
            { id: 3, name: "Cinema", amount: 210, spendDate: "2020-10-16", category: "Entertainment" },
            { id: 4, name: "Java Programming book", amount: 242, spendDate: "2020-10-15", category: "Academic" },
            { id: 5, name: "Mango Juice", amount: 35, spendDate: "2020-10-16", category: "Food" },
            { id: 6, name: "Dress", amount: 2000, spendDate: "2020-10-25", category: "Cloth" },
            { id: 7, name: "Tour", amount: 2555, spendDate: "2020-10-29", category: "Entertainment" },
            { id: 8, name: "Meals", amount: 300, spendDate: "2020-10-30", category: "Food" },
            { id: 9, name: "Mobile", amount: 3500, spendDate: "2020-11-02", category: "Gadgets" },
            { id: 10, name: "Exam Fees", amount: 1245, spendDate: "2020-11-04", category: "Academic" }
         ]
         items.forEach((item) => {
            this.props.onAddExpense(
               { 
                  name: item.name, 
                  amount: item.amount, 
                  spendDate: item.spendDate, 
                  category: item.category 
               }
            );
         })
      }
   }
   handleDelete = (id,e) => {
      e.preventDefault();
      this.props.onDelete(id);
   }
   getTotal() {
      let total = 0;
      for (var i = 0; i < this.props.expenses.length; i++) {
         total += this.props.expenses[i].amount
      }
      return total;
   }
   render() {
      const lists = this.props.expenses.map((item) =>
         <tr key={item.id}>
            <td>{item.name}</td>
            <td>{item.amount}</td>
            <td>{new Date(item.spendDate).toDateString()}</td>
            <td>{item.category}</td>
            <td><a href="#"
               onClick={(e) => this.handleDelete(item.id, e)}>Remove</a></td>
         </tr>
      );
      return (
         <div>
            <table>
               <thead>
                  <tr>
                     <th>Item</th>
                     <th>Amount</th>
                     <th>Date</th>
                     <th>Category</th>
                     <th>Remove</th>
                  </tr>
               </thead>
               <tbody>
                  {lists}
                  <tr>
                     <td colSpan="1" style={{ textAlign: "right" }}>Total Amount</td>
                     <td colSpan="4" style={{ textAlign: "left" }}>
                        {this.getTotal()}
                     </td>
                  </tr>
               </tbody>
            </table>
         </div>
      );
   }
}
const mapStateToProps = state => {
   return {
      expenses: state
   };
};
const mapDispatchToProps = dispatch => {
   return {
      onAddExpense: expense => {
         dispatch(addExpense(expense));
      },
      onDelete: id => {
         dispatch(deleteExpense(id));
      }
   };
};
export default connect(
   mapStateToProps,
   mapDispatchToProps
)(ExpenseEntryItemList);

接下來,在src/components資料夾下建立一個檔案App.js,並使用ExpenseEntryItemList元件。

import React, { Component } from 'react';
import ExpenseEntryItemList from './ExpenseEntryItemList';

class App extends Component {
   render() {
      return (
         <div>
            <ExpenseEntryItemList />
         </div>
      );
   }
}
export default App;

接下來,在src資料夾下建立一個檔案index.js

import React from 'react';
import ReactDOM from 'react-dom';
import { createStore } from 'redux';
import { Provider } from 'react-redux';
import rootReducer from './reducers';
import App from './components/App';

const store = createStore(rootReducer);

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

這裡,

  • 使用createStore建立一個儲存,並附加我們的reducer。

  • 使用React redux庫中的Provider元件並將store設定為props,這使得所有巢狀元件都可以使用connect api連線到store。

最後,在根資料夾下建立一個public資料夾,並在其中建立一個index.html檔案。

<!DOCTYPE html>
<html lang="en">
   <head>
      <meta charset="utf-8">
      <title>React Containment App</title>
   </head>
   <body>
      <div id="root"></div>
      <script type="text/JavaScript" src="./index.js"></script>
   </body>
</html>

接下來,使用npm命令啟動應用程式。

npm start

接下來,開啟瀏覽器並在位址列中輸入https://:3000,然後按回車鍵。

點選移除連結將從redux儲存中移除該專案。

Redux
廣告