ReactJS – Redux

React redux 是 React 的高级状态管理库。正如我们之前所了解的,React 仅支持组件级别的状态管理。在一个大而复杂的应用程序中,使用了大量的组件。React 建议将状态移动到顶级组件,并使用属性将状态传递给嵌套组件。它在一定程度上有所帮助,但是当组件增加时它变得复杂。

React redux 参与并帮助维护应用程序级别的状态。React redux 允许任何组件随时访问状态。此外,它允许任何组件随时更改应用程序的状态。

让我们在本章中学习如何使用 React redux 编写 React 应用程序。

概念

React redux 将应用程序的状态维护在一个称为 Redux 存储的地方。React 组件可以从 store 中获取最新状态,也可以随时更改状态。Redux 提供了一个简单的过程来获取和设置应​​用程序的当前状态,并涉及以下概念。

Store – 存储应用程序状态的中心位置。

Actions – Action 是一个普通对象,具有要完成的操作类型和执行操作所需的输入(称为有效负载)。例如,在商店中添加项目的操作包含ADD_ITEM作为类型和一个以项目详细信息作为有效负载的对象。该动作可以表示为 –

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

Reducers – Reducers 是纯函数,用于根据现有状态和当前操作创建新状态。它返回新创建的状态。例如,在添加项目场景中,它创建一个新的项目列表,并将状态和新项目中的项目合并并返回新创建的列表。

动作创建者动作创建者创建具有适当动作类型和动作所需数据的动作并返回动作。例如, addItem动作创建者返回下面的对象 –

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

组件– 组件可以连接到商店以获取当前状态并将操作发送到商店,以便商店执行操作并更新其当前状态。

一个典型的 redux store 的工作流程可以表示如下。

  • React 组件订阅 store 并在应用程序初始化期间获取最新状态。
  • 为了改变状态,React 组件创建必要的动作并分发动作。
  • Reducer 根据动作创建一个新的状态并返回它。存储使用新状态更新自身。
  • 一旦状态发生变化,store 会将更新后的状态发送给其所有订阅的组件。

Redux API

Redux 提供了一个单独的 api,connect,它将一个组件连接到 store,并允许组件获取和设置 store 的状态。

连接 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。每当更新商店的状态时都会调用它。

(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: { } }),
}

提供者组件

React Redux 提供了一个 Provider 组件,其唯一目的是让 Redux 存储可供所有使用连接 API 连接到存储的嵌套组件使用。示例代码如下 –

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 组件内的所有组件都可以通过连接 API 访问 Redux 商店。

工作示例

让我们重新创建我们的费用管理器应用程序并使用 React redux 概念来维护应用程序的状态。

首先,按照创建 React 应用程序章节中的说明,使用 Create React AppRollup bundler 创建一个新的 react 应用程序react-message-app 。

接下来,安装 Redux 和 React redux 库。

npm install redux react-redux --save

接下来,安装 uuid 库以生成新费用的唯一标识符。

npm install uuid --save

接下来,在您喜欢的编辑器中打开应用程序。

接下来,在应用程序的根目录下创建src文件夹。

接下来,在src文件夹下创建actions文件夹。

接下来,在src/actions文件夹下创建一个文件types.js并开始编辑。

接下来,添加两种操作类型,一种用于添加费用,一种用于删除费用。

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

接下来,在src/actions文件夹下创建一个文件index.js以添加操作并开始编辑。

接下来,导入uuid以创建唯一标识符。

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
   }
});

在这里,该函数需要费用对象和ADD_EXPENSE的返回操作类型以及费用信息的有效负载。

接下来,添加一个新函数以返回用于删除费用的操作类型并将其导出。

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

在这里,该函数期望删除费用项目的 id 并返回“DELETE_EXPENSE”的操作类型以及费用 id 的有效负载。

该操作的完整源代码如下 –

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 函数并开始编辑。

接下来,导入动作类型。

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

接下来,添加一个函数,exploresReducer来完成在 redux 存储中添加和更新费用的实际功能。

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;
   }
}

减速器的完整源代码如下 –

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 检查动作类型并执行相关代码。

接下来,在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';

接下来,导入动作创建者。

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
   };
};

在这里,我们将输入状态复制到组件的费用属性中。

接下来,创建mapDispatchToProps函数。

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

在这里,我们创建了两个函数,一个调度添加费用(addExpense)函数,另一个调度删除费用(deleteExpense)函数,并将这些函数映射到组件的props。

接下来,使用connect api 导出组件。

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

现在,该组件获得了下面给出的三个新属性 –

  • 费用 – 费用清单
  • onAddExpense – 调度addExpense函数的函数
  • onDelete – 调度deleteExpense函数的函数

接下来,使用onAddExpense属性在构造函数的 redux 存储中添加一些费用。

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调度程序,该调度程序调用deleteExpense以及费用 ID。

接下来,添加一个方法来计算所有费用的总额。

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')
);

这里,

  • 通过附加我们的 reducer使用createStore创建一个商店。
  • 使用 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

接下来,打开浏览器,在地址栏输入http://localhost:3000,回车。

单击删除链接将从 redux 商店中删除该项目。