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.actions

export 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>
        <button
          aria-label="Increment value"
          onClick={() => dispatch(increment())}
        >
          递增
        </button>
        <span>{count}</span>
        <button
          aria-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.jsx

export 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.jsx

import { 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.js

import { 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.jsx

import { 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.jsx

import { 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.jsx

import { 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.jsx

import { configureStore } from '@reduxjs/toolkit'
import rootReducer from "./reducers";

const rootStore =  configureStore({
    reducer: rootReducer
});

export default rootStore;

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

准备相关的组件

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

创建AddTodo组件

// ./Test07/components/Addtodo.jsx

import 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}>
                <TextField
                    onChange={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 }) => (
    <ListItemButton
       
        onClick={() => { 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.jsx

import 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}>
                <TextField
                    onChange={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}>
                <TextField
                    onChange={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 (
        <ListItemButton
            selected={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 是不是也没有那么难,一但掌握,就能极大的简化组件间的通信代码。 最后送上完成后的动图,是不是相当完美。

在这里插入图片描述

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

如若内容造成侵权/违法违规/事实不符,请联系我们进行投诉反馈qq邮箱809451989@qq.com,一经查实,立即删除!

相关文章

D3703F——应用于音响系统的静音检测电路 当音响系统在放音或快进 / 退时进行静音检测,输出控制信号。

D3703F 是 一 块 汽 车 音 响 静 音 检 测 电 路 。 用 于 音 响 系 统 检 测 在 放 音 或 快 进 / 退 时 进 行 静 音 检 测 。 D3703F 的 的 电 压 范 围 &#xff1a; 3.2V &#xff5e; 16V &#xff0c; 信 号 检 测 和 静 音 时 间 可 通 过 外 围 电 阻 、 电 容 来 …

中小型企业知识库建设的秘诀来啦,赶紧收藏起来

知识库是企业的智慧宝库&#xff0c;其中的信息和知识的整合&#xff0c;可以极大地提高工作效率和团队协作能力。尤其对中小企业来说&#xff0c;知识库的建设更是关系企业未来发展的重要因素。那么&#xff0c;怎样有效地构建高效的知识库系统呢&#xff1f;下面这些秘诀值得…

Arthas的使用

1. 简介 官网 线上debug神器&#xff0c;就不过多介绍 2. 环境搭建 win11环境 ,jdk11 2.1 安装 下载地址 2.2 启动 cmd java -jar arthas-boot.jar启动之后会自动检测启动的java服务 1~4 &#xff0c;springboot是启动类名&#xff0c;所以我选择了3 3. 常用操作 3.…

Hadoop3.x基础(1)

来源&#xff1a;B站尚硅谷 这里写目录标题 大数据概论大数据概念大数据特点(4V)大数据应用场景 Hadoop概述Hadoop是什么Hadoop发展历史&#xff08;了解&#xff09;Hadoop三大发行版本&#xff08;了解&#xff09;Hadoop优势&#xff08;4高&#xff09;Hadoop组成&#xf…

非阿里云注册域名如何在云解析DNS设置解析?

概述 非阿里云注册域名使用云解析DNS&#xff0c;按照如下步骤&#xff1a; 添加域名。 添加解析记录。 修改DNS服务器。 DNS服务器变更全球同步&#xff0c;等待48小时。 添加解析记录 登录云解析DNS产品控制台。 在 域名解析 页面中&#xff0c;单击 添加域名 。 在 …

SkyWalking+es部署与使用

第一步下载skywalking :http://skywalking.apache.org/downloads/ 第二步下载es:https://www.elastic.co/cn/downloads/elasticsearch 注&#xff1a;skywalking 和es要版本对应&#xff0c;可从下面连接查看版本对应关系&#xff0c;8.5.0为skywalking 版本号 Index of /di…

实惨!多本EI接连被各大数据库剔除!2024年EI期刊目录首次更新-附下载

EI目录更新 本月爱思唯尔&#xff08;Elsevier&#xff09;官网更新了EI Compendex收录期刊目录&#xff0c;这是2024年第一次更新。 Elsevier发布2024年第一版EI期刊目录 更新时间&#xff1a;2024年1月1日 不同于SCI/SSCI目录每月更新一次的频率&#xff0c;EI目录更新没有…

VBA技术资料MF112:列出目录中的所有文件和文件夹

我给VBA的定义&#xff1a;VBA是个人小型自动化处理的有效工具。利用好了&#xff0c;可以大大提高自己的工作效率&#xff0c;而且可以提高数据的准确度。我的教程一共九套&#xff0c;分为初级、中级、高级三大部分。是对VBA的系统讲解&#xff0c;从简单的入门&#xff0c;到…

构建高效外卖系统:利用Spring Boot框架实现

在当今快节奏的生活中&#xff0c;外卖系统已经成为人们生活中不可或缺的一部分。为了构建一个高效、可靠的外卖系统&#xff0c;我们可以利用Spring Boot框架来实现。本文将介绍如何利用Spring Boot框架构建一个简单但功能完善的外卖系统&#xff0c;并提供相关的技术代码示例…

解析线上HBase集群CPU飙高的原因与解决方案

在日常的运维工作中&#xff0c;CPU负载高是一种常见的故障状况&#xff0c;它可能对系统的正常运行和性能产生不利影响。为了准确地定位具体的异常原因&#xff0c;掌握一些专业的工具和方法是至关重要的。本文将通过一个实际的案例&#xff0c;详细介绍如何排查在线上HBASE集…

【C++】C++入门基础讲解(二)

&#x1f497;个人主页&#x1f497; ⭐个人专栏——C学习⭐ &#x1f4ab;点击关注&#x1f929;一起学习C语言&#x1f4af;&#x1f4ab; 导读 接着上一篇的内容继续学习&#xff0c;今天我们需要重点学习引用。 1. 引用 在C中&#xff0c;引用是一种特殊的变量&#xff…

动态gif图如何在线做?这一招分分钟生成

Gif动图是怎么制作呢&#xff1f;Gif动画已经是日常聊天娱乐必备的了&#xff0c;那么这种有趣的gif表情要怎么操作呢&#xff1f;很简单&#xff0c;使用gif动图生成&#xff08;https://www.gif.cn/&#xff09;工具无需下载软件&#xff0c;小白也能轻松操作。可上传MP4格式…

Vue2 VS Vue3 生命周期

一、生命周期的概念 Vue组件实例在创建时要经历一系列的初始化步骤&#xff0c;在此过程中Vue会在合适的时机&#xff0c;调用特定的函数&#xff0c;从而让开发者有机会在特定阶段运行自己的代码&#xff0c;这些特定的函数统称为&#xff1a;生命周期钩子&#xff08;也会叫…

每日一道面试题:Java中序列化与反序列化

写在开头 哈喽大家好&#xff0c;在高铁上码字的感觉是真不爽啊&#xff0c;小桌板又拥挤&#xff0c;旁边的小朋友也比较的吵闹&#xff0c;影响思绪&#xff0c;但这丝毫不影响咱学习的劲头&#xff01;哈哈哈&#xff0c;在这喧哗的车厢中&#xff0c;思考着这样的一个问题…

对Spring当中AOP的理解

AOP(面向切面编程)全称Aspect Oriented Programminge AOP就是把系统中重复的代码抽取出来&#xff0c;单独开发&#xff0c;在系统需要时&#xff0c;使用动态代理技术&#xff0c;在不修改源码的基础上&#xff0c;将单独开发的功能通知织入(应用)到系统中的过程&#xff0c;完…

shopee,lazada卖家自养号测评补单的方法和技巧

现在很多卖家都是自己管理几百个账号&#xff0c;交给服务商不是特别靠谱 一&#xff1a;送测不及时&#xff0c;产品时常送不出去 二&#xff1a;账号质量不稳定&#xff0c;账号一天下了多少你也不清楚&#xff0c;如果下了很多单万一封号被关联了怎么办 三&#xff1a;as…

ESP8266采用AT指令连接华为云服务器方法(MQTT固件)

一、前言 本篇文章主要介绍3个内容&#xff1a; &#xff08;1&#xff09;ESP8266-WIFI模块常用的AI指令功能介绍 &#xff08;2&#xff09;ESP8266烧写MQTT固件连接华为云IOT服务器。 &#xff08;3&#xff09;介绍华为云IOT服务器的配置过程。 ESP8266是一款功能强大…

vite引入图片用法

在 vite 中 引入图片方式跟其他脚手架创建项目的引入方式不一样 要使用 import.meta.url 他是一个 ESM 的原生功能 const mapPicSrc ref(new URL(/assets/charts/bdf.png, import.meta.url).href) 如果使用 require 引入会报错 说没有 require 模块

【Shell实战案例面试题】输入网卡的名字,来输出网卡的IP

1.问题 参数后判断要加"" 名字为空时显示ip 2.分析 把本机的所有网卡名列出来&#xff0c;来引导用户输入 使用命令列出所有网卡信:ifconfig/ip a 设计一个函数&#xff0c;把网卡名作为参数&#xff0c;函数返回网卡的IP 在获取某个网卡IP时&#xff0c;考虑网…

解决NuxtJS3中安装pinia报错

使用npm install pinia 安装pinia报错。 修改镜像后也报错&#xff1a; 方式一&#xff1a; npm config set registry https://registry.npm.taobao.org/ npm install pinia方式二&#xff1a; npm config set registry https://registry.npmjs.org/ npm install pinia解决方…