likes
comments
collection
share

如何整合 React redux-toolkit 和 Redux Saga

作者站长头像
站长
· 阅读数 10

如何在 React 应用程序中实现整合Redux Saga和redux-toolkit。

开发 React 应用程序时,您开始问自己要使用哪些功能。但是,当谈到“存储数据”时,甚至是“Context API”或“Redux”。

所以,你应该问自己一些问题......

  • 我是否需要使用 API?
  • 我是否使用了太多来自这个 API 的数据?
  • 这个 API 中是否有太多的实体?

如果你的答案是“yes”,那么你将使用“Redux”。否则,你将使用“ Context API ”。

注意:当你都“是”时,你可以使用 Context API,并且你可以使用 React 提供的“ useReducer ”钩子,但我认为它不会当应用程序变得更大时,可以像 Redux 结构一样组织起来。

1 — 概述

A — 什么是 Redux?

Redux 是 JavaScript 应用程序的状态管理库,通常与 React 一起使用。它提供了一个可预测的状态容器,允许在单个集中位置轻松管理应用程序状态。 Redux 基于不变性和单向数据流的原则,这使得更容易推断应用程序状态随时间的变化。使用 Redux,会分派操作来更新状态,这些操作由称为“reducer”的纯函数处理,该函数根据操作类型和有效负载返回新状态。 Redux 还提供用于处理异步操作和其他高级用例的中间件。总体而言,Redux 有助于简化复杂应用程序状态的管理,并使构建可扩展、可维护的应用程序变得更加容易。

因此……我们正在使用 Redux Toolkit。

B—什么是 Redux Toolkit?

Redux Toolkit 包括一个预配置的 Redux store,其中包括必要的中间件和reducer设置,以及对 DevTools 的支持。它还包括一组用于创建 Redux 切片的强大实用程序,这些切片是小型、独立的 Redux 状态和逻辑模块。使用切片,您可以用更简洁直观的语法定义reducer函数和操作创建器。总体而言,Redux Toolkit 提供了一种精简且自以为是的 Redux 使用方式,可以帮助开发人员以更少的精力编写出更好、更易于维护的 Redux 代码。

C — 异步操作

管理 Redux 状态是一项简单的任务,但应在 Redux 结构中考虑使用 REST API。

Redux Toolkit 还提供了一组用于处理异步代码的实用函数,包括用于定义 Redux thunk 的 createAsyncThunk 和用于管理规范化数据的 createEntityAdapter。

D——Redux Thunk VS. Redux Saga

Redux Thunk 和 Redux Saga 都是 Redux 的中间件库,提供了一种管理 Redux 应用程序中异步逻辑的方法。

  • Redux Thunk 是一个简单的中间件库,它允许您编写返回函数而不是 action 对象的 action 创建器。然后,此函数可以执行异步操作,如 API 调用,并在操作完成后分派操作。该函数还可以根据操作的结果分派其他操作。这种方法学起来很简单,可以用于大多数常见的用例。ReduxThunk 易于设置,可以使用最少的配置。
  • ReduxSaga 是一个更强大、更灵活的中间件库,用于管理 Redux 中的异步操作。它允许您将复杂的异步逻辑定义为一系列步骤,称为传奇。传说可用于处理更复杂的场景,如竞态条件、重试和取消。Redux Saga 使用生成器函数来定义 Saga,它允许以更易读和更直观的方式表达更复杂的逻辑。然而,与 ReduxThunk 相比,它可能更难学习和有效使用。

总体而言,Redux Thunk 是一个更简单易用的中间件库,用于管理 Redux 中的异步操作,而 Redux Saga 则提供了更强大、更灵活的方法来处理更复杂的场景。它们之间的选择很大程度上取决于应用程序的复杂性和需要处理的特定用例。

如前所述,在本文中,我一步一步展示如何整合 React redux-toolkit 和 Redux Saga。所有 Saga 使用的函数都将以简单的方式进行解释。

注意:我将提供 Typescript 代码片段。如果您使用的是 Javascript,只需删除类型即可。

2 — 开始编码

A — 创建 React 项目

让我们创建一个名为 redux-with-saga 的 React 项目作为示例:

# Javascript project  
$ npx create-react-app redux-with-saga  
  
# Typescript project  
$ npx create-react-app redux-with-saga --template typescript

B — 添加包

$ npm install react-redux @reduxjs/toolkit redux-saga  
# Or  
$ yarn add react-redux @reduxjs/toolkit redux-saga

C — 配置 Redux store

包安装完成后,在 src/ 文件夹中,创建一个新文件夹“ store ”或“ redux ”或任何涉及 redux 实现的文件夹:

在“ redux ”文件夹中,创建一个 index.ts/js 文件进行配置redux:

import createSagaMiddleware from "@redux-saga/core";  
import { configureStore } from "@reduxjs/toolkit";  
  
const sagaMiddleware = createSagaMiddleware();  
  
const store = configureStore({  
reducer: {},  
middleware: [sagaMiddleware],  
});  
  
export default store;

说明:

CreateSagaMiddleware ー创建一个 Redux 中间件实例并将 Sagas 连接到 Redux store。 ConfigureStore ー@reduxjs/toolkit 包提供的一个函数,它简化了创建具有合理默认值和内置中间件的 Redux store的过程。

reducer key 是一个对象,包含组合在单个全局reducer(对象)中的所有应用程序reducer。现在我们正在传递一个空对象,因为我们还没有reducer。

middleware表示将在 redux 配置中使用的中间件列表。

最后,我们导出 store 。

D — 在我们的 React 应用程序中使用 store 配置

import React from 'react';  
import ReactDOM from 'react-dom/client';  
import { Provider } from 'react-redux';  
import App from './App';  
import store from './redux';  
  
const root = ReactDOM.createRoot(  
document.getElementById('root') as HTMLElement  
);  
root.render(  
<React.StrictMode>  
<Provider store={store}>  
<App />  
</Provider>  
</React.StrictMode>  
);

组件 Provider 是从 react-redux 包导出的 React 元素,它将 App 组件作为子组件,并将存储配置作为名为 store 的 prop。

这个想法是应用程序内的每个组件都可以访问store。但是……如果我们将 Provider 元素移动到 App 组件内会怎样?在这种情况下,除了App组件之外,所有组件都可以使用redux store。因为它不被视为 Provider 组件的“子组件”之一。就这么简单…

E — 创建root reducer

一般来说,我们已经完成了 redux store的配置,但是…我们不希望我们的 redux/index.ts/js 随着添加更多reducer而变得更大。所以......我们正在创建一个名为 redux/ root -reducer.ts/js 的单独文件:

import userReducer from "./users/slice";  
  
export type StateType = {  
// Reducers types here  
};  
  
const rootReducers = {  
// Reducers here  
};  
  
export default rootReducers;

创建一个单独的文件包含rootReducer,它是一个对象,将我们所有的应用程序的reducer组合在一个reducer中,被认为是保持代码干净清晰的最佳实践。

StateType 是一个 Typescript 类型,代表我们的全局状态的类型。

我们回到我们的index.ts/js文件并将根reducer传递给配置中的reducer属性:

import createSagaMiddleware from "@redux-saga/core";  
import { configureStore } from "@reduxjs/toolkit";  
import rootReducers from "./root-reducers";  
  
const sagaMiddleware = createSagaMiddleware();  
  
const store = configureStore({  
// Update the line below  
reducer: rootReducers,  
middleware: [sagaMiddleware],  
});  
  
export default store;

F - 创建slice

假设我们正在使用一些 REST API 的用户端点。为此,我们创建了 3 个文件,因为我总是想保持内容干净。

  • types.ts/js
  • slice.ts/js
  • sagas.ts/js

types文件

// Define the user type  
export type UserType = {  
id: string;  
name: string;  
lastname: string;  
email: string;  
active: boolean;  
createdAt: Date;  
updatedAt: Date;  
}  
  
// This type will represent the sub-state for getting a single user by ID  
export type IUserState = {  
data: UserType | null;  
isLoading: boolean;  
errors: string;  
}  
  
// The users global state  
export type UsersStateType = {  
user: IUserState,  
// Later, we can add other sub-states like:  
// list,  
// create,  
// update,  
// remove  
}  
  
// (1)  
export const USERS = "users";  
export type USERS = typeof USERS; // Typescript line  
  
// (2)  
export const GET_USER_BY_ID = `${USERS}/getUserByAction`;  
export type GET_USER_BY_ID = typeof GET_USER_BY_ID; // Typescript line

(1) — 创建一个采用“users”文本的 USERS 常量,然后创建一个与 USERS const 名称相同的类型,并为其指定 const USERS 的类型。这将告诉typescript:任何使用 USERS 类型键入的内容都应该只接受值“users”。如果我们分配文本“users2”,typescript将向您显示一个错误,该变量仅接受“users”作为值。

随着项目变得越来越大,这种技术应该使您的编码更安全,并且您也可能会错误输入单词“users” ” 就像“usres”。所以,typescript 会给你一个提示。

与 (2) 相同 - GET_USER_BY_ID

注意:在 TypeScript 中,typeof 可以用作类型运算符,以将值或变量的类型提取为类型。

slice文件

import { createSlice, PayloadAction } from "@reduxjs/toolkit";  
import { USERS, UsersStateType, UserType } from "./types";  
  
const usersInitialState: UsersStateType = {  
user: {  
data: null,  
isLoading: false,  
errors: '',  
}  
}  
  
export const usersSlice = createSlice({  
name: USERS,  
initialState: usersInitialState,  
reducers: {  
/* This action will trigger our saga middleware  
and set the loader to true and reset error message.  
*/  
getUserAction: (state: UsersStateType, { payload: id }: PayloadAction<string>) => {  
state.user.isLoading = true;  
state.user.errors = '';  
},  
getUserSuccessAction: (state: UsersStateType, { payload: user }: PayloadAction<UserType>) => {  
state.user.isLoading = false;  
state.user.data = user;  
},  
getUserErrorAction: (state: UsersStateType, { payload: error }: PayloadAction<string>) => {  
state.user.isLoading = false;  
state.user.errors = error;  
},  
}  
}  
  
/* getUserSuccessAction and getUserErrorAction will be used inside the saga  
middleware. Only getUserAction will be used in a React component.  
*/  
  
export {  
getUserAction,  
getUserSuccessAction,  
getUserErrorAction  
} = usersSlice.actions;  
export default usersSlice.reducer;

导出userReducer后,我们应该将其添加到我们的 root-reducer 文件中并更新状态类型:


import userReducer from "./users/slice";  
import { UsersStateType } from "./users/types";  
import usersReducer from "./users/slice";  
  
export type StateType = {  
users: UsersStateType;  
};  
  
const rootReducers = {  
users: usersReducer,  
};  
  
export default rootReducers;

saga文件

import { PayloadAction } from "@reduxjs/toolkit";  
import { AxiosResponse } from "axios";  
import { put, takeLatest } from "redux-saga/effects";  
import { UserType, GET_USER_BY_ID } from "./types";  
import { getUserErrorAction, getUserSuccessAction } from "./slice";  
  
// Generator function  
function* getUserSaga({ payload: id }: PayloadAction<string>) {  
try {  
// You can also export the axios call as a function.  
const response: AxiosResponse<UserType> = yield axios.get(`your-server-url:port/api/users/${id}`);  
yield put(getUserSuccessAction(response.data));  
} catch (error) {  
yield put(getUserErrorAction(error));  
}  
}  
  
// Generator function  
export function* watchGetUser() {  
yield takeLatest(GET_USER_BY_ID, getUserSaga);  
}

什么是Generator function?

Generator function(生成器函数)是使用 function* 语法定义的,这与常规函数语法不同。在生成器函数中,yield 关键字用于暂停执行并向调用者返回一个值。当生成器函数恢复时,它会从中断处继续执行。

这是一个简单生成器函数的示例:

function* myGenerator() {  
yield 1;  
yield 2;  
yield 3;  
}  
  
const gen = myGenerator();  
  
console.log(gen.next()); // { value: 1, done: false }  
console.log(gen.next()); // { value: 2, done: false }  
console.log(gen.next()); // { value: 3, done: false }  
console.log(gen.next()); // { value: undefined, done: true }

此处阅读更多有关生成器函数的信息。

yield——yield 关键字是 Redux Saga 的基本组成部分,因为它允许 Saga 使用的生成器函数暂停和恢复执行以响应分派的操作。当生成器函数遇到yield 语句时,它会返回一个迭代器,可用于检索生成的值。然后该值可以由 Saga 中间件的其他部分处理。

在 Redux Saga 中,yield 通常与其他函数结合使用,例如 call 、 put 和 select 。例如,call 函数用于调用异步函数并返回其结果。

put — put 函数用于将操作分派到 Redux store。

takeLetst — 在 Redux 中,takeLatest 是 redux-saga 中间件提供的一个函数,它允许开发人员以更受控的方式处理异步操作。当一个操作被分派到 Redux store时,takeLatest 将取消任何之前正在进行的任务并只运行最新的任务。

要使用 takeLatest ,需要定义另一个生成器函数来侦听特定操作。

在上面的代码中, watchGetUser 是一个生成器函数,它使用 takeLatest 监听 GET_USER_BY_ID 操作。当调度此操作时,将调用 getUserSaga 生成器函数。 getUserSaga 调用 API 并使用 put 效果分派 getUserSuccessAction 或 getUserErrorAction 操作,具体取决于 API 调用是成功还是失败。

使用 takeLatest 的好处是它可以确保只处理最新的请求,即使以前的请求仍在处理中。这可以帮助避免竞争条件并确保应用程序状态始终与最新数据同步。

现在我们可以将 watchGetUser 生成器函数添加到 redux 进程中,因此......我们将在与 root -reducer 文件相同的级别创建一个名为 root -sagas.ts/js 的新文件:

import { all, fork } from "redux-saga/effects";  
import { watchGetUser } from "./users/sagas";  
  
const rootSaga = function* () {  
yield all([  
fork(watchGetUser),  
// Other forks  
]);  
};  
  
export default rootSaga;

导出 rootSaga 后,我们应该将其添加到 redux 配置文件中。所以,在我们的 index.ts/js 中:

import createSagaMiddleware from "@redux-saga/core";  
import { configureStore } from "@reduxjs/toolkit";  
import rootReducers from "./root-reducers";  
// Add the import  
import rootSaga from "./root-sagas";  
  
const sagaMiddleware = createSagaMiddleware();  
  
const store = configureStore({  
reducer: rootReducers,  
middleware: [sagaMiddleware],  
});  
  
// Added line  
sagaMiddleware.run(rootSaga);  
  
export default store;

回到 root -sagas 文件,一直到索引文件中所做的更新:

all / fork — 在 Redux Saga 中,fork 和所有函数用于创建和管理并发 Sagas。

fork 用于创建与父 Saga 并发运行的新子 Saga。

all 函数用于并行运行多个 Sagas,并在继续执行父 Saga 之前等待所有 Saga 完成。

如果我们有另一个类似的saga:

import { all, fork } from "redux-saga/effects";  
import { watchGetUser } from "./users/sagas";  
  
const rootSaga = function* () {  
yield all([  
fork(watchGetUser),  
// Considering we have the watcher below  
// fork(watchGetUsersList),  
]);  
};  
  
export default rootSaga;

watchGetUser 和 watchGetUsersList 是两个使用 all 启动的 Sagas。 all 函数会等待两个 Sagas 完成,然后再继续执行父 Saga rootSaga 。

通过使用 fork 和 all ,我们可以在 Redux Saga 中创建和管理并发 Sagas。这有助于提高应用程序的响应速度和效率,因为可以并行执行多个任务。

run — 在 Redux Saga 中,run 函数用于启动中间件并运行 root Saga。 run 函数接受一个参数,即根 Saga 函数。 rootSaga 函数被传递给中间件实例的 run 函数,该函数启动 Saga 并允许其侦听分派的操作。

当使用 run 函数启动中间件时,将执行根 Saga,并且其中的任何生成器函数都会被执行。开始了。然后,中间件侦听分派的操作,并使用yield关键字将它们传递给相关的Saga生成器函数。

如果分派的操作与 Saga 中定义的模式匹配,中间件将在yield 语句处暂停 Saga 并运行 Saga 中的代码。一旦 Saga 完成执行,中间件将恢复生成器功能并继续处理任何剩余的分派操作。

G — 在 React 组件中使用 redux 操作

在与 saga 中间件、类型、切片和 sagas 文件一起创建 Redux 配置后。现在,是时候消耗状态并分派触发操作了。

让我们假设我们有一个代表个人资料页面的组件。

注意:在撰写本文时,我仍然相信您在 React 方面拥有良好且扎实的知识。

import React, { useEffect } from "react";  
import { useDispatch, useSelector } from "react-redux";  
import { useParams } from "react-router-dom";  
import { getUserAction } from "../redux/users/slice";  
import { StateType } from "../redux/root-reducer";  
  
const ProfilePage: React.FC = () => {  
  
const { data, isLoading } = useSelector((state: StateType) => state.users);  
  
const { id } = useParams();  
  
const dispatch = useDispatch();  
  
useEffect(() => {  
dispatch(getUserAction(id));  
}, [id]);  
  
return (  
<div>  
{  
isLoading  
?  
(<span>Loading...</span>)  
:  
data  
?  
(<div>Hi, I'm {data.name}</div>)  
:  
(<span>No user found!</span>)  
}  
</div>  
)  
}

useSelector — useSelector 是react-redux 库提供的React hook,允许组件从Redux store中读取数据。它接受一个函数作为参数,该函数应该返回组件感兴趣的 store 部分。

当 store 发生变化时,useSelector 会将所选数据的先前值与新值进行比较,并触发重新渲染如果它们不同,则为组件的名称。这样可以轻松地使组件与store保持同步,而无需手动订阅store更新或管理组件中的状态。

请注意,useSelector 只能用于从store中读取数据,而不能用于分派操作或直接修改store。

useDispatch — 在 Redux 中,useDispatch 是 React-redux 库提供的 React hook,允许组件将操作分派到 Redux store。它提供了一种从组件内部触发store更改的方法。它用于获取对 Redux store提供的调度函数的引用。然后,该组件可以使用操作对象调用调度来触发对store的更改。

请注意,useDispatch 只能用于分派操作,而不应用于从store中读取数据。

将 useDispatch 和 useSelector 一起使用是 Redux 应用程序中的常见模式,因为它允许组件从store中读取数据并调度操作来修改数据。

结论

过去,我们曾经使用 Redux 库来存储数据。使用 redux-toolkit ,现在比以前更容易、代码更少。你总是可以找到自己的方式来创建 redux 结构,比如将切片分割成更小的切片以防止大文件等等。

我希望你们喜欢阅读这篇文章,并且希望它对您有所帮助并且充满“简化和解释的概念”。如果还有什么模糊或不够清晰的地方,或者您有任何建议……请在评论部分告诉我。

快乐编码!

参考文献: