React一学就会(7): 细说redux及其应用

不知不觉一个星期结束了,很快就要过年了,中间休息了两天,小孩生病,我也有点感冒了,还好,我的这个 React 基础教学课程也基本结束了。大家有不明白的可以留言问我,我一定竭尽所能的帮助你。后面几节课就 React 常用的几款第三方插件进行详细的讲解。本系列课程以 React ReduxReact RouterAxiosstyled-componentsredux-thunkReact Transition Group等插件主题进行讲解。都非常重要,可以说是学习 React 绕不过去的知识内容。这节课是Redux的内容。

React Redux

官方是这样说的:React ReduxRedux 的官方 React UI 绑定层。它允许你的 React 组件从 Redux 存储中读取数据,并将操作调度到存储以更新状态。这话说的依然很抽象。我来说的通俗一点,就是打通组件的任通二脉,让数据在组件间无障碍共享,无需通过Props或Contex那样层层包裹就能轻松的获取到数据并且能实现更改。 这你应该能明白了吧。还是那句话,完美。
这节课的内容我会适当的引用官方的示例作讲解。官方的东西往往东西写的多,讲的让人摸不着头脑,我就在关键点上再把讲的更通俗一点就很好了。

安装

直接在你的项目中安装,一定要在你项目的根目录中执行安装。如下所示,进入我们的项目目录:

cd my-react-app# npm下安装方式:
npm install @reduxjs/toolkit react-redux# Yarn安装方式:
yarn add @reduxjs/toolkit react-redux

Redux 的实现思路是这样的:把所有的组件中要使用的项目数据集中放在一个数据仓库中(store),甚后,用一个 Provider 组件对这个项目的根进行一次包裹, 这样,整个项目中所有的组件就都能够拿到这个store里的数据了。当这个store里数据发生改变,相当的组件也会及时的对UI进行更新。这就相当于在很多场景下把可以替换 state 和 props 的某些功能,让我们的组件结构更清晰。

Vite创建的项目中,在index.css文件中定义了暗模式,为了在本项目教学过程中更好的查看校果,我们把相关的CSS代码给屏蔽掉

/* color-scheme: light dark;color: rgba(255, 255, 255, 0.87);background-color: #242424; */
数据仓库 Store

首先,我们要创建这个数据仓库,所有要共享的数据都放在这个里面。如下所示

// store.jsx
import { configureStore } from '@reduxjs/toolkit'export default configureStore({reducer: {},
})

通过configStore函数创建了一个数据仓库并作为默认数据库导出。目前是一个空 Store

Provider

有了数据,当然还要提供共享数据的方法才行。React Redux 包含一个组件 <Provider />,它使得组件内所有组件包括子组件都可以获取到store 中的数据:下面的App.jsx中展示了基本的用法。

//App.jsx
import "./styles.css";import ReactDOM from 'react-dom/client'
import { Provider } from 'react-redux'
import store from './store'function App() {return (<Provider store={store}><MyApp /></Provider>)
}export default App

注意,上面的store是默认导出项,所以我们在导入时可以随意取个名。这里为store

数据切片 slice

什么叫数据切片呢,数据仓库就像一个大蛋糕,我们把数据根据业务需要分割成不同的类型数据块,这样,组件就可以按需取用,增强了逻辑清晰度。我们通过工具中的 createSlice工具来创建数据切片。如下所示:

// counterSlice.jsx
import { createSlice } from '@reduxjs/toolkit'export const counterSlice = createSlice({name: 'counter',initialState: {value: 0,},reducers: {increment: (state) => {state.value += 1},decrement: (state) => {state.value -= 1},incrementByAmount: (state, action) => {state.value += action.payload},},
})// Action creators are generated for each case reducer function
export const { increment, decrement, incrementByAmount } = counterSlice.actionsexport default counterSlice.reducer

createSlice函数需要一个对象参数,这个对象分三部分

  • name: 切片在store内的数据名称。
  • initialState: 切片数据的初始值。 本示例中相当于 store.counter.value = 0, counter 代表了当前的切片对象的状态。
  • reducers: 切片数据的操作方法。这些方法可以对切片数据进行更新。其语法类似于我们前几章前面讲到的 statesetState。其实原理是一样的。这些操作方法我们称之为动作(action)
  • action的语法: 每一个action都是一个函数对象,其函数格式如下:
 /*** @param state {当前切片状态的数据}* @param action {操作类型, 应用的时候需要从外部传入到action, 后面会有示例讲解}*/(state, action) => {// 状态的更新操作}

以上面increment为例,(state) => { state.value += 1 }state 就是当前整个切片对象状态,即 store.counter;
函数体内对 state 中的 value 进行了更改。
综上所述, 上面的 actions 中, increment为递增 value 的值, decrement 为递减value的值, 而incrementByAmount 则是根据步幅值增加value的值。

最后, 这个切片对象counterSlice就有了 actionsreducer 两个部分,actions 代表了数据操作部分,reducer代表了切片对象的状态。我们分别把它们导出就可以了。我们把reducer 作为默认项导出,方便后面导入。

将切片状态添加到 store

接下来,我们把切片状态合并到store中,如下所示, 对 store.jsx 做如下更改。

//store.jsx
import { configureStore } from '@reduxjs/toolkit'// 导入切片状态数据
import counterReducer from './counterSlice'export default configureStore({reducer: {counter: counterReducer, // 合并到store中,},
})
数据在组件中的应用

redux中,提供了两个非常重要的钩子(Hooks)来做为中间件实现 对store中的切片数据进行读取和更新的操作。

  • useDispatch: action做为参数,实现对切片数据进行更新的目的。
  • useSelectore: 实现从store中读取切片数据。

示例:

//counter.jsx
import React from 'react'
import { useSelector, useDispatch } from 'react-redux'
import { decrement, increment } from './counterSlice'
import styles from './Counter.module.css'export function Counter() {const count = useSelector((store) => store.counter.value)const dispatch = useDispatch()return (<div><div><buttonaria-label="Increment value"onClick={() => dispatch(increment())}>递增</button><span>{count}</span><buttonaria-label="Decrement value"onClick={() => dispatch(decrement())}>递减</button></div></div>)
}

详讲:

  • 示例中 useSelector 已经写的很明白了,接受一个函数参数, 这个函数的返回值即是我们要从store中读取的值, 当前为0。 参数store即是整个数据仓库对象。
  • 当点击 递增递减 按钮时,通过 diaspatch() 这个钩子,实现了 imcrement() 及 decrement() 的操作更新了切片数据。当store中的数据发生改变,也会及时的反应到相应的组件渲染中。

想一想,上面的示例是不是用Redux 实现了React中的 state的功能? 不过,我们只是用这个示例展示了它的基本用法,当然其强大的功能肯定比state的应用要广,要方便。 你看,只要把你想要共享的数据通过切片整合到 store,中,那么,所有函数组件就可能以过 useSelector 和 useDispatch 这两个钩子实现切片数据的读取和更新。相当的丝滑。

将数据连接到组件 Connect

这里我借用官方一个 TodoList 的例子,对Redux的功能应用作一个详细的讲解,我相信,我讲的更适合我们广大初学者。上面所讲针对函数组件而言,虽然已经极大的简化的我们的应用场景,但凡事都有例外不是。接下来是针对类组件的应用做一些功能解析。这个示例相对深入一些,有不懂的给我留言。先看效果:
在这里插入图片描述

目录结构

src中创建目录 Test07, 在这个目录中再创建两个目录 componentsredux, components存放组件, redux内存放数据切片(reducers)actions 等。

构建数据

我们在 Test07的目录中创建一个常量定义文件,定义一些在数据过滤状态中的数据的状态:

// ./Test07/constants.jsxexport const VISIBILITY_FILTERS = {ALL: "所有", // 表示所有数据COMPLETED: "已完成", // 已完成的数据INCOMPLETE: "未完成" // 未完成的数据
};

回到Redux目录, 创建以下文件,定义 reducer 的操作的类型

// ./Test07/redux/actionTypes.jsx
export const ADD_TODO = "ADD_TODO"; // 添加数据
export const TOGGLE_TODO = "TOGGLE_TODO"; // 切换数据
export const SET_FILTER = "SET_FILTER"; // 设置过滤条件

这个文件中的常量用于标识 action 在reducers中的操作类型。

下面定义action, 关于action请看参数前面讲的action语法小节的内容。这个action是一个数据对象。约定俗成的包含两个部:

  • type: 用于标识类型,以唯一区别每个action
  • payload: reducer 中要更新的新的数据来源。
    如下所示:
// ./Test07/redux/actions.jsximport { ADD_TODO, TOGGLE_TODO, SET_FILTER } from "./actionTypes";let nextTodoId = 0;// 增加事件操作,操作类型为 ADD_TODO, 指示 reducer 增加一个事项,增加的事件参数在 payload 中。
export const addTodo = lable => ({type: ADD_TODO,payload: {id: ++nextTodoId,lable}
});// 切换事项的完成状态操作, 指示 Reducer 将 payload 中的id事项的完成状进行切换。
export const toggleTodo = id => ({type: TOGGLE_TODO,payload: { id }
});// 过滤事项状态
export const setFilter = filter => ({ type: SET_FILTER, payload: { filter } });

我们定义了三个action: addTodotoggleTodoseFilter,这三个代表的 对Redux store的三种更新操作。上面的函数定义很简单,相信大家都能看懂

接下来我们创建selector数据选择器,因为我们针对类组件的操作,所以这时不能用useSelector这个钩子。所以我们要定义这个函数文件,具体的用法后面会讲:

// ./Test07/redux/selector.jsimport { VISIBILITY_FILTERS } from "../constants";// 根据过滤条件从store中获取数据, 用于组件创建待办事件列表
export const getTodosByVisibilityFilter = (store, visibilityFilter) => {const { todoList } = store.todos; const allTodos = Object.values(todoList); // 将对象转换为数组switch (visibilityFilter) {case VISIBILITY_FILTERS.COMPLETED: // 显示所有已完成事件return allTodos.filter((todo) => todo.completed)case VISIBILITY_FILTERS.INCOMPLETE: // 显示所有未办理事件return allTodos.filter((todo) => !todo.completed)case VISIBILITY_FILTERS.ALL:  // 显示所有事件default:return allTodos}
}
创建reducer

之前我们用数据切片(slice)方式创建的reducer, 相当于切片数据项,这里提供了另一种方式来实现。根据数据操作的类型,我们分为两个部,然后再把这两个部分集成在一个Store中。在redux目录中再创建一个目录 reducers, 在这个目录下创建文件:

// ./Test07/redux/reducers/todosReducer.jsximport { ADD_TODO, TOGGLE_TODO } from "../actionTypes";//数据切片的初始值。
const initialState = {todoList: [], // 所有事项列表byIds: {} //根据过虑条件的不同,存储相应的状态的事项列表。
};// reducer 负责根据 Action 指定的操作类型来对数据切片做出相应的更新。
function todosReducer (state = initialState, action) {switch (action.type) {case ADD_TODO: {const { id, lable } = action.payload;return {...state,todoList: {...state.todoList,[id]: {id,lable,completed: false}}};}case TOGGLE_TODO: {const { id } = action.payload;return {...state,todoList: {...state.todoList,[id]: {...state.todoList[id],completed: !state.todoList[id].completed}}};}default:return state;}
}export default todosReducer;

你看,我们很快就用到了我们上面定义的actions文件里面的action了, 根据action里的type,执行相应的更新操作。payload中提供了更新操作所要依赖的数据。
...state是ES6当中的解析语法。用于对象的复制操作是相当的棒:

const nArray = [1, 2, 3];
const newArray = [...nArray]; // 复制了nArray数组const user1 = {name: "speedx", age: 21, birthday: "2000-09-10"};
const user2 = {...user1, birthday: "2020-03-06"}
//usr2先是复制了usr1的所有属性,后面的birthday又覆盖了前面复制的birthday属性。相同的属性名后面的总会覆盖前面的值。

[id]: {...}的用法是以变量id的值作为新属性名,比如,id值为“usr“, 则 [id]:{} 等同于 usr: {...}
这样上面的todos.jsx中的内容就一目了然了。

相同的目录下,我们创建另一个reducer数据文件:

// ./Test07/redux/reducers/visibilityFilterReducer.jsximport { SET_FILTER } from "../actionTypes";
import { VISIBILITY_FILTERS } from "../../constants";const initialState = VISIBILITY_FILTERS.ALL;const visibilityFilterReducer = (state = initialState, action) => {switch (action.type) {case SET_FILTER: {return action.payload.filter;}default: {return state;}}
};export default visibilityFilterReducer;

现在我们在reducers目录中创建整合上面两个切片数据的文件,合成一个store数据仓库。创建默认文件 index.jsx, 这样我们store里就有了两个切片数据:todos, visibilityFilter

// ./Test07/redux/reducers/index.jsximport { combineReducers } from "redux";
import visibilityFilter from "./reducers/visibilityFilterReducer";
import todos from "./todosReducer";const rootReducer = combineReducers({ todos, visibilityFilter });
export default rootReducer;

为什么要用index来命名呢,index相当于文件的默认导入文件, 当我们在导入一个导出项时,只要导向到上级目录就行了,不用明确到index文件。如下面的store文件, 我们导入rootReducer只导向到目录reducers,并没有指定 reducers/index, 这就是默认文件的用法。
之前我们用的是 @reduxjs/toolkit 工具中的 configureStore 来整合切片数据的,这里用 combineReducers 来直接整合的。方法不同,目的相同。这很好理解。
现在切片已经整合好了,那么我们创建仓库吧。在redux目录下创建 store.jsx文件

// ./Test07/redux/store.jsximport { configureStore } from '@reduxjs/toolkit'
import rootReducer from "./reducers";const rootStore =  configureStore({reducer: rootReducer
});export default rootStore;

数据已经准备好了,剩下的就是数据的展示了。

准备相关的组件

我们在components目录中创建一些必要的展示组件。这些组件中使用了 MUI框架。

创建AddTodo组件

// ./Test07/components/Addtodo.jsximport React from "react";
import Button from "@mui/material/Button";
import TextField from '@mui/material/TextField';
import { Stack } from "@mui/material";import { connect } from 'react-redux'
import { addTodo } from '../redux/actions'class AddTodo extends React.Component {constructor(props) {super(props);this.state = { input: "" };}updateInput = input => {this.setState({ input });};handleAddTodo = () => {// dispatches actions to add todo// sets state back to empty string};render() {return (<Stack direction="row" spacing={2}><TextFieldonChange={e => this.updateInput(e.target.value)}value={this.state.input}/><Button variant="contained" onClick={this.handleAddTodo}>添加事项</Button></Stack>);}
}const ConnectAddTodo = connect(null, { addTodo })(AddTodo);export default ConnectAddTodo

Todo组件

//./Test07/components/Todo.jsx
import ListItemIcon from '@mui/material/ListItemIcon';
import ListItemText from '@mui/material/ListItemText';
import ListItemButton from '@mui/material/ListItemButton';import { connect } from "react-redux";
import { toggleTodo } from "../redux/actions";const Todo = ({ todo }) => (<ListItemButtononClick={() => { toggleTodo() } }><ListItemIcon>{todo && todo.completed ? "👌" : "👋"}{" "}</ListItemIcon><ListItemText>{ todo.content }</ListItemText></ListItemButton>
);export default Todo;

TodoList组件:

// ./Test07/components/TodoList.jsx
import Todo from "./Todo";
import List from '@mui/material/List';
const TodoList = ({ todos }) => (<List>{todos && todos.length? todos.map((todo, index) => {return <Todo key={index} todo={todo} />;}): "没有待办事项! 噢耶!"}</List>
);export default TodoList;

VisibilityFilters组件

// ./Test07/components/VisibilityFilters.jsx
import { Button, Stack } from "@mui/material";
import { VISIBILITY_FILTERS } from "../constants";
import Box from '@mui/material/Box';const VisibilityFilters = ({ activeFilter }) => {return (<Stack spacing={2} direction="row">{Object.keys(VISIBILITY_FILTERS).map((filterKey, index) => {const currentFilter = VISIBILITY_FILTERS[filterKey];return (<Button variant="contained"key={index}onClick={() => { } /** waiting for setFilter handler*/}>{ currentFilter }</Button>);})}</Stack>);
};export default VisibilityFilters;

创建 TodoApp 组件,这个组件我们在Test07目录下创建

import Typography from "@mui/material/Typography";
import AddTodo from "./components/Addtodo";
import TodoList from "./components/TodoList";
import VisibilityFilters from "./components/VisibilityFilters";
import Stack from "@mui/material/Stack";export default function TodoApp() {return (<Stack spacing={3} alignItems={"center"}><Typography variant="h1">待办事项列表</Typography><AddTodo /><TodoList /><VisibilityFilters /></Stack>);
}

目前,我们并没有把数据连接到组件中。只是先把 UI 框架搭建起来了。

为App提供数据

在App.jsx中引入store, 并向我们的App中提供数据

// App.jsx
import './App.css'
import "./styles.css";
import TodoApp from './Test07/TodoApp';import { Provider } from 'react-redux'
import store from './redux/store';function App() {return (<Provider store={store}><TodoApp /></Provider>)
}export default App
Connect函数

React Redux 提供了一个函数connect(),用于从 Redux 存储中读取值(并在存储更新时重新读取值)。
该函数采用两个参数,均为可选参数:

  • mapStateToProps:此函数返回一个数据对象,传递给Connect后将状态数据整合到组件的Props中。
  • mapDispatchToProps:此参数可以是函数,也可以是对象。如果它是一个函数,它将在创建组件时调用一次dispatchdispatch将作为参数接收。如果它是一个action组成的对象,则每个action都将变成一个函数,该函数在调用时自动调度其action
    还是很抽象对不对。没有示例的解说都是很惨白的。看示例:
const mapStateToProps = (store, ownProps) => ({// 计算状态数据return {name: "data1",todos: [],id: "idIndex"}
})const mapDispatchToProps = (dispatch) => {return {// 其实所有的 action 都是通过 dispatch 派发下去的。increment: () => dispatch({ type: 'INCREMENT' }),decrement: () => dispatch({ type: 'DECREMENT' }),reset: () => dispatch({ type: 'RESET' }),}
}// 将上面生成的状态和动作进行组合生成一个新的连接函数
const connectToStore = connect(mapStateToProps, mapDispatchToProps)//用这个连接函数与组件连接把 状态数据 和 action 数据 传递到组件的Props
const ConnectedComponent = connectToStore(Component)
connect(mapStateToProps, mapDispatchToProps)(Component)

我们首先定义mapStateToProps函数和 mapDispatchToProps函数, 顾名思义, mapStateToProps的意思就是把state数据解构到组件的 Props当中。 而 mapDispatchToProps 则是把action的动作解构到 Props中。
例如:我们在actions.jsx中定义了多个action. 以addTodo为例:

// actions.jsx...
let nextTodoId = 0;export const addTodo = content => ({type: ADD_TODO,payload: {id: ++nextTodoId,content}
});
...

我们将它传递到Connect函数并与组件结合, 修改AddTodo组件如下,导入 connectaddTodo :

// ./Test07/components/Addtodo.jsximport React from "react";
import Button from "@mui/material/Button";
import TextField from '@mui/material/TextField';
import { Stack } from "@mui/material";import { connect } from 'react-redux'
import { addTodo } from '../redux/actions'class AddTodo extends React.Component {constructor(props) {super(props);this.state = { input: "" };}updateInput = input => {this.setState({ input });};handleAddTodo = () => {// dispatches actions to add todo// sets state back to empty string};render() {return (<Stack direction="row" spacing={2}><TextFieldonChange={e => this.updateInput(e.target.value)}value={this.state.input}/><Button variant="contained" onClick={this.handleAddTodo}>添加事项</Button></Stack>);}
}const ConnectAddTodo = connect(null, { addTodo })(AddTodo);export default ConnectAddTodo

这里我们并没有定义mapDispatchToProps函数, 而是直接传递了一个 Actions 对象,有时这比定义mapDispatchToProps要简单, 省去了dispatch的麻烦, 当直接传递Actions对象时,就自动dispatch
注意最后两句,connect()函数会返回一个新的组件,我们导出的是这个连接后的新的组件。现在你应该懂了,connect的第一参数是纯数据组成的对象,而第二个参数可以是mapDispatchToProps函数,也可以是像上面所示的 actions 对象. 想想看,这样一来,有时一个组件内的点击事件是由调用方定义的,这时我们就可以通过这个方法把定义好的事件传递进去。这样,我们连接后的组件的props中就有一个 addTodo 的属性。
我们继续修改这个组件,组件内handleAddTodo事件目录是空的。做如下修改:

...class AddTodo extends React.Component {constructor(props) {super(props);this.state = { input: "" };}updateInput = input => {this.setState({ input });};// 这里直接使用了props中的 addTodo 事件对象了。handleAddTodo = () => {this.props.addTodo(this.state.input);this.setState({ input: "" });};render() {return (<Stack direction="row" spacing={2}><TextFieldonChange={e => this.updateInput(e.target.value)}value={this.state.input}/><Button variant="contained" onClick={this.handleAddTodo}>添加事项</Button></Stack>);}
}const ConnectAddTodo = connect(null, { addTodo })(AddTodo);
export default ConnectAddTodo;

现在我们在 input 中输入内容后点击 “添加事项" 按钮后,内容会被清空,除此以外没有什么反应。没有关系,这是因为,我们的其它组件还没有连接到 store。为了观察到store中的数据,我们对TodoList也进行连接:

import Todo from "./Todo";
import List from '@mui/material/List';
import { connect } from 'react-redux'const TodoList = ({ todos }) => (<List>{todos.length > 0? todos.map((todo, index) => {return <Todo key={index} todo={todo} />;}): "没有待办事项! 噢耶!"}</List>
);const mapStateToProps = state => {const { todoList, byIds } = state.todos || {};console.log("todoList =>", todoList)const todos =byIds.length > 0? byIds.map(todo => todoList[todo]): Object.values(todoList);console.log("todos =>", todos)return { todos };
};const ConnectedTodoList = connect(mapStateToProps)(TodoList);
export default ConnectedTodoList;

定义 mapStateToProps ,并在 mapStateToProps 内打印state数据。mapStateToProps 返回一个todos数据,通过connect连接到组件中。这样我们就能看到数据的变化,如下所示:
在这里插入图片描述

我们再继续,对Todo.jsx进行修改,把数据连接进去

import ListItemIcon from '@mui/material/ListItemIcon';
import ListItemText from '@mui/material/ListItemText';
import ListItemButton from '@mui/material/ListItemButton';
import { toggleTodo } from '../redux/actions';
import { connect } from 'react-redux';const Todo = ({ todo, toggleTodo }) => {console.log("todo =>", todo)return (<ListItemButtonselected={todo.completed}onClick={() => toggleTodo(todo.id) }><ListItemIcon>{ todo.completed ? "👌" : "👋"}{" "}</ListItemIcon><ListItemText>{ todo.lable }</ListItemText></ListItemButton>
)};const ConnectedTodo = connect(null, { toggleTodo })(Todo);
export default ConnectedTodo;

让我们实现VisibilityFilters的功能。对VisibilityFilters.jsx进行修改:

import { Button, Stack } from "@mui/material";
import { VISIBILITY_FILTERS } from "../constants";
import { connect } from 'react-redux';
import { setFilter } from '../redux/actions';const VisibilityFilters = ({ activeFilter, setFilter }) => {return (<Stack spacing={2} direction="row">{Object.keys(VISIBILITY_FILTERS).map((filterKey, index) => {const currentFilter = VISIBILITY_FILTERS[filterKey];return (<Button variant={currentFilter == activeFilter ? "contained" : "outlined"}key={index}onClick={() => setFilter(currentFilter) }>{ currentFilter }</Button>);})}</Stack>);
};const mapStateToProps = state => {return { activeFilter: state.visibilityFilter };
};const ConnectedVisibilityFilter = connect(mapStateToProps,{ setFilter }
)(VisibilityFilters);export default ConnectedVisibilityFilter;

这个功能应该不用过多的解释,就是对三个按钮的点击事件进行设置,通过 setFilter 这个action 更新 store.visibilityFilter状态状态值。
最后,就是根据所选的显示功能按钮,刷新事项列表。修改TodoList组件,导入selector.jsx中的功能函数,如下所示:

import Todo from "./Todo";
import List from '@mui/material/List';
import { connect } from 'react-redux';
import { getTodosByVisibilityFilter } from "../redux/selector";const TodoList = ({ todos }) => (<List>{todos.length > 0? todos.map((todo, index) => {return <Todo key={index} todo={todo} />;}): "没有待办事项! 噢耶!"}</List>
);const mapStateToProps = state => {// const { todoList, byIds } = state.todos || {};// console.log("todoList =>", todoList)// const todos =//     byIds.length > 0//         ? byIds.map(todo => todoList[todo])//         : Object.values(todoList);// console.log("todos =>", todos)const { visibilityFilter } = state;const todos = getTodosByVisibilityFilter(state, visibilityFilter);return { todos };
};const ConnectedTodoList = connect(mapStateToProps)(TodoList);
export default ConnectedTodoList;

到目前为止,项目的目录结构如下所示:
在这里插入图片描述

OK, 完美收官。怎么样,Redux 是不是也没有那么难,一但掌握,就能极大的简化组件间的通信代码。 最后送上完成后的动图,是不是相当完美。

在这里插入图片描述

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.hqwc.cn/news/437994.html

如若内容造成侵权/违法违规/事实不符,请联系编程知识网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

leetcode刷题(剑指offer) 79.单词搜索

79.单词搜索 给定一个 m x n 二维字符网格 board 和一个字符串单词 word 。如果 word 存在于网格中&#xff0c;返回 true &#xff1b;否则&#xff0c;返回 false 。 单词必须按照字母顺序&#xff0c;通过相邻的单元格内的字母构成&#xff0c;其中“相邻”单元格是那些水…

如何在Win系统安装Jupyter Notbook并实现无公网ip远程访问本地笔记

文章目录 1.前言2.Jupyter Notebook的安装2.1 Jupyter Notebook下载安装2.2 Jupyter Notebook的配置2.3 Cpolar下载安装 3.Cpolar端口设置3.1 Cpolar云端设置3.2.Cpolar本地设置 4.公网访问测试5.结语 1.前言 在数据分析工作中&#xff0c;使用最多的无疑就是各种函数、图表、…

数字与数学高频问题(算法村第十三关白银挑战)

数组实现加法专题 数组实现整数加法 66. 加一 - 力扣&#xff08;LeetCode&#xff09; 给定一个由 整数 组成的 非空 数组所表示的非负整数&#xff0c;在该数的基础上加一。 最高位数字存放在数组的首位&#xff0c; 数组中每个元素只存储单个数字。 你可以假设除了整数…

软件测试人员常用的功能测试方法分享

功能测试就是对产品的各功能进行验证&#xff0c;根据功能测试用例&#xff0c;逐项测试&#xff0c;检查产品是否达到用户要求的功能。 常用的测试方法如下&#xff1a; 1. 页面链接检查 每一个链接是否都有对应的页面&#xff0c;并且页面之间切换正确。 2. 相关性检查 删除/…

[k8s系列]:kubernetes·概念入门

文章目录 序言1 kubernetes概述1.1 kubernetes解决的问题1.1.1 部署方式的演变1.1.2 容器化部署——容器编排问题 1.2 kubernetes组件1.2.1 kubernetes组件调用关系1.2.2 调用逻辑示例 序言 序言&#xff1a;本文将从&#xff0c;第一节&#xff1a;kubernetes解决的问题、组件…

【网络】WireShark过滤 | WireShark实现TCP三次握手和四次挥手

目录 一、开启WireShark的大门 1.1 WireShark简介 1.2 常用的Wireshark过滤方式 二、如何抓包搜索关键字 2.1 协议过滤 2.2 IP过滤 ​编辑 2.3 过滤端口 2.4 过滤MAC地址 2.5 过滤包长度 2.6 HTTP模式过滤 三、ARP协议分析 四、WireShark之ICMP协议 五、TCP三次握…

【hcie-cloud】【23】容器编排【k8s】【Kubernetes常用工作负载、Kubernetes调度器简介、Helm简介、缩略词】【下】

文章目录 单机容器面临的问题、Kubernetes介绍与安装、Kubernetes对象的基本操作、Kubernetes YAML文件编写基础Kubernetes常用工作负载Kubernetes常用工作负载简介创建一个无状态nginx集群无状态工作负载Deployment说明无状态工作负载Deployment常见操作创建一个有状态的MySQL…

MySQL解决 恢复从备份点到灾难点之间数据恢复

CSDN 成就一亿技术人&#xff01; 今天分享一期 mysql中 备份之后发生灾难造成数据丢失 那么如何恢复中间的数据呢&#xff1f; 数据库数据高于一切&#xff08;任何数据是不能丢失的&#xff09; CSDN 成就一亿技术人&#xff01; 目录 1.准备测试数据库 2.备份数据库 观…

D6287F——正反转马达驱动电路,采 用 SOP8的 封 装 形 式 封 装 。驱动电流最高可达1.0A

D6287F 是 一 块 正 反 转 马 达 驱 动 电 路 &#xff0c;两 种 逻 辑 输 入 方 式 可 控 制 马 达 的 正 转 、 反 转 、 停 止 、 中 断 等 。 内 置 马 达 停 止 时 省 电 电 路 及 热 保 护 电 路 。 最 大 驱 动 电 流 达 1.0A 。 广 泛 用 于 VCRs及 音 频 设 备 等 电…

QEMU源码全解析42 —— Machine(12)

接前一篇文章&#xff1a;QEMU源码全解析41 —— Machine&#xff08;11&#xff09; 本文内容参考&#xff1a; 《趣谈Linux操作系统》 —— 刘超&#xff0c;极客时间 《QEMU/KVM》源码解析与应用 —— 李强&#xff0c;机械工业出版社 特此致谢&#xff01; 上一回针对于…

springboot外出务工人员信息管理系统源码和论文

网络的广泛应用给生活带来了十分的便利。所以把疫情防控期间某村外出务工人员信息管理与现在网络相结合&#xff0c;利用java技术建设疫情防控期间某村外出务工人员信息管理系统&#xff0c;实现疫情防控期间某村外出务工人员信息的信息化。则对于进一步提高疫情防控期间某村外…

前端Vue v-for 的使用

目录 ​编辑 简介 使用方式 基本使用 v-for"(item, index)中item和index作用 示例 迭代对象 示例 结果 前言-与正文无关 生活远不止眼前的苦劳与奔波&#xff0c;它还充满了无数值得我们去体验和珍惜的美好事物。在这个快节奏的世界中&#xff0c;我们往往容易陷入…