作为 Write for DOnations 计划的一部分,作者选择了 Creative Commons 来接受捐赠。
介绍
Redux 是 JavaScript 和 React 应用程序的流行数据存储。 它遵循一个中心原则,即数据绑定应沿一个方向流动,并应作为单一事实来源存储。 Redux 因其设计理念的简单性和相对较小的实现而广受欢迎。
Redux 根据几个概念运行。 首先,store 是单个对象,其中包含每个数据选择的字段。 您可以通过调度说明数据应如何更改的 action 来更新数据。 然后使用 reducers 解释动作并更新数据。 Reducers 是对数据应用动作并返回一个新的 state 的函数,而不是改变之前的状态。
在小型应用程序中,您可能不需要全局数据存储。 您可以混合使用 local state 和 context 来管理状态。 但是随着您的应用程序扩展,您可能会遇到这样的情况:集中存储信息以便它可以跨路由和 组件 持续存在是很有价值的。 在这种情况下,Redux 将为您提供一种以有组织的方式存储和检索数据的标准方法。
在本教程中,您将通过构建一个观鸟测试应用程序在 React 应用程序中使用 Redux。 用户将能够添加他们见过的鸟,并在每次再次看到它时增加一只鸟。 您将构建单个数据存储,并创建操作和缩减程序来更新存储。 然后,您将数据拉入您的组件并发送新的更改以更新数据。
先决条件
- 您将需要一个运行 Node.js 的开发环境; 本教程在 Node.js 版本 10.22.0 和 npm 版本 6.14.6 上进行了测试。 要在 macOS 或 Ubuntu 18.04 上安装它,请按照 如何在 macOS 上安装 Node.js 和创建本地开发环境中的步骤或 的 使用 PPA 部分安装如何在 Ubuntu 18.04 上安装 Node.js。
- 使用 Create React App 设置的 React 开发环境,删除了非必要的样板。 要进行此设置,请按照 步骤 1 — 创建一个空项目的如何管理 React 类组件上的状态教程 。 本教程将使用
redux-tutorial作为项目名称。 - 您将在本教程中使用 React 组件、Hook 和表单,包括
useStateHook 和自定义 Hook。 您可以在我们的教程 如何使用 React 组件上的 Hooks 和 如何在 React 中构建表单 中了解组件和 Hooks。 - 您还需要 JavaScript、HTML 和 CSS 的基本知识,您可以在我们的 如何使用 HTML 构建网站系列、如何使用 CSS 构建网站系列 中找到这些知识, 以及 如何在 JavaScript 中编码 。
第 1 步 — 开设商店
在这一步中,您将安装 Redux 并将其连接到您的根 组件 。 然后,您将创建一个基础商店并在您的组件中显示信息。 在这一步结束时,您将拥有一个 Redux 的工作实例,并在您的组件中显示信息。
首先,安装 redux 和 react-redux。 包 redux 与框架无关,将连接您的操作和减速器。 包 react-redux 包含在 React 项目中运行 Redux 存储的绑定。 您将使用 react-redux 中的代码从组件发送操作并将数据从存储中提取到组件中。
使用npm安装这两个包,命令如下:
npm install --save redux react-redux
组件安装完成后,您将收到如下输出。 您的输出可能略有不同:
Output... + redux@4.0.5 + react-redux@7.2.1 added 2 packages from 1 contributor, updated 1 package and audited 1639 packages in 20.573s
现在您已经安装了包,您需要将 Redux 连接到您的项目。 要使用 Redux,您需要使用 Provider 包装根组件,以确保存储可用于树中的所有子组件。 这类似于使用 React 的原生上下文 添加 Provider 的方式。
打开src/index.js:
nano src/index.js
从 react-redux 包中导入 Provider 组件。 通过对代码进行以下突出显示的更改,将 Provider 添加到围绕任何其他组件的根组件中:
redux-tutorial/src/index.js
import React from 'react';
import ReactDOM from 'react-dom';
import './index.css';
import App from './components/App/App';
import * as serviceWorker from './serviceWorker';
import { Provider } from 'react-redux';
ReactDOM.render(
<React.StrictMode>
<Provider>
<App />
</Provider>
</React.StrictMode>,
document.getElementById('root')
);
// If you want your app to work offline and load faster, you can change
// unregister() to register() below. Note this comes with some pitfalls.
// Learn more about service workers: https://bit.ly/CRA-PWA
serviceWorker.unregister();
现在您已经包装了组件,是时候添加一个 store。 store 是您的数据中心集合。 在下一步中,您将学习创建 reducers 来设置默认值并更新您的商店,但现在您将对数据进行硬编码。
从 redux 导入 createStore 函数,然后传递一个返回 object 的函数。 在这种情况下,返回一个对象,其中包含一个名为 birds 的字段,该字段指向单个鸟类的 array。 每只鸟都有一个 name 和一个 views 计数。 将函数的输出保存为一个名为 store 的值,然后将 store 传递给 Provider 中名为 store 的 prop :
redux-tutorial/src/index.js
import React from 'react';
import ReactDOM from 'react-dom';
import './index.css';
import App from './components/App/App';
import * as serviceWorker from './serviceWorker';
import { Provider } from 'react-redux';
import { createStore } from 'redux';
const store = createStore(() => ({
birds: [
{
name: 'robin',
views: 1
}
]
}));
ReactDOM.render(
<React.StrictMode>
<Provider store={store}>
<App />
</Provider>
</React.StrictMode>,
document.getElementById('root')
);
// If you want your app to work offline and load faster, you can change
// unregister() to register() below. Note this comes with some pitfalls.
// Learn more about service workers: https://bit.ly/CRA-PWA
serviceWorker.unregister();
保存并关闭文件。 现在您有了一些数据,您需要能够显示它。 打开src/components/App/App.js:
nano src/components/App/App.js
与 context 一样,每个子组件都无需任何额外的道具即可访问商店。 要访问 Redux 商店中的项目,请使用 react-redux 包中名为 useSelector 的 Hook。 useSelector Hook 将选择器函数作为参数。 选择器函数将接收商店的状态作为参数,您将使用该参数返回所需的字段:
redux-tutorial/src/components/App/App.js
import React from 'react';
import { useSelector } from 'react-redux';
import './App.css';
function App() {
const birds = useSelector(state => state.birds);
return <></>
}
export default App;
由于 useSelector 是自定义 Hook,因此每当调用 Hook 时组件都会重新渲染。 这意味着数据 - birds - 将始终是最新的。
现在您有了数据,您可以在无序列表中显示它。 使用 wrapper 的 className 创建一个周围的 <div>。 在内部,添加一个 <ul> 元素并使用 map() 循环遍历 birds 数组,为每个元素返回一个新的 <li> 项。 请务必将 bird.name 用作 key:
redux-tutorial/src/components/App/App.js
import React from 'react';
import { useSelector } from 'react-redux'
import './App.css';
function App() {
const birds = useSelector(state => state.birds);
return (
<div className="wrapper">
<h1>Bird List</h1>
<ul>
{birds.map(bird => (
<li key={bird.name}>
<h3>{bird.name}</h3>
<div>
Views: {bird.views}
</div>
</li>
))}
</ul>
</div>
);
}
export default App;
保存文件。 保存文件后,浏览器将重新加载,您将找到您的鸟类列表::
现在您有了一个基本列表,添加您的观鸟应用程序所需的其余组件。 首先,在视图列表之后添加一个按钮来增加视图:
redux-tutorial/src/components/App/App.js
import React from 'react';
import { useSelector } from 'react-redux'
import './App.css';
function App() {
const birds = useSelector(state => state.birds);
return (
<div className="wrapper">
<h1>Bird List</h1>
<ul>
{birds.map(bird => (
<li key={bird.name}>
<h3>{bird.name}</h3>
<div>
Views: {bird.views}
<button><span role="img" aria-label="add">➕</span></button>
</div>
</li>
))}
</ul>
</div>
);
}
export default App;
接下来,在鸟列表之前创建一个 <form> 和一个 <input>,以便用户可以添加新鸟。 确保用 <label> 将 <input> 包围,并将 submit 的 type 添加到添加按钮以确保所有内容都可以访问:
redux-tutorial/src/components/App/App.js
import React from 'react';
import { useSelector } from 'react-redux'
import './App.css';
function App() {
const birds = useSelector(state => state.birds);
return (
<div className="wrapper">
<h1>Bird List</h1>
<form>
<label>
<p>
Add Bird
</p>
<input type="text" />
</label>
<div>
<button type="submit">Add</button>
</div>
</form>
<ul>
{birds.map(bird => (
<li key={bird.name}>
<h3>{bird.name}</h3>
<div>
Views: {bird.views}
<button><span role="img" aria-label="add">➕</span></button>
</div>
</li>
))}
</ul>
</div>
);
}
export default App;
保存并关闭文件。 接下来,打开 App.css 添加一些样式:
nano src/components/App/App.css
添加一些 padding 到 wrapper 类。 然后将包含鸟名的 h3 元素大写。 最后,为按钮设置样式。 删除添加 <button> 上的默认按钮样式,然后为表单 <button> 添加边距。
将文件内容替换为以下内容:
redux-tutorial/src/components/App/App.css
.wrapper {
padding: 20px;
}
.wrapper h3 {
text-transform: capitalize;
}
.wrapper form button {
margin: 10px 0;
cursor: pointer;
}
.wrapper ul button {
background: none;
border: none;
cursor: pointer;
}
此外,给每个按钮一个 pointer 的 cursor,当鼠标悬停在按钮上时会改变光标,向用户表明该按钮是可点击的。
保存并关闭文件。 当您这样做时,浏览器将使用您的组件刷新:
按钮和表单尚未连接到任何操作,因此无法与 Redux 存储交互。 您将在步骤 2 中添加操作并在步骤 3 中连接它们。
在这一步中,您安装了 Redux 并为您的应用程序创建了一个新商店。 您使用 Provider 将商店连接到您的应用程序,并使用 useSelector Hook 访问组件内的元素。
在下一步中,您将创建操作和缩减程序以使用新信息更新您的商店。
第 2 步——创建动作和减速器
接下来,您将创建添加鸟和增加视图的操作。 然后,您将创建一个减速器,它将根据操作类型更新信息。 最后,您将使用 reducer 通过 combineReducers 创建默认存储。
操作是您向数据存储发送的带有预期更改的消息。 Reducers 获取这些消息并通过根据操作类型应用更改来更新共享存储。 您的组件将发送他们希望您的商店使用的操作,您的减速器将使用操作来更新商店中的数据。 你永远不会直接调用 reducer,在某些情况下,一个操作可能会影响多个 reducer。
组织你的动作和减速器 有许多不同的选项。 在本教程中,您将按域进行组织。 这意味着您的操作和减速器将由它们将影响的功能类型定义。
创建一个名为 store 的目录:
mkdir src/store
该目录将包含您的所有操作和减速器。 一些模式将它们与组件一起存储,但这里的优点是您对整个商店的形状有一个单独的参考点。 当新的开发人员进入项目时,他们将能够一目了然地阅读商店的结构。
在 store 目录中创建一个名为 birds 的目录。 这将包含专门用于更新鸟类数据的操作和减速器:
mkdir src/store/birds
接下来,打开一个名为 birds.js 的文件,以便您可以开始添加动作和减速器。 如果你有大量的 action 和 reducer,你可能希望将它们拆分成单独的文件,例如 birds.actions.js 和 birds.reducers.js,但是当只有几个时,它会更容易阅读它们位于同一位置:
nano src/store/birds/birds.js
首先,您将创建操作。 操作是您使用称为 dispatch 的方法从组件发送到商店的消息,您将在下一步中使用该方法。
一个动作必须返回一个带有 type 字段的对象。 否则,返回对象可以包含您要发送的任何附加信息。
创建一个名为 addBirds 的函数,该函数将 bird 作为参数并返回一个包含 'ADD_BIRD' 的 type 和 bird 的对象一个领域:
redux-tutorial/src/store/birds/birds.js
export function addBird(bird) {
return {
type: 'ADD_BIRD',
bird,
}
}
请注意,您正在导出该函数,以便以后可以从组件中导入和调度它。
type 字段对于与 reducer 通信很重要,因此按照惯例,大多数 Redux 存储会将类型保存到变量中以防止拼写错误。
创建一个名为 ADD_BIRD 的 const 来保存字符串 'ADD_BIRD'。 然后更新动作:
redux-tutorial/src/store/birds/birds.js
const ADD_BIRD = 'ADD_BIRD';
export function addBird(bird) {
return {
type: ADD_BIRD,
bird,
}
}
现在您有了一个动作,创建一个将响应该动作的减速器。
Reducers 是一种函数,它们将根据动作确定状态应如何变化。 动作本身不会改变; 减速器将获取状态并根据操作进行更改。
reducer 接收两个参数:当前状态和动作。 当前状态是指商店特定部分的状态。 一般reducer的名字会和store中的一个字段匹配。 例如,假设您有一个形状像这样的商店:
{
birds: [
// collection of bird objects
],
gear: {
// gear information
}
}
您将创建两个减速器:birds 和 gear。 birds 减速器的 state 将是鸟类数组。 gear 减速器的 state 将是包含齿轮信息的对象。
在 birds.js 内部创建一个名为 birds 的减速器,它采用 state 和 action 并返回 state 而不做任何更改:
redux-tutorial/src/store/birds/birds.js
const ADD_BIRD = 'ADD_BIRD';
export function addBird(bird) {
return {
type: ADD_BIRD,
bird,
}
}
function birds(state, action) {
return state;
}
请注意,您没有导出减速器。 您不会直接使用 reducer,而是将它们组合成一个可用的集合,您将导出并使用该集合在 index.js 中创建基本存储。 另请注意,如果没有更改,您需要返回 state。 Redux 将在您派发操作时运行所有减速器,因此如果您不返回状态,则可能会丢失更改。
最后,由于 Redux 在没有变化的情况下返回状态,所以使用 默认参数 添加默认状态。
创建一个包含占位符鸟类信息的 defaultBirds 数组。 然后更新 state 以包含 defaultBirds 作为默认参数:
redux-tutorial/src/store/birds/birds
const ADD_BIRD = 'ADD_BIRD';
export function addBird(bird) {
return {
type: ADD_BIRD,
bird,
}
}
const defaultBirds = [
{
name: 'robin',
views: 1,
}
];
function birds(state=defaultBirds, action) {
return state;
}
现在您有了一个返回状态的 reducer,您可以使用该操作来应用更改。 最常见的模式是在 action.type 上使用 开关 来应用更改。
创建将查看 action.type 的 switch 语句。 如果情况是 ADD_BIRD,则 将当前状态展开 到一个新数组中,并用单个视图添加鸟:
redux-tutorial/src/store/birds/birds.js
const ADD_BIRD = 'ADD_BIRD';
export function addBird(bird) {
return {
type: ADD_BIRD,
bird,
}
}
const defaultBirds = [
{
name: 'robin',
views: 1,
}
];
function birds(state=defaultBirds, action) {
switch (action.type) {
case ADD_BIRD:
return [
...state,
{
name: action.bird,
views: 1
}
];
default:
return state;
}
}
请注意,您将 state 作为 default 值返回。 更重要的是,您没有直接改变 state 。 相反,您通过扩展旧数组并添加新值来创建新数组。
现在您有了一个操作,您可以创建一个用于增加视图的操作。
创建一个名为 incrementBird 的动作。 与 addBird 动作类似,这将把鸟作为参数并返回一个带有 type 和 bird 的对象。 唯一的区别是类型将是 'INCREMENT_BIRD':
redux-tutorial/src/store/birds/birds.js
const ADD_BIRD = 'ADD_BIRD';
const INCREMENT_BIRD = 'INCREMENT_BIRD';
export function addBird(bird) {
return {
type: ADD_BIRD,
bird,
}
}
export function incrementBird(bird) {
return {
type: INCREMENT_BIRD,
bird
}
}
const defaultBirds = [
{
name: 'robin',
views: 1,
}
];
function birds(state=defaultBirds, action) {
switch (action.type) {
case ADD_BIRD:
return [
...state,
{
name: action.bird,
views: 1
}
];
default:
return state;
}
}
此操作是单独的,但您将使用相同的减速器。 请记住,动作传达了您想要对数据进行的更改,reducer 应用这些更改以返回新状态。
增加一只鸟不仅仅是添加一只新鸟。 在 birds 内部为 INCREMENT_BIRD 添加一个新的案例。 然后使用 find() 将需要递增的鸟拉出数组,将每个 name 与 action.bird 进行比较:
redux-tutorial/src/store/bird/birds.js
const ADD_BIRD = 'ADD_BIRD';
...
function birds(state=defaultBirds, action) {
switch (action.type) {
case ADD_BIRD:
return [
...state,
{
name: action.bird,
views: 1
}
];
case INCREMENT_BIRD:
const bird = state.find(b => action.bird === b.name);
return state;
default:
return state;
}
}
您有需要更改的鸟,但您需要返回一个新状态,其中包含所有未更改的鸟以及您正在更新的鸟。 通过选择 name 不等于 action.name 的所有鸟来选择所有剩余的具有 state.filter 的鸟。 然后通过展开 birds 数组并在末尾添加 bird 来返回一个新数组:
redux-tutorial/src/store/bird/birds.js
const ADD_BIRD = 'ADD_BIRD';
...
function birds(state=defaultBirds, action) {
switch (action.type) {
case ADD_BIRD:
return [
...state,
{
name: action.bird,
views: 1
}
];
case INCREMENT_BIRD:
const bird = state.find(b => action.bird === b.name);
const birds = state.filter(b => action.bird !== b.name);
return [
...birds,
bird,
];
default:
return state;
}
}
最后,通过创建具有递增 view 的新对象来更新 bird:
redux-tutorial/src/store/bird/birds.js
const ADD_BIRD = 'ADD_BIRD';
...
function birds(state=defaultBirds, action) {
switch (action.type) {
case ADD_BIRD:
return [
...state,
{
name: action.bird,
views: 1
}
];
case INCREMENT_BIRD:
const bird = state.find(b => action.bird === b.name);
const birds = state.filter(b => action.bird !== b.name);
return [
...birds,
{
...bird,
views: bird.views + 1
}
];
default:
return state;
}
}
请注意,您没有使用 reducer 对数据进行排序。 排序可以被认为是视图问题,因为视图将信息显示给用户。 您可以有一个按名称排序的视图和一个按视图计数排序的视图,因此最好让单个组件处理排序。 相反,让 reducer 专注于更新数据,而组件专注于将数据转换为用户可用的视图。
这个减速器也不完美,因为你可以添加同名的鸟。 在生产应用程序中,您需要在添加之前进行验证或给鸟一个唯一的 id,以便您可以通过 id 而不是 name 选择鸟。
现在你有两个完整的 action 和一个 reducer。 最后一步是导出reducer,以便它可以初始化存储。 在第一步中,您通过传递一个返回对象的函数来创建商店。 在这种情况下,您将做同样的事情。 该函数将获取 store 和 action,然后将 store 的特定切片与动作一起传递给减速器。 它看起来像这样:
export function birdApp(store={}, action) {
return {
birds: birds(store.birds, action)
}
}
为了简化事情,Redux 有一个名为 combineReducers 的辅助函数,它为你组合了 reducer。
在 birds.js 内部,从 redux 导入 combineReducers。 然后用 birds 调用函数并导出结果:
redux-tutorial/src/store/bird/birds.js
import { combineReducers } from 'redux';
const ADD_BIRD = 'ADD_BIRD';
const INCREMENT_BIRD = 'INCREMENT_BIRD';
export function addBird(bird) {
return {
type: ADD_BIRD,
bird,
}
}
export function incrementBird(bird) {
return {
type: INCREMENT_BIRD,
bird
}
}
const defaultBirds = [
{
name: 'robin',
views: 1,
}
];
function birds(state=defaultBirds, action) {
switch (action.type) {
case ADD_BIRD:
return [
...state,
{
name: action.bird,
views: 1
}
];
case INCREMENT_BIRD:
const bird = state.find(b => action.bird === b.name);
const birds = state.filter(b => action.bird !== b.name);
return [
...birds,
{
...bird,
views: bird.views + 1
}
];
default:
return state;
}
}
const birdApp = combineReducers({
birds
});
export default birdApp;
保存并关闭文件。
你的 action 和 reducer 都设置好了。 最后一步是使用组合减速器而不是占位符函数来初始化您的商店。
打开src/index.js:
nano src/index.js
从 birds.js 导入 birdApp。 然后使用 birdApp 初始化 store:
redux-tutorial/src/index.js
import React from 'react';
import ReactDOM from 'react-dom';
import './index.css';
import App from './components/App/App';
import * as serviceWorker from './serviceWorker';
import { Provider } from 'react-redux'
import { createStore } from 'redux'
import birdApp from './store/birds/birds';
const store = createStore(birdApp);
ReactDOM.render(
<React.StrictMode>
<Provider store={store}>
<App />
</Provider>
</React.StrictMode>,
document.getElementById('root')
);
// If you want your app to work offline and load faster, you can change
// unregister() to register() below. Note this comes with some pitfalls.
// Learn more about service workers: https://bit.ly/CRA-PWA
serviceWorker.unregister();
保存并关闭文件。 当您这样做时,浏览器将使用您的应用程序刷新:
在这一步中,您创建了动作和减速器。 您学习了如何创建返回 type 的操作,以及如何构建使用该操作来构建和返回基于该操作的新状态的化简器。 最后,您将 reducer 组合成一个用于初始化存储的函数。
您的 Redux 存储现已全部设置好并准备好进行更改。 在下一步中,您将从组件调度操作以更新数据。
第 3 步 — 分发组件中的更改
在此步骤中,您将从组件中导入并调用您的操作。 您将使用一个名为 dispatch 的方法来发送操作,并且您将在 form 和 button 的 事件处理程序 内分派操作.
在这一步结束时,您将拥有一个结合了 Redux 存储和您的自定义组件的工作应用程序。 您将能够实时更新 Redux 存储,并且能够在组件更改时显示组件中的信息。
现在您有了工作操作,您需要将它们连接到您的事件,以便您可以更新商店。 您将使用的方法称为 dispatch,它将特定操作发送到 Redux 存储。 当 Redux 接收到你派发的 action 时,它会将 action 传递给 reducer,它们会更新数据。
打开App.js:
nano src/components/App/App.js
在 App.js 内部,从 react-redux 导入 Hook useDispath。 然后调用该函数创建一个新的dispatch函数:
redux-tutorial/src/components/App/App.js
import React from 'react';
import { useDispatch, useSelector } from 'react-redux'
import './App.css';
function App() {
...
}
export default App;
接下来,您需要导入您的操作。 请记住,动作是返回对象的函数。 该对象是您最终将传递给 dispatch 函数的对象。
从商店导入 incrementBird。 然后在按钮上创建一个 onClick 事件。 当用户点击按钮时,用 bird.name 调用 incrementBird 并将结果传递给 dispatch。 为了使内容更具可读性,请在 dispatch 内部调用 incrementBird 函数:
redux-tutorial/src/components/App/App.js
import React from 'react';
import { useDispatch, useSelector } from 'react-redux'
import { incrementBird } from '../../store/birds/birds';
import './App.css';
function App() {
const birds = useSelector(state => state.birds);
const dispatch = useDispatch();
return (
<div className="wrapper">
<h1>Bird List</h1>
<form>
<label>
<p>
Add Bird
</p>
<input type="text" />
</label>
<div>
<button type="submit">Add</button>
</div>
</form>
<ul>
{birds.map(bird => (
<li key={bird.name}>
<h3>{bird.name}</h3>
<div>
Views: {bird.views}
<button onClick={() => dispatch(incrementBird(bird.name))}><span role="img" aria-label="add">➕</span></button>
</div>
</li>
))}
</ul>
</div>
);
}
export default App;
保存文件。 当你这样做时,你将能够增加 robin 计数:
接下来,您需要调度 addBird 动作。 这将需要两个步骤:将输入保存到内部状态并使用 onSubmit 触发调度。
使用 useState Hook 保存输入值。 请务必通过在输入上设置 value 将输入转换为受控组件。 查看教程 How To Build Forms in React 以更深入地了解受控组件。
对您的代码进行以下更改:
redux-tutorial/src/components/App/App.js
import React, { useState } from 'react';
import { useDispatch, useSelector } from 'react-redux'
import { incrementBird } from '../../store/birds/birds';
import './App.css';
function App() {
const [birdName, setBird] = useState('');
const birds = useSelector(state => state.birds);
const dispatch = useDispatch();
return (
<div className="wrapper">
<h1>Bird List</h1>
<form>
<label>
<p>
Add Bird
</p>
<input
type="text"
onChange={e => setBird(e.target.value)}
value={birdName}
/>
</label>
<div>
<button type="submit">Add</button>
</div>
</form>
<ul>
...
</ul>
</div>
);
}
export default App;
接下来,从 birds.js 导入 addBird,然后创建一个名为 handleSubmit 的函数。 在 handleSubmit 函数中,使用 event.preventDefault 阻止页面表单提交,然后使用 birdName 作为参数调度 addBird 操作。 调度动作后,调用 setBird() 清除输入。 最后,将 handleSubmit 传递给 form 上的 onSubmit 事件处理程序:
redux-tutorial/src/components/App/App.js
import React, { useState } from 'react';
import { useDispatch, useSelector } from 'react-redux'
import { addBird, incrementBird } from '../../store/birds/birds';
import './App.css';
function App() {
const [birdName, setBird] = useState('');
const birds = useSelector(state => state.birds);
const dispatch = useDispatch();
const handleSubmit = event => {
event.preventDefault();
dispatch(addBird(birdName))
setBird('');
};
return (
<div className="wrapper">
<h1>Bird List</h1>
<form onSubmit={handleSubmit}>
<label>
<p>
Add Bird
</p>
<input
type="text"
onChange={e => setBird(e.target.value)}
value={birdName}
/>
</label>
<div>
<button type="submit">Add</button>
</div>
</form>
<ul>
{birds.map(bird => (
<li key={bird.name}>
<h3>{bird.name}</h3>
<div>
Views: {bird.views}
<button onClick={() => dispatch(incrementBird(bird.name))}><span role="img" aria-label="add">➕</span></button>
</div>
</li>
))}
</ul>
</div>
);
}
export default App;
保存文件。 当你这样做时,浏览器将重新加载,你将能够添加一只鸟:
您现在正在调用您的操作并更新商店中的鸟类列表。 请注意,当您的应用程序刷新时,您丢失了以前的信息。 存储全部包含在内存中,因此页面刷新将擦除数据。
如果您在列表中增加一只鸟,此列表顺序也会发生变化。
正如您在第 2 步中看到的,您的 reducer 不关心数据的排序。 为防止组件发生意外更改,您可以对组件中的数据进行排序。 将 sort() 函数添加到 birds 数组。 请记住,排序会改变数组,而您永远不想改变存储。 确保在排序之前通过传播数据来创建一个新数组:
redux-tutorial/src/components/App/App.js
import React, { useState } from 'react';
import { useDispatch, useSelector } from 'react-redux'
import { addBird, incrementBird } from '../../store/birds/birds';
import './App.css';
function App() {
const [birdName, setBird] = useState('');
const birds = [...useSelector(state => state.birds)].sort((a, b) => {
return a.name.toLowerCase() > b.name.toLowerCase() ? 1 : -1;
});
const dispatch = useDispatch();
const handleSubmit = event => {
event.preventDefault();
dispatch(addBird(birdName))
setBird('');
};
return (
<div className="wrapper">
<h1>Bird List</h1>
<form onSubmit={handleSubmit}>
<label>
<p>
Add Bird
</p>
<input
type="text"
onChange={e => setBird(e.target.value)}
value={birdName}
/>
</label>
<div>
<button type="submit">Add</button>
</div>
</form>
<ul>
{birds.map(bird => (
<li key={bird.name}>
<h3>{bird.name}</h3>
<div>
Views: {bird.views}
<button onClick={() => dispatch(incrementBird(bird.name))}><span role="img" aria-label="add">➕</span></button>
</div>
</li>
))}
</ul>
</div>
);
}
export default App;
保存文件。 当您这样做时,组件将在您增加鸟类时保持字母顺序。
重要的是不要在 Redux 商店中尝试做太多事情。 让 reducer 专注于维护最新信息,然后在组件内为您的用户提取和操作数据。
注意: 在本教程中,请注意每个动作和减速器都有相当多的代码。 幸运的是,有一个名为 Redux Toolkit 的官方支持项目可以帮助您减少样板代码的数量。 Redux Toolkit 提供了一套实用的工具来快速创建 action 和 reducer,还可以让你用更少的代码创建和配置你的 store。
在这一步中,您从一个组件中分派了您的操作。 您学习了如何调用操作以及如何将结果发送到调度函数,并将它们连接到组件上的事件处理程序以创建一个完全交互式的存储。 最后,您学习了如何通过对数据进行排序而不直接改变商店来保持一致的用户体验。
结论
Redux 是一个受欢迎的单一商店。 在使用需要公共信息源的组件时,这可能是有利的。 然而,它并不总是在所有项目中都是正确的选择。 较小的项目或具有独立组件的项目将能够使用内置的状态管理和上下文。 但是随着您的应用程序变得越来越复杂,您可能会发现中央存储对于维护数据完整性至关重要。 在这种情况下,Redux 是一个出色的工具,可以创建一个统一的数据存储,您可以轻松地跨组件使用该数据存储。
如果您想阅读更多 React 教程,请查看我们的 React 主题页面 ,或返回 如何在 React.js 系列页面中编码 。