- ReactJS 教程
- ReactJS - 首页
- ReactJS - 简介
- ReactJS - 路线图
- ReactJS - 安装
- ReactJS - 特性
- ReactJS - 优点与缺点
- ReactJS - 架构
- ReactJS - 创建 React 应用
- ReactJS - JSX
- ReactJS - 组件
- ReactJS - 嵌套组件
- ReactJS - 使用新创建的组件
- ReactJS - 组件集合
- ReactJS - 样式
- ReactJS - 属性 (props)
- ReactJS - 使用属性创建组件
- ReactJS - props 验证
- ReactJS - 构造函数
- ReactJS - 组件生命周期
- ReactJS - 事件管理
- ReactJS - 创建一个事件感知组件
- ReactJS - 在 Expense Manager 应用中引入事件
- ReactJS - 状态管理
- ReactJS - 状态管理 API
- ReactJS - 无状态组件
- ReactJS - 使用 React Hooks 进行状态管理
- ReactJS - 使用 React Hooks 进行组件生命周期管理
- ReactJS - 布局组件
- ReactJS - 分页
- ReactJS - Material UI
- ReactJS - Http 客户端编程
- ReactJS - 表单编程
- ReactJS - 受控组件
- ReactJS - 非受控组件
- ReactJS - Formik
- ReactJS - 条件渲染
- ReactJS - 列表
- ReactJS - Keys
- ReactJS - 路由
- ReactJS - Redux
- ReactJS - 动画
- ReactJS - Bootstrap
- ReactJS - Map
- ReactJS - 表格
- ReactJS - 使用 Flux 管理状态
- ReactJS - 测试
- ReactJS - CLI 命令
- ReactJS - 构建和部署
- ReactJS - 示例
- Hooks
- ReactJS - Hooks 简介
- ReactJS - 使用 useState
- ReactJS - 使用 useEffect
- ReactJS - 使用 useContext
- ReactJS - 使用 useRef
- ReactJS - 使用 useReducer
- ReactJS - 使用 useCallback
- ReactJS - 使用 useMemo
- ReactJS - 自定义 Hooks
- ReactJS 高级
- ReactJS - 可访问性
- ReactJS - 代码分割
- ReactJS - Context
- ReactJS - 错误边界
- ReactJS - 转发 Refs
- ReactJS - 片段
- ReactJS - 高阶组件
- ReactJS - 集成其他库
- ReactJS - 性能优化
- ReactJS - Profiler API
- ReactJS - Portals
- ReactJS - 无 ES6 ECMAScript 的 React
- ReactJS - 无 JSX 的 React
- ReactJS - 调和
- ReactJS - Refs 和 DOM
- ReactJS - Render Props
- ReactJS - 静态类型检查
- ReactJS - Strict Mode
- ReactJS - Web Components
- 附加概念
- ReactJS - 日期选择器
- ReactJS - Helmet
- ReactJS - 行内样式
- ReactJS - PropTypes
- ReactJS - BrowserRouter
- ReactJS - DOM
- ReactJS - 轮播图
- ReactJS - 图标
- ReactJS - 表单组件
- ReactJS - 参考 API
- ReactJS 有用资源
- ReactJS - 快速指南
- ReactJS - 有用资源
- ReactJS - 讨论
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 的工作流程如下图所示。
- 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://127.0.0.1:3000,然后按回车键。
点击移除链接将从redux存储中移除该项目。