ReactJS - Redux
React redux 是 React 的高级状态管理库。正如我们之前所了解的,React 仅支持组件级状态管理。在大型复杂的应用程序中,会使用大量组件。React 建议将状态移动到顶级组件,并使用属性将状态传递给嵌套组件。这在一定程度上有帮助,但当组件增加时会变得复杂。
React redux 介入并帮助在应用程序级别维护状态。React redux 允许任何组件随时访问状态。此外,它还允许任何组件随时更改应用程序的状态。
让我们在本章中学习如何使用 React redux 编写 React 应用程序。
概念
React redux 将应用程序的状态维护在一个名为 Redux 存储的地方。React 组件可以从存储中获取最新状态,也可以随时更改状态。 Redux 提供了一个简单的过程来获取和设置应用程序的当前状态,并涉及以下概念。
Store − 存储应用程序状态的中心位置。
Actions − Action 是一个普通对象,具有要执行的操作的类型和执行操作所需的输入(称为有效负载)。例如,在商店中添加商品的操作包含 ADD_ITEM 作为类型,以及包含商品详细信息的对象作为有效负载。该操作可以表示为 −
{
type: 'ADD_ITEM',
payload: { name: '..', ... }
}
Reducers − Reducers 是纯函数,用于根据现有状态和当前操作创建新状态。它返回新创建的状态。例如,在添加项目场景中,它会创建一个新的项目列表,并将状态和新项目中的项目合并,然后返回新创建的列表。
Action creators(动作创建者) − 动作创建者 创建一个具有适当动作类型和动作所需数据的动作并返回该动作。例如,addItem 动作创建者返回以下对象 −
{
type: 'ADD_ITEM',
payload: { name: '..', ... }
}
Component − 组件可以连接到 store 以获取当前状态并将操作分派到 store,以便 store 执行操作并更新其当前状态。
典型的 redux store 的工作流程可以表示如下。
- React 组件在应用程序初始化期间订阅 store 并获取最新状态。
- 要更改状态,React 组件会创建必要的操作并分派该操作。
- Reducer 根据操作创建一个新状态并返回它。 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 指的是商店的当前状态,Object 指的是组件的新道具。每当 store 的状态更新时,它都会被调用。
(state) => { prop1: this.state.anyvalue }
mapDispatchToProps − 接受具有以下签名的函数。
Object | (dispatch, ownProps?) => Object
此处,dispatch 指的是用于在 redux store 中调度操作的调度对象,而 Object 指的是作为组件 props 的一个或多个调度函数。
(dispatch) => {
addDispatcher: (dispatch) => dispatch({ type: 'ADD_ITEM', payload: { } }),
removeispatcher: (dispatch) => dispatch({ type: 'REMOVE_ITEM', payload: { } }),
}
Provider 组件
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 存储。
工作示例
让我们重新创建Expense Manager(费用管理器)应用程序,并使用 React redux 概念来维护应用程序的状态。
首先,按照创建 React 应用程序章节中的说明,使用 Create React App 或 Rollup 捆绑器创建一个新的 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';
接下来,添加一个函数,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 检查操作类型并执行相关代码。
接下来,在 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
};
};
在这里,我们将输入状态复制到组件的 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 − 费用清单
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 调度程序,该调度程序连同费用 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')
);
这里,
通过附加我们的 Reducer,使用 createStore 创建存储。
使用 React redux 库中的 Provider 组件并将存储设置为 props,这使得所有嵌套组件能够使用 connect api connect 连接到存储。
最后,在根文件夹下创建一个 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 并按 Enter。
单击删除链接将从 redux 商店中删除该项目。

