Redux最新实践指南之Redux-Toolkit

news2025/1/27 9:36:12

前言

redux-toolkit是目前redux官方推荐的编写redux逻辑的方法,针对redux的创建store繁琐、样板代码太多、依赖外部库等问题进行了优化,官方总结了四个特点是简易的/有想法的/强劲的/高效的,总结来看,就是更加的方便简单了。

从官方github的记录来看,该项目从2018年就开始了,从官方文档的更新可以看出,现在redux已经正式开始推广,其文档内容中结合了大量的redux-toolkit示例,具体可参考Redux中文官网或Redux官网,最新的redux4.2.0版本也把redux-toolkit定为最佳redux使用方式

而且原来的createStore方法已经被标识为弃用。

本文主要对redux-toolkit进行介绍,以下简称为RTK。如果对redux的概念有不清楚的请先了解后再来学习。本文示例代码见https://github.com/storepage/redux-toolkit-demo

使用介绍

安装包依赖

yarn add @reduxjs/toolkit react-redux

不再需要单独安装redux了

react配置

import React from 'react';
import { createRoot } from 'react-dom/client';
import { Provider } from 'react-redux';
import { store } from './app/store';
import App from './App';
import './index.css';

const container = document.getElementById('root')!;
const root = createRoot(container);

root.render(
  <Provider store={store}>
    <App />
  </Provider>
);

这里和之前react-redux基本没有差别,还是提供Provider的根组件,传入store树作为参数,但store的定义有些许区别,具体见下一节。(root组件渲染这里用了react 18的写法)

创建Store

/* app/store.ts */
import { configureStore } from '@reduxjs/toolkit';
import counterReducer from '../features/counter/counterSlice';
import todoReducer from '../features/todo/todoSlice';

export const store = configureStore({
  reducer: {
    counter: counterReducer,
    todo: todoReducer,
  },
});

调用rtk的configureStore方法,该方法相当于集成了redux和redux-redux的createStore、combineReducers、middleware、enhancers,以及默认支持了扩展工具Redux DevTools。

参数选项为一个对象,包含以下内容

参数key值说明
reducer创建reducer,传递给combineReducers使用
middleware中间件,传递给applyMiddleware使用
devTools扩展工具,默认为true
preloadedState初始state值,传递给createStore
enhancers增强 store,传递给createStore

数据获取和发起action

先来看下组件内如何使用的,这一步可以认为完全是react-redux的内容

import React, { useState } from 'react';
import { useDispatch, useSelector } from 'react-redux';

import {
  decrement,
  selectCount
} from './counterSlice';

export function Counter() {
  const count = useSelector(selectCount);
  const dispatch = useAppDispatch();

  return (
    <div>
      <div className={styles.row}>
        <button
          className={styles.button}
          aria-label="Decrement value"
          onClick={() => dispatch(decrement())}
        >
          -
        </button>
        <span className={styles.value}>{count}</span>
        <button
          className={styles.button}
          aria-label="Increment value"
          onClick={() => dispatch(increment())}
        >
          +
        </button>
      </div>
    </div>
  );
}

示例为一个简单的计数器,自react支持hooks的写法后,react-redux也提供了获取state和dispatch一个action的自定义hooks,可以不再需要使用connect方法往props上绑定。

针对上面两个hooks,关于如何使用redux的数据,官方也给出了新的建议

  • 任意组件都能从 Redux Store 中读取任意数据
  • 任意组件都能通过 dispatch actions 引发状态更新(state updates)

目前官方文档上已经找不到之前关于pressenational和container component的概念(翻译不用,有叫做UI组件和容器组件,也有叫做智能组件和木偶组价)(这个概念是redux作者于2015年推荐的划分,喜欢考究的可以阅读Presentational and Container Components)

 ——以上截图出自目前找到的繁体版的文档,Read Me | Redux

redux官方不再推荐上面组件的划分,一方面原因应该是他把重心更多放在了redux本身上,至于用户怎么拆分组件,就交给用户思考好了。另一方面可能是之前connect的方法相比较会更影响性能。

定义切片Slice

rtk引入了新的定义slice,它是应用中对action和reducer逻辑的整合,通过createSlice方法进行创建

import { createSlice, PayloadAction } from '@reduxjs/toolkit';


export interface CounterState {
  value: number;
  status: 'idle' | 'loading' | 'failed';
}

const initialState: CounterState = {
  value: 0,
  status: 'idle',
};


export const counterSlice = createSlice({
  name: 'counter',
  initialState,
  reducers: {
    increment: (state: CounterState) => {
      state.value += 1;
    },
    decrement: (state: CounterState) => {
      state.value -= 1;
    },
    // Use the PayloadAction type to declare the contents of `action.payload`
    incrementByAmount: (state: CounterState, action: PayloadAction<number>) => {
      state.value += action.payload;
    },
  },
});

export const { increment, decrement, incrementByAmount } = counterSlice.actions;

export default counterSlice.reducer;


这里的reducers参数既定义了reducers,又创建了关联的action。这个方法最终返回一个包含actions和reducer的对象。

可以看到这里对state进行了直接修改,表面看来是违背了redux禁止修改state原则,实际是因为引用了immer的库,总是返回一个安全的、不可变的更新值,极大的简化reducer的写法。注意该方式只能在createSlice和createReducer中编写。

从redux扩展工具可以看到,这里相当生成了{name}/{reducers.key}的type

注意这里reducer的action中如果要传入参数,只能是一个payload,如果是多个参数的情况,那就需要封装成一个payload的对象。

实际是用中可能需要对数据进行处理,reducer的定义除了支持一个方法,还可以提供一个预处理方法,因为reducer必须是纯函数,其他可变的参数就可以放到预处理函数中,预处理函数必须返回一个带有payload字段的对象


import { createSlice, PayloadAction, nanoid } from '@reduxjs/toolkit';

const todosSlice = createSlice({
  name: 'todos',
  initialState: [],
  reducers: {
    addTodo: {
      reducer(state: TodoState[], action: PayloadAction<TodoState>) {
        const { id, text } = action.payload;
        state.push({ id, text, completed: false });
      },
      prepare(text: string) {
        /* nanoid 随机一个字符串 */
        return { payload: { text, id: nanoid() } };
      },
    },
    toggleTodo(state: TodoState[], action: PayloadAction<string>) {
      const todo = state.find((todo: { id: any }) => todo.id === action.payload);
      if (todo) {
        todo.completed = !todo.completed;
      }
    },
  },
});

 rtk还提供了一个nanoid方法,用于生成一个固定长度的随机字符串,类似uuid功能。

可以打印dispatch(addTodo(text))的结果看到,返回了一个带有payload字段的action

副作用处理

rtk集成了redux-thunk来处理异步事件,所以可以按照之前thunk的写法来写异步请求

// 外部的 thunk creator 函数
const fetchSomething = params => {
  // 内部的 thunk 函数
  return async (dispatch, getState) => {
    try {
      // thunk 内发起异步数据请求
      const res = await someApi(params)
      // 但数据响应完成后 dispatch 一个 action
      dispatch(someAction(res));
      return res;
    } catch (err) {
      // 如果过程出错,在这里处理
    }
  }
}
/* 组件中发起action */
dispatch(fetchSomething(params))

 rtk提供新的方法createAsyncThunk支持thunk,这也是redux推荐的方式

export const incrementAsync = createAsyncThunk('counter/fetchCount', async (amount: number) => {
  const response = await fetchCount(amount);
  return response.data;
});

export const counterSlice = createSlice({
  name: 'counter',
  initialState,
  extraReducers: (builder) => {
    builder
      .addCase(incrementAsync.pending, (state) => {
        state.status = 'loading';
      })
      .addCase(incrementAsync.fulfilled, (state, action) => {
        state.status = 'idle';
        state.value += action.payload;
      })
      .addCase(incrementAsync.rejected, (state) => {
        state.status = 'failed';
      });
  },
});

createAsyncThunk 可以写在任何一个slice的extraReducers中,它接收2个参数,

  • 生成action的type值,这里type是要自己定义,不像是createSlice自动生成type,这就要注意避免命名冲突问题了(如果createSlice定义了相当的name和方法,也是会冲突的)
  • 包含数据处理的promise,首先会dispatch一个action类型为'counter/fetchCount/pending',当异步请求完成后,根据结果成功或是失败,决定dispatch出action的类型为'counter/fetchCount/fulfilled'或'counter/fetchCount/rejected',这三个action可以在slice的extraReducers中进行处理。这个promise也只接收2个参数,分别是payload和包含了dispatch、getState的thunkAPI对象,所以除了在slice的extraReducers中处理之外,createAsyncThunk中也可以调用任意的action,这样就很像原本thunk的写法了,并不推荐
export const incrementAsync = createAsyncThunk('counter/fetchCount', 
  async (amount: number, thunkApi) => {
    const response = await fetchCount(amount);
    thunkApi.dispatch(counterSlice.actions.incrementByAmount(response.data));
    return response.data;
  }
);

  另外, extraReducers除了上面链式方法外,还可以定义为也可以是一个对象,结构为{[type]: function(state, action)},这是一种遗留的语法,虽然支持,但不被官方推荐。

  extraReducers: {
    [incrementAsync.pending.type]: (state) => {
      state.status = 'loading';
    },
    [incrementAsync.fulfilled.type]: (state, action) => {
      state.status = 'idle';
      state.value += action.payload;
    },
    [incrementAsync.rejected.type]: (state) => {
      state.status = 'failed';
    },
  },

 dispatch了createAsyncThunk的action会捕获所有的异常,最终返回一个action

如果要想要获取createAsyncThunk的promose里返回的数据,并且自己处理try/catch,可以调用unwrap方法

async () => {
  try {
    const payload = await dispatch(incrementAsync(incrementValue)).unwrap();
    console.log(payload);
  } catch (error) {
    // deal with error
  }
}

进阶使用

Selector使用缓存

当useSelector方法涉及到复杂逻辑运算时,且返回一个对象的时候,每次运行都返回了一个新的引用值,会使组件重新渲染,即使返回的数据内容并没有改变,如下带有过滤的todoList所示

const list = useSelector((state: RootState) => {
    const { todo, visibilityFilter } = state;
    switch (visibilityFilter) {
      case VisibilityFilters.SHOW_ALL:
        return todo;
      case VisibilityFilters.SHOW_COMPLETED:
        return todo.filter((t: TodoState) => t.completed);
      case VisibilityFilters.SHOW_ACTIVE:
        return todo.filter((t: TodoState) => !t.completed);
      default:
        throw new Error('Unknown filter: ' + visibilityFilter);
    }
  });

为了解决这个问题,可以使用Reselect库,它是一个创建记忆化selector的库,只有在输入发生变化时才会重新计算结果,rtk正是集成了这个库,并把它导出为createSelector函数,上面的selector就可以做如下改写

const selectTodos = (state: RootState) => state.todo;
const selectFilter = (state: RootState) => state.visibilityFilter;

// 创建记忆化selector
const selectList = createSelector(selectTodos, selectFilter, (todo, filter) => {
  switch (filter) {
    case VisibilityFilters.SHOW_ALL:
      return todo;
    case VisibilityFilters.SHOW_COMPLETED:
      return todo.filter((t: TodoState) => t.completed);
    case VisibilityFilters.SHOW_ACTIVE:
      return todo.filter((t: TodoState) => !t.completed);
    default:
      throw new Error('Unknown filter: ' + filter);
  }
});

// 使用记忆化selector
const list = useSelector((state: RootState) => selectList(state));

createSelector接收一个或多个selector输入函数(逐个传入或作为一个数组)和一个selector输出函数,其中每个输入函数的出参会作为最后一个输出函数的入参。

另外,上述问题还可以通过useSelector传入第二个参数比较函数来解决。

范式化state结构

“范式化 state”是指:

  • 我们 state 中的每个特定数据只有一个副本,不存在重复。
  • 已范式化的数据保存在查找表中,其中项目 ID 是键,项本身是值。
  • 也可能有一个特定项用于保存所有 ID 的数组。

根据以上定义,决定了它的结构形式为

{
  ids: ["user1", "user2", "user3"],
  entities: {
    "user1": {id: "user1", firstName, lastName},
    "user2": {id: "user2", firstName, lastName},
    "user3": {id: "user3", firstName, lastName},
  } 
}

这样的结构类似于字典,方便增删改查功能。rtk提供了createEntityAdapter api,对范式化结构的存储进行一系列标准化操作。

import { 
  createSlice, 
  PayloadAction, 
  createEntityAdapter, 
  nanoid, 
  EntityState 
} from '@reduxjs/toolkit';
import { RootState } from '../../app/store';

export interface TodoPayload {
  todoId: string;
  text: string;
  completed?: boolean;
  createdTimestamp: number;
}

/* 创建EntityAdapter */
const todoAdapter = createEntityAdapter<TodoPayload>({
  /* 默认值为id */
  selectId: (todo) => todo.todoId,
  /* 对ids进行排序,方法与Array.sort相同,如果不提供,不能保证ids顺序 */
  sortComparer: (a, b) => a.createdTimestamp - b.createdTimestamp,
});

const todosSlice = createSlice({
  name: 'todosEntity',
  initialState: todoAdapter.getInitialState(),
  reducers: {
    /* 增 */
    addTodo: {
      reducer(state: EntityState<TodoPayload>, action: PayloadAction<TodoPayload>) {
        todoAdapter.addOne(state, action.payload);
      },
      prepare(text: string) {
        return {
          payload: {
            text,
            todoId: nanoid(),
            createdTimestamp: Date.now(),
          },
        };
      },
    },
    /* 删 */
    removeTodo(state: EntityState<TodoPayload>, action: PayloadAction<string>) {
      todoAdapter.removeOne(state, action.payload);
    },
    /* 改 */
    toggleTodo(state: EntityState<TodoPayload>, action: PayloadAction<string>) {
      const todo = state.entities[action.payload];
      if (todo) {
        todo.completed = !todo.completed;
      }
    },
  },
});

/* 查 */
export const { selectAll: selectAllTodos } = todoAdapter.getSelectors((state: RootState) => state.todoEntity);

/* action */
export const { actions: todoActions } = todosSlice;
/* reducer */
export default todosSlice.reducer;

掌握该结构主要包含三个要点

  1. 创建一个特定id标识、支持排序的EntityAdapter
  2. 获取state的初始值getInitialState,除了默认的ids和entities外,还可以添加自定义字段
  3. 一系列的增删改查方法,这些方法大都需要传入当前的state和要操作的数据

使用 Redux-Persist

如果要使用Redux-Persist的话,需要特定地忽略它dispatch的所有的action types。参考Question: How to use this with redux-persist? #121

import { configureStore, getDefaultMiddleware } from '@reduxjs/toolkit'
import {
  persistStore,
  persistReducer,
  FLUSH,
  REHYDRATE,
  PAUSE,
  PERSIST,
  PURGE,
  REGISTER
} from 'redux-persist'
import storage from 'redux-persist/lib/storage'
import { PersistGate } from 'redux-persist/integration/react'

import App from './App'
import rootReducer from './reducers'

const persistConfig = {
  key: 'root',
  version: 1,
  storage
}

const persistedReducer = persistReducer(persistConfig, rootReducer)

const store = configureStore({
  reducer: persistedReducer,
  middleware: getDefaultMiddleware({
    serializableCheck: {
      ignoredActions: [FLUSH, REHYDRATE, PAUSE, PERSIST, PURGE, REGISTER]
    }
  })
})

let persistor = persistStore(store)

ReactDOM.render(
  <Provider store={store}>
    <PersistGate loading={null} persistor={persistor}>
      <App />
    </PersistGate>
  </Provider>,
  document.getElementById('root')
)

上面rootReducer写法同之前的reducer一样,需要使用combineReducers整合

import { combineReducers } from 'redux';

import counterReducer from '../features/counter/counterSlice';
import todoReducer from '../features/todos/todoSlice';
import todoEntityReducer from '../features/todosEntity/todoSlice';
import visibilityFilterReducer from '../features/filters/filtersSlice';

export default combineReducers({
  counter: counterReducer,
  todo: todoReducer,
  todoEntity: todoEntityReducer,
  visibilityFilter: visibilityFilterReducer,
});

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

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

vue-treeselect 的基本使用

官网地址&#xff1a;Vue-Treeselecthttps://vue-treeselect.js.org/公司用 若依 搞了一个速成项目&#xff0c;若依是一个免费开源的前后端项目&#xff0c;感兴趣的朋友百度。 在里边接触到了一个神奇的东西 &#xff1a;vue-treeselect&#xff0c;用起来真的是一言难尽&am…

uniapp全局弹窗(APP端)

uniapp自带的提示框不符合我们的要求&#xff0c;需要自己写一个提示框&#xff0c;且全局通用。 解决思路: 使用 plus.nativeObj 来绘制窗口以及事件监听。 官方文档 1. 首先创建一个整个屏幕的控件&#xff0c;作为一个父容器。 此时还看不到任何东西 let screenHeight…

牛客网前端刷题(二)

还在担心面试不通过吗?给大家推荐一个超级好用的刷面试题神器:牛客网,里面涵盖了各个领域的面试题库,还有大厂真题哦! 赶快悄悄的努力起来吧,不苒在这里衷心祝愿各位大佬都能顺利通过面试。 面试专栏分享,感觉有用的小伙伴可以点个订阅,不定时更新相关面试题:面试专栏…

微信小程序之开发遇到 does not have a method “xxxx“ to handle event “tap“ 问题的解决方案【已解决】

今天在开发一个小功能&#xff0c;copy了之前写的代码&#xff0c;但是在实现功能时&#xff0c;出现了如下问题&#xff1a; 先在这简单总结一下解决方案&#xff1a; 在调用方法时&#xff0c;在" "中前后多加了空格&#xff1b;在 js 中没有定义该方法&#xff1…

纯前端导出表格

前端 excel 表格导出 我们习惯了后端去处理表格&#xff0c;直接接口返回 &#xff0c;那前端如何轻松的导出表格呢&#xff1f; 文章目录前端 excel 表格导出Ⅰ. 通过 js-xlsx ⭐⭐⭐⭐⭐安装① vue2 中使用② vue3 中使用③ react 中使用Ⅲ. 通过 vue-json-excel ⭐⭐安装使…

SpringBoot--Filter过滤器(一)

一.了解过滤器Filter 过滤器: Filter, 是 Servlet 技术中最实用的技术。过滤器是处于客户端和服务器之间的一个过滤网,可以过滤掉一些不符合要求的请求常见场景: Session 校验判断用户权限不符合设定条件&#xff0c;则会被重定向特殊的地址或者设定的响应过滤敏感词汇设置编…

一天撸一个财务APP系统【安卓端+前端+后端】

昨天有个粉丝朋友也想学开发Web和小程序、安卓&#xff0c;问可以在大学学会吗&#xff1f; 在学校学到的东西真的有限&#xff1a; 在很多的高校&#xff0c;有一些教授是学院派的&#xff0c;他们没有做过多少开发工作&#xff0c;上课就是照本宣科&#xff0c;讲的知识点都…

Vue打包优化篇-CDN加速

优化原因 在没有使用cdn加速之前打包后数据如下&#xff0c;可以看出element-ui、vue、vuex、vue-router这些依赖都打进chunk-vendors.js中导致体积很大&#xff0c;假设再来很多依赖项是不是更大&#xff0c;同时也会影响单页面应用首屏加载速度&#xff0c;所以这里采用一种打…

Vue的组件化编程

非单文件组件 注册局部组件 此时上面书写的组件都是局部组件,每一个vue实例要想使用上面的组件时都需要在components中进行注册才可以使用,此时如果再创建一个Vue实例vms,这个实例不在components中注册就直接使用组件会产生什么变化: 此时控制台报错 ‘是否正确注册了组件&a…

function 函数

一、函数的基本注意事项 function函数的名字也是一个标识符&#xff0c;通过关键字function申明一个函数 function 函数名(){ 代码块 } 二、函数基本用法 1、形参与实参 1、形参与实参可以有无数个&#xff0c;实参按照顺序赋值给形参&#xff1b; 2、实参个数不一定要与实…

web前端面试高频考点——Vue原理(理解MVVM模型、深度/监听data变化、监听数组变化、深入了解虚拟DOM)

系列文章目录 内容参考链接Vue基本使用Vue的基本使用&#xff08;一文掌握Vue最基础的知识点&#xff09;Vue通信和高级特性Vue组件间的通信及高级特性&#xff08;多种组件间的通信、自定义v-model、nextTick、插槽&#xff09;Vue高级特性Vue的高级特性&#xff08;动态组件…

axios发post请求,后端接收不到参数的原因及解决

做分页数据时&#xff0c;使用vue3 axios发post请求&#xff0c;后端服务调用成功但数据没有根据参数动态变化&#xff0c;换为js ajax也是同样错误。 后来是后端没有接收到参数。 网上看了资料&#xff0c;原因&#xff1a; 使⽤axios请求数据时&#xff0c;我们的 …

Module Federation在vue3中使用vue2的组件

前言&#xff1a; 备注&#xff1a;本文基于对webpack Module Federation有一定了解的情况下 一般情况下使用模块联邦都是会使用相同的版本&#xff0c;如Vue2的组件时在Vue2中使用&#xff0c;但我为什么会在Vue3项目中去使用Vue2的组件呢&#xff0c;其实是因为历史原因。好…

点击《el-table》让选中的行变色,亲测实用

前期回顾 Vue项目实战 —— 哔哩哔哩移动端开发_0.活在风浪里的博客-CSDN博客撑着下班前半小时我用vue写《哔哩哔哩 项目》移动端、新手还在哭、老鸟一直在笑。。。技术选型Vue2&#xff0c;技术栈有axios、Vh等&#xff0c;下班过来敲哈哈https://blog.csdn.net/m0_5790…

vite+vue3+ts 手把手教你创建一个vue3项目

使用vite 创建一个vue3项目&#xff0c;基本全程cv。 安装router&#xff0c;less / scss&#xff0c;pinia&#xff0c;axios&#xff0c; element / Ant Design Vue&#xff0c;Echarts 以及如何配置别名&#xff0c;自定义组件&#xff0c;vue-devtools插件。 目录 1、创建…

vue设置cookie和路由守卫

vue项目中登录页面用户登录成功后&#xff0c;会把用户信息存储到cookie中&#xff0c;然后跳转进入首页&#xff0c;当用户没有登录时&#xff0c;直接输入页面地址会经过路由守卫检测cookie中是否存在用户信息&#xff0c;如果不存在&#xff0c;重定向到登录页让用户进行登录…

使用idea运行VUE项目

1.电脑先安装好node.js并配置好环境变量&#xff0c; 安装方法参考windows mac 2.使用管理员身份运行idea&#xff0c;把项目拉取下来&#xff0c; 并给idea安装vue的插件 安装方法参考链接 3.删除“node_modules”文件夹和“package-lock.json” 将项目里的“node_modules”…

简单的Java web项目代码(8个)

引言&#xff1a;Java web项目主要采用mvc的的设计思想&#xff0c;系统主要采用javajspservletmysqleclipse实现&#xff0c;具有登陆、分页、导出excel&#xff0c;增删改查等功能,适合初学者&#xff0c;满足基本的实训需求&#xff0c;以下是推荐的几款&#xff0c;总有适合…

uniapp及uniCloud开发中经常会出现的问题汇总

一、manifest.json内未配置Appld,请重新获取后再 uniCloud本地调试服务启动失败&#xff0c;错误信息为:manifest.json内未配置Appld,请重新获取后再试 新建uniapp项目会出现以下报错&#xff0c;说明还没有创建APPID。 解决办法&#xff1a; 打开DCloud开发者中心&#xff…

Vite基础配置之 - 分包

Vite基础配置之 - 分包 什么是分包呢&#xff1f;它有什么好处呢&#xff1f; 还是举个例子&#xff0c;来回忆一下吧&#xff0c;什么呢&#xff1f; 比如说&#xff0c;我使用了 loadsh.js 的东西&#xff0c;那么我们在打包的时候&#xff0c;就会把 loadsh 相关资源也会打…