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 包含一个组件 ,它使得组件内所有组件包括子组件都可以获取到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的功能应用作一个详细的讲解,我相信,我讲的更适合我们广大初学者。上面所讲针对函数组件而言,虽然已经极大的简化的我们的应用场景,但凡事都有例外不是。接下来是针对类组件的应用做一些功能解析。这个示例相对深入一些,有不懂的给我留言。先看效果:
React一学就会(7): 细说redux及其应用_第1张图片

目录结构

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连接到组件中。这样我们就能看到数据的变化,如下所示:
React一学就会(7): 细说redux及其应用_第2张图片

我们再继续,对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;

到目前为止,项目的目录结构如下所示:
React一学就会(7): 细说redux及其应用_第3张图片

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

你可能感兴趣的:(前端react技术积累,react.js,前端,前端框架,javascript,Redux)