ReactJS - 冗余



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

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

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

概念

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

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

动作 - 动作是一个普通对象,具有要执行的操作类型和执行操作所需的输入(称为有效载荷)。例如,用于在商店中添加商品的操作包含 ADD_ITEM 类型,并包含商品详细信息作为有效负载的对象。动作可以表示为 -


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

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

动作创建者 - 动作创建者使用正确的动作类型和动作所需的数据创建一个动作,并返回动作。例如,addItem 动作创建器返回以下对象 -


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

组件 - 组件可以连接到存储以获取当前状态,并将操作分派到存储,以便存储执行操作并更新其当前状态。

典型的 redux 存储的工作流程可以表示为如下所示。

Redux Store

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

Redux 接口

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

connect API的签名是:


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

所有参数都是可选的,它返回一个 HOC(高阶分量)。高阶分量是一个函数,它包装了一个组件并返回一个新组件。


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

让我们看看前两个参数,这对于大多数情况来说已经足够了。

  • mapStateToProps − 接受具有以下签名的函数。

 (state, ownProps?) => Object

在这里,state 指的是存储的当前状态,Object 指的是组件的新属性。每当更新存储的状态时,都会调用它。


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

 Object | (dispatch, ownProps?) => Object

这里,dispatch 指的是用于在 redux 存储中调度动作的调度对象,Object 指的是一个或多个调度函数作为组件的 props。


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

提供程序组件

React Redux 提供了一个 Provider 组件,其唯一目的是使 Redux 存储可用于使用 connect 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 组件内的所有组件都可以使用 connect API 访问 Redux 商店。

工作示例

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

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

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

npm install redux react-redux --save

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

npm install uuid --save

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

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

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

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

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


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

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

接下来,导入 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。

接下来,创建一个文件,index.js src/reducers 下编写 reducer 函数并开始编辑。

接下来,导入操作类型。


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

接下来,添加一个函数 expensesReducer 来做 redux store 中添加和更新 expenses 的实际功能。


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 文件夹下创建组件文件夹。

接下来,创建一个文件,ExpenseEntryItemList.css src/components 文件夹下,并为 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;
}

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

接下来,导入 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
	 	};
};

在这里,我们将输入状态复制到组件的 expenses props 中。

接下来,创建 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);

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

  • expenses − expenses 清单
  • 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);

接下来,创建一个文件,App.js src/components 文件夹下,并使用 ExpenseEntryItemList 组件。


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

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

接下来,创建一个文件,index.js src 文件夹下。


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。

最后,在根文件夹下创建一个公共文件夹,并创建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 商店中删除该项目。

Redux (英语)