Redux 最佳实践 Redux Toolkit 🔥🔥

47,836 阅读9分钟

岁月悠悠,道路险阻,我们总是用顺其自然来敷衍人生道路上面的荆棘坎坷,却很少承认,真正的顺其自然是竭尽全部力量后的不强求,而并非是两手一摊,只有抱怨和埋怨的不作为。

前言

使用过 Vuex 再来使用 Redux 我自己的感觉就是 Redux 的写法太复杂、太分散了,不像 Vuex 在一个文件里聚合所有东西。但现在 Redux 官方推出了 Redux Toolkit,从此 Redux 写起来也能很爽了。

Redux 是什么?

Redux 是一个使用叫做 “action” 的事件来管理和更新应用状态的模式和工具库 它以集中式 Store 的方式对整个应用中使用的状态进行集中管理,确保状态只能以可预测的方式更新。

Redux Toolkit 是什么?

Redux Toolkit 是官方推荐的编写 Redux 逻辑的方法。 它包含我们对于构建 Redux 应用程序必不可少的包和函数。 Redux Toolkit 的构建简化了大多数 Redux 任务,防止了常见错误,并使编写 Redux 应用程序变得更加容易。可以说 Redux Toolkit 就是目前 Redux 的最佳实践方式。

为了方便后面内容,之后 Redux Toolkit 简称 RTK

从零开始搭一个 RTK

学习的最佳方法我个人觉得还是看官方文档比较权威: 中文官方文档英文官方文档

在官方文档中其实提供了完整的 RTK 项目创建命令,但咱们学习就从基础的搭建开始吧。
那么想手动搭建一个 RTK 项目改如何做呢?

启动一个react项目

这里直接创建一个react项目,然后我们再开始唠如何使用 RTK

yarn create react-app my-redux-toolkit
cd my-redux-toolkit

安装RTK相关包和开发工具

创建完项目以后我们开始安装 RTK 相关的东西

// 安装 @reduxjs/toolkit 和 react-redux
yarn add @reduxjs/toolkit react-redux

为了方便我们开发,这里推荐安装一下 redux 的开发工具:redux-devtools,需要注意的是在 chrome 浏览器中我们也需要安装对应的插件:redux-devtools 来结合使用。

// 安装 redux 开发工具 redux-devtools
yarn add redux-devtools -D

基础开发流程

安装完相关包以后开始编写基本的 RTK 程序

  • 创建一个store文件夹
  • 创建一个index.ts做为主入口
  • 创建一个festures文件夹用来装所有的store
  • 创建一个counterSlice.ts文件,并导出简单的加减方法

内容结构如下图:

image.png

相关代码如下:

// counterSlice.ts 文件

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

export interface CounterState {
  value: number;
  title: string
}
const initialState: CounterState = {
  value: 0,
  title: "redux toolkit pre"
};

// 创建一个 Slice 
export const counterSlice = createSlice({
  name: 'counter',
  initialState,
  // 定义 reducers 并生成关联的操作
  reducers: {
    // 定义一个加的方法
    increment: (state) => {
      state.value += 1;
    },
    // 定义一个减的方法
    decrement: (state) => {
      state.value -= 1;
    },
  },
});
// 导出加减的方法
export const { increment, decrement } = counterSlice.actions;

// 默认导出
export default counterSlice.reducer;

// index.ts 文件

import { configureStore } from "@reduxjs/toolkit";
import counterSlice from "./features/counterSlice.ts";

// configureStore创建一个redux数据
const store = configureStore({
  // 合并多个Slice
  reducer: {
    counter: counterSlice
  },
});

export default store;


基本代码开发完以后,我们需要看看应用到页面中是否OK,先把store加到全局,如下图:

image.png

对应的代码如下:

// index.ts 文件

import React from 'react';
import ReactDOM from 'react-dom/client';
import './index.css';
import App from './App';
import reportWebVitals from './reportWebVitals';

// redux toolkit
import {Provider} from 'react-redux';
import store from './store/index.ts';

const root = ReactDOM.createRoot(document.getElementById('root'));
root.render(
  <React.StrictMode>
    <Provider store={store}>
      <App />
    </Provider>
</React.StrictMode>
);
reportWebVitals();

添加到全局以后我们如何使用呢?如下图:

image.png

相关代码如下:

// App.js 文件


// 引入相关的hooks
import {useSelector, useDispatch} from 'react-redux';
// 引入对应的方法
import {increment, decrement} from './store/features/counterSlice.ts';

import logo from './logo.svg';
import './App.css';

function App() {
  // 通过useSelector直接拿到store中定义的value
  const {value} = useSelector((store)=>store.counter)
  // 通过useDispatch 派发事件
  const dispatch = useDispatch()
  return (
    <div className="App">
      <header className="App-header">
        <img src={logo} className="App-logo" alt="logo" />
        {/* 页面中应用的代码 */}
        <p>{value}</p>
        <button onClick={()=>{dispatch(increment())}}>加</button>
        <button onClick={()=>{dispatch(decrement())}}>减</button>
      </header>
    </div>
  );
}

export default App;

到此就完成了 RTK 的使用,看一下效果:

12.gif

如何传参?

上面的项目中固定的加一减一,那如果我们想加多少就能动态加多少,那就需要传参。那如何传参呢?
redux 的传参一样,如下图: image.png

相关代码为:

// App.js 文件

import {useState} from 'react';
// 引入相关的hooks
import {useSelector, useDispatch} from 'react-redux';
// 引入对应的方法
import {increment, decrement} from './store/features/counterSlice.ts';


import logo from './logo.svg';
import './App.css';

function App() {
  // 通过useSelector直接拿到store中定义的value
  const {value} = useSelector((store)=>store.counter)
  // 通过useDispatch 派发事件
  const dispatch = useDispatch()
  // 变量
  const [amount, setAmount] = useState(1);
  return (
    <div className="App">
      <header className="App-header">
        <img src={logo} className="App-logo" alt="logo" />
        {/* 页面中应用的代码 */}
        <p>{value}</p>
        <input value={amount} onChange={(e) => setAmount(+e.target.value)}/>
        <button onClick={()=>{dispatch(increment({value: amount}))}}>加</button>
        <button onClick={()=>{dispatch(decrement())}}>减</button>
      </header>
    </div>
  );
}

export default App;

如何接收参数?

接收参数的方式也和 Redux 一样,我们可以通过 action 来接收参数,如下图:

image.png

相关代码如下:

// counterSlice.ts 文件

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

export interface CounterState {
  value: number;
  title: string
}
const initialState: CounterState = {
  value: 0,
  title: "redux toolkit pre"
};

// 创建一个 Slice 
export const counterSlice = createSlice({
  name: 'counter',
  initialState,
  // 定义 reducers 并生成关联的操作
  reducers: {
    // 定义一个加的方法
    increment: (state, {payload}) => {
      // action 里面有 type 和 payload 两个属性,所有的传参都在payload里面
      state.value += payload.value;
    },
    // 定义一个减的方法
    decrement: (state) => {
      state.value -= 1;
    },
  },
});
// 导出加减的方法
export const { increment, decrement } = counterSlice.actions;

// 默认导出
export default counterSlice.reducer;

写完了那就效果如下:

2.gif

如何实现一个异步请求?

异步请求在我们的项目中时必不可少的,那如何实现一个异步请求呢?
这里我“偷”了一个电影列表接口,咱们重新开一个 slice,看一下多个模块的 RTK 是如何实现的。如下图:

image.png

相关代码为:

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

export interface MovieState {
  list: object;
  totals: number
}
const initialState: MovieState = {
  list: [],
  totals: 0
};

// 请求电影列表
const getMovieListApi = ()=> 
  fetch(
    'https://pcw-api.iqiyi.com/search/recommend/list?channel_id=1&data_type=1&mode=24&page_id=1&ret_num=48'
  ).then(res => res.json())

// thunk函数允许执行异步逻辑, 通常用于发出异步请求。
// createAsyncThunk 创建一个异步action,方法触发的时候会有三种状态:
// pending(进行中)、fulfilled(成功)、rejected(失败)
export const getMovieData = createAsyncThunk( 'movie/getMovie', 
  async () => {
    const res= await getMovieListApi();
    return res;
  }
);

// 创建一个 Slice 
export const movieSlice = createSlice({
  name: 'movie',
  initialState,
  reducers: {
    // 数据请求完触发
    loadDataEnd: (state, {payload}) => {
      state.list = payload;
      state.totals = payload.length;
    },
  },
  // extraReducers 字段让 slice 处理在别处定义的 actions, 
  // 包括由 createAsyncThunk 或其他slice生成的actions。
  extraReducers(builder) {
    builder
    .addCase(getMovieData.pending, (state) => {
      console.log("🚀 ~ 进行中!")
    })
    .addCase(getMovieData.fulfilled, (state, {payload}) => {
      console.log("🚀 ~ fulfilled", payload);
      state.list = payload.data.list
      state.totals = payload.data.list.length
    })
    .addCase(getMovieData.rejected, (state, err) => {
      console.log("🚀 ~ rejected", err)
    });
  },
});

// 导出方法
export const { loadDataEnd } = movieSlice.actions;

// 默认导出
export default movieSlice.reducer;


然后在主入口引入:

import { configureStore } from "@reduxjs/toolkit";
import counterSlice from "./features/counterSlice.ts";
import movieSlice from "./features/movieSlice.ts";

// configureStore创建一个redux数据
const store = configureStore({
  // 合并多个Slice
  reducer: {
    counter: counterSlice,
    movie: movieSlice
  },
});

export default store;

应用也是一样的,如下图:

image.png

相关代码为:

import {useState} from 'react';
// 引入相关的hooks
import {useSelector, useDispatch} from 'react-redux';
// 引入对应的方法
import {increment, decrement, asyncIncrement} from './store/features/counterSlice.ts';
import {getMovieData} from './store/features/movieSlice.ts';
import logo from './logo.svg';
import './App.css';
function App() {
  // 通过useSelector直接拿到store中定义的value
  const {value} = useSelector((store)=>store.counter)
  // 通过useSelector直接拿到store中定义的list
  const {list} = useSelector((store)=>store.movie)
  // 通过useDispatch 派发事件
  const dispatch = useDispatch()
  // 变量
  const [amount, setAmount] = useState(1);
  return (
    <div className="App">
      <header className="App-header">
        <img src={logo} className="App-logo" alt="logo" />
        {/* 页面中应用的代码 */}
        <p>{value}</p>
        <input value={amount} onChange={(e) => setAmount(+e.target.value)}/>
        <button onClick={()=>{dispatch(increment({value: amount}))}}>加</button>
        <button onClick={()=>{dispatch(asyncIncrement())}}>异步加</button>
        <button onClick={()=>{dispatch(decrement())}}>减</button>
        <button onClick={()=>{dispatch(getMovieData())}}>获取数据</button>
        <ul>
          {
            list.map((item)=>{ return <li key={item.tvId}> {item.name}</li> })
          }
        </ul>
      </header>
    </div>
  );
}

export default App;

看一下具体的效果:

6.gif

createAsyncThunk

createAsyncThunk 可以创建一个异步action,通常用于发出异步请求。方法触发的时候会有三种状态:pending(进行中)、fulfilled(成功)、rejected(失败)

extraReducers

extraReducers 可以让 slice 处理在别处定义的 actions,包括由 createAsyncThunk 或其他 slice 生成的 actions
刚才的代码里咱们处理的是 createAsyncThunk,接下来看一下如何处理其他 slice 生成的 actions
直接看一下 counterSlice.ts 中的 increment 方法如何处理吧,如下图:

image.png

相关代码为:

import { createSlice, createAsyncThunk } from '@reduxjs/toolkit';
import { increment } from './counterSlice';

export interface MovieState {
  list:Array<any>;
  totals: number
}
const initialState: MovieState = {
  list: [],
  totals: 0
};

// 请求电影列表
const getMovieListApi = ()=> 
  fetch(
    'https://pcw-api.iqiyi.com/search/recommend/list?channel_id=1&data_type=1&mode=24&page_id=1&ret_num=48'
  ).then(res => res.json())

// thunk函数允许执行异步逻辑, 通常用于发出异步请求。
// createAsyncThunk 创建一个异步action,方法触发的时候会有三种状态:
// pending(进行中)、fulfilled(成功)、rejected(失败)
export const getMovieData = createAsyncThunk( 'movie/getMovie', 
  async () => {
    const res= await getMovieListApi();
    return res;
  }
);

// 创建一个 Slice 
export const movieSlice = createSlice({
  name: 'movie',
  initialState,
  reducers: {
    // 数据请求完触发
    loadDataEnd: (state, {payload}) => {
      state.list = payload;
      state.totals = payload.length;
    },
  },
  // extraReducers 字段让 slice 处理在别处定义的 actions, 
  // 包括由 createAsyncThunk 或其他slice生成的actions。
  extraReducers(builder) {
    builder
    .addCase(increment, (state, {payload})=>{
      // increment方法触发时的处理
      state.list.push(payload.value)
      state.totals = state.list.length
    })
    .addCase(getMovieData.pending, (state) => {
      console.log("🚀 ~ 进行中!")
    })
    .addCase(getMovieData.fulfilled, (state, {payload}) => {
      console.log("🚀 ~ fulfilled", payload);
      state.list = payload.data.list
      state.totals = payload.data.list.length
    })
    .addCase(getMovieData.rejected, (state, err) => {
      console.log("🚀 ~ rejected", err)
    });
  },
});

// 导出方法
export const { loadDataEnd } = movieSlice.actions;

// 默认导出
export default movieSlice.reducer;

完事看一下具体的效果:

2387654.gif

总结一下

RTK 的实现使我们对 Redux 的实现更加容易,说是目前最佳实践也不为过。整体总结一下:

// 引入 RTK
import { createSlice } from '@reduxjs/toolkit';

// thunk函数允许执行异步逻辑, 通常用于发出异步请求。
// createAsyncThunk 创建一个异步action,方法触发的时候会有三种状态:
// pending(进行中)、fulfilled(成功)、rejected(失败)
export const getMovieData = createAsyncThunk();

// 创建一个 Slice(切片)
export const counterSlice = createSlice({
  // 命名空间,name会作为action type的前缀
  name: 'counter',
  
  // 初始化状态
  initialState: {},
  
  // 1、定义reducer更新状态的函数
  // 2、组件中dispatch使用的action函数
  reducers: {
    xxx: (state, action) => {},
  },
  // extraReducers 字段让 slice 可以处理在别处定义的 actions, 
  // 包括由 createAsyncThunk或其他slice生成的actions。
  extraReducers() {},
});

// 导出action函数
export const { xxx } = counterSlice.actions;

// 导出reducer,创建store
export default counterSlice.reducer;

直接创建一个RTK

在我门熟悉如何使用 RTK 之后咱们再来看看官方提供一键生成的 RTK 应用。
官方推荐的创建 React Redux 新应用的方式有两种,都是基于 Create React App,它利用了 Redux Toolkit 和 Redux 与 React 组件的集成.分别是:

# Redux + JS 模版
npx create-react-app my-app --template redux

# Redux + TS 模版
npx create-react-app my-app --template redux-typescript

个人觉得react和ts比较搭,这里就用 Redux + TS 模版 来创建一个看看。还有个人比较喜欢 yarn,所以这里用的是 yarn 的方式:

// 安装
yarn create react-app redux-toolkit --template redux-typescript
cd redux-toolkit

安装完成后 可以看到基本我们需要的一些包就都有了,如下图:

image.png

看一下项目结构我们发现,相对于正常的 react 项目我们在 src 里面多了 appfeatures 两个文件夹。如下图:

image.png

点开文件夹可以看到里面的结构,app 中其实就是两个入口文件,一个是 hooks 的,一个是 store 的。他们其实就是咱们自己搭建的 RKT 项目下的 index.ts。 如下图:

image.png

同样的 点开 features 文件以后可以看到里面包了一个文件夹 counter,这个其实可以理解成对不同的 slice 进行分组,目前官方的例子是一个计数器,所以分了一个 counter 文件夹。
解析一下 counter 文件夹里面的代码:

  • counterSlice.ts 计数器slice的核心代码,也就是RTK的实现
  • counterAPI.ts 其实就是把异步请求单独提出来放在一起
  • Counter.tsx 是计数器的视图文件,可以理解为html部分
  • Counter.module.css 是计数器的样式,也就是css部分
  • counterSlice.spec.ts 对应生成的单元测试文件

具体结构如下图:

image.png

启动看一下:

// 启动项目
yarn start

效果如下图:

1111.gif

到此整个 RTK 的使用就介绍完了,整体只属于一个基础讲解,如果项目中需要使用 RTK 的话,还需要仔细看一下官方文档。

若有帮助记得三连哦!🙏🙏🙏