Redux Toolkit作为有效Redux开发的工具

图片
当前,正在使用Redux库开发基于React框架的大部分Web应用程序。 该库是FLUX架构最流行的实现,尽管有很多明显的优点,但它也具有非常明显的缺点,例如:


  • 推荐的用于编写和组织代码的模式的复杂性和“冗长性”,这需要大量样板;
  • 缺乏用于异步行为和副作用的内置控件,这导致需要从第三方开发人员编写的各种附件中选择合适的工具。

为了解决这些缺点,Redux开发人员引入了Redux Toolkit库。 该工具是一组实用的解决方案和方法,旨在简化使用Redux的应用程序开发。 该库的开发人员旨在简化使用Redux的典型案例。 在使用Redux的每种可能情况下,该工具都不是通用的解决方案,但它可以简化开发人员需要编写的代码。


在本文中,我们将讨论Redux Toolkit中包含的主要工具,并以内部应用程序片段的示例为例,展示如何在现有代码中使用它们。


图书馆简介


Redux工具包摘要:


  • 在发布之前,该库称为redux-starter-kit;
  • 发布于2019年10月底进行;
  • Redux开发人员正式支持该库。

根据开发人员的说法,Redux Toolkit执行以下功能:


  • 帮助您快速开始使用Redux。
  • 使用典型任务和Redux代码简化工作;
  • 默认情况下,允许您使用Redux的最佳做法;
  • 提供的解决方案可减少对样板的不信任。

Redux Toolkit提供了一组专门设计的工具 ,并添加了许多与Redux共同使用的经过验证的工具 。 这种方法使开发人员可以决定如何在其应用程序中使用哪些工具。 在本文的过程中,我们将注意该库使用哪些借用。 有关Redux Toolkit的更多信息和依赖性,请参阅@ reduxjs / toolkit软件包说明。


Redux Toolkit库提供的最重要的功能是:


  • # configureStore-旨在简化创建和配置存储的过程的功能;
  • #createReducer-此功能有助于简洁明了地描述和创建化器;
  • #createAction-为动作类型的指定字符串返回动作创建者的函数;
  • #createSlice-结合了createAction和createReducer的功能;
  • createSelector是Reselect库中的一个函数,已重新导出以便于使用。

还应注意Redux Toolkit与TypeScript完全集成。 有关更多信息,请参见官方文档中的TypeScript用法


申请书


考虑使用Redux Toolkit库作为实际使用的React Redux应用程序片段的示例。
注意事项 在本文的进一步内容中,将在不使用Redux Toolkit且不使用Redux Toolkit的情况下展示源代码,这将有助于更好地评估使用该库的积极和消极方面。


挑战赛


在我们的一个内部应用程序中,需要添加,编辑和显示有关我们软件产品版本的信息。 对于每个操作,都开发了单独的API函数,需要将其结果添加到Redux存储中。 作为控制异步行为和副作用的一种方法,我们将使用Thunk


创建存储


创建存储库的源代码的初始版本如下所示:


import { createStore, applyMiddleware, combineReducers, compose, } from 'redux'; import thunk from 'redux-thunk'; import * as reducers from './reducers'; const ext = window.__REDUX_DEVTOOLS_EXTENSION__; const devtoolMiddleware = ext && process.env.NODE_ENV === 'development' ? ext() : f => f; const store = createStore( combineReducers({ ...reducers, }), compose( applyMiddleware(thunk), devtoolMiddleware ) ); 

如果仔细看一下上面的代码,您会看到很长的动作序列,必须完成这些动作才能完全配置存储。 Redux Toolkit包含一个旨在简化此过程的工具,即configureStore函数。


ConfigureStore功能


该工具可让您自动组合化归约器,添加Redux中间件(默认包括redux-thunk)以及使用Redux DevTools扩展。 configureStore函数接受具有以下属性的对象作为输入参数:


  • 减速器-一组定制减速器,
  • 中间件-一个可选参数,用于指定旨在连接到存储库的中间件数组,
  • devTools-逻辑类型参数,可让您启用浏览器中安装的Redux DevTools扩展(默认值为true),
  • preloadedState-一个可选参数,用于设置存储库的初始状态,
  • 增强器-定义一组放大器的可选参数。

要获得最受欢迎的中间件列表,可以使用特殊功能getDefaultMiddleware,它也是Redux Toolkit的一部分。 此函数返回Redux Toolkit库中默认启用了中间件的数组。 这些中间件的列表因执行代码的方式而异。 在生产模式下,数组仅包含一个元素-thunk。 在开发模式下,在编写本文时,该列表将补充以下中间件:


  • serializableStateInvariant-一种专门用于Redux Toolkit的工具,旨在检查状态树中是否存在不可序列化的值,例如函数,Promise,Symbol和不是简单JS数据的其他值;
  • immutableStateInvariant-来自redux-immutable-state-invariant包的中间件,用于检测存储中包含的数据中的突变。

要指定中间件的升序列表,getDefaultMidlleware函数接受一个对象,该对象定义了所包括的中间件及其设置的列表。 有关此信息的更多信息,请参见官方文档的相应部分


现在,我们将重写代码部分,以使用上述工具创建存储库。 结果,我们得到以下信息:


 import { configureStore, getDefaultMiddleware } from '@reduxjs/toolkit'; import * as reducers from './reducers'; const middleware = getDefaultMiddleware({ immutableCheck: false, serializableCheck: false, thunk: true, }); export const store = configureStore({ reducer: { ...reducers }, middleware, devTools: process.env.NODE_ENV !== 'production', }); 

使用本节代码的示例,您可以清楚地看到configureStore函数解决了以下问题:


  • 需要合并减速器,自动调用CombineReducers,
  • 需要结合中间件时,会自动调用applyMiddleware。

它还允许您使用redux-devtools-extension包中的composeWithDevTools函数更方便地启用Redux DevTools扩展。 以上所有这些都表明使用此功能可以使代码更紧凑和易于理解。


这样就完成了存储库的创建和配置。 我们将其转移给提供商,然后继续。


动作,动作创建者和减速器


现在,让我们从开发动作,动作创建者和化简器的角度来看一下Redux Toolkit的功能。 不使用Redux Toolkit的代码的初始版本被组织为actions.js和reducers.js文件。 actions.js文件的内容如下所示:


 import * as productReleasesService from '../../services/productReleases'; export const PRODUCT_RELEASES_FETCHING = 'PRODUCT_RELEASES_FETCHING'; export const PRODUCT_RELEASES_FETCHED = 'PRODUCT_RELEASES_FETCHED'; export const PRODUCT_RELEASES_FETCHING_ERROR = 'PRODUCT_RELEASES_FETCHING_ERROR'; … export const PRODUCT_RELEASE_UPDATING = 'PRODUCT_RELEASE_UPDATING'; export const PRODUCT_RELEASE_UPDATED = 'PRODUCT_RELEASE_UPDATED'; export const PRODUCT_RELEASE_CREATING_UPDATING_ERROR = 'PRODUCT_RELEASE_CREATING_UPDATING_ERROR'; function productReleasesFetching() { return { type: PRODUCT_RELEASES_FETCHING }; } function productReleasesFetched(productReleases) { return { type: PRODUCT_RELEASES_FETCHED, productReleases }; } function productReleasesFetchingError(error) { return { type: PRODUCT_RELEASES_FETCHING_ERROR, error } } … export function fetchProductReleases() { return dispatch => { dispatch(productReleasesFetching()); return productReleasesService.getProductReleases().then( productReleases => dispatch(productReleasesFetched(productReleases)) ).catch(error => { error.clientMessage = "Can't get product releases"; dispatch(productReleasesFetchingError(error)) }); } } … export function updateProductRelease( id, productName, productVersion, releaseDate ) { return dispatch => { dispatch(productReleaseUpdating()); return productReleasesService.updateProductRelease( id, productName, productVersion, releaseDate ).then( productRelease => dispatch(productReleaseUpdated(productRelease)) ).catch(error => { error.clientMessage = "Can't update product releases"; dispatch(productReleaseCreatingUpdatingError(error)) }); } } 

使用Redux Toolkit之前reducers.js文件的内容:


 const initialState = { productReleases: [], loadedProductRelease: null, fetchingState: 'none', creatingState: 'none', updatingState: 'none', error: null, }; export default function reducer(state = initialState, action = {}) { switch (action.type) { case productReleases.PRODUCT_RELEASES_FETCHING: return { ...state, fetchingState: 'requesting', error: null, }; case productReleases.PRODUCT_RELEASES_FETCHED: return { ...state, productReleases: action.productReleases, fetchingState: 'success', }; case productReleases.PRODUCT_RELEASES_FETCHING_ERROR: return { ...state, fetchingState: 'failed', error: action.error }; … case productReleases.PRODUCT_RELEASE_UPDATING: return { ...state, updatingState: 'requesting', error: null, }; case productReleases.PRODUCT_RELEASE_UPDATED: return { ...state, updatingState: 'success', productReleases: state.productReleases.map(productRelease => { if (productRelease.id === action.productRelease.id) return action.productRelease; return productRelease; }) }; case productReleases.PRODUCT_RELEASE_UPDATING_ERROR: return { ...state, updatingState: 'failed', error: action.error }; default: return state; } } 

如我们所见,这是大多数样板所在的位置:动作类型常量,动作创建者,常量,但是在化简器代码中,编写所有这些代码需要时间。 您可以使用redux Toolkit的createAction和createReducer函数来部分摆脱此样板。


CreateAction函数


在代码的给定部分中,使用了在Redux中定义动作的标准方法:首先,分别定义一个常量,该常量确定动作的类型,然后-此类型的动作的创建者的功能。 createAction函数将这两个声明合并为一个。 在输入时,它接受一个操作类型并返回该类型的操作的创建者。 动作创建者可以不带参数,也可以带某些参数(有效负载)调用,其值将放置在所创建动作的有效负载字段中。 此外,动作创建者会覆盖toString()函数,以便动作类型成为其字符串表示形式。


在某些情况下,您可能需要编写其他逻辑来调整有效负载的值,例如,接受动作创建者的多个参数,创建随机标识符或获取当前时间戳。 为此,createAction采用一个可选的第二个参数-一个将用于更新有效负载值的函数。 有关此参数的更多信息,请参见官方文档。
使用createAction函数,我们得到以下代码:


 export const productReleasesFetching = createAction('PRODUCT_RELEASES_FETCHING'); export const productReleasesFetched = createAction('PRODUCT_RELEASES_FETCHED'); export const productReleasesFetchingError = createAction('PRODUCT_RELEASES_FETCHING_ERROR'); … export function fetchProductReleases() { return dispatch => { dispatch(productReleasesFetching()); return productReleasesService.getProductReleases().then( productReleases => dispatch(productReleasesFetched({ productReleases })) ).catch(error => { error.clientMessage = "Can't get product releases"; dispatch(productReleasesFetchingError({ error })) }); } } ... 

CreateReducer函数


现在考虑减速器。 如我们的示例中所示,reducer通常是使用switch语句实现的,每种处理的操作类型都有一个寄存器。 这种方法效果很好,但并非没有样板且容易出错。 例如,很容易忘记描述默认情况或不设置初始状态。 createReducer函数通过将化简函数定义为用于处理每种动作的函数搜索表,简化了化简函数的创建过程。 它还允许您通过在化简器中以“可变”样式编写代码来显着简化不可变更新的逻辑。


通过使用Immer库,可以使用强大的事件处理样式。 处理程序函数可以“改变”传递的状态以更改属性,也可以返回新状态,例如以不可变样式工作时,但是由于Immer的存在,对象的真正变异无法执行。 第一个选项对于工作和感知来说要容易得多,尤其是在使用深层嵌套更改对象时。


注意:从函数返回新对象将覆盖“可变”更改。 不能同时使用两种状态更新方法。


createReducer函数接受以下参数作为输入参数:


  • 初始存储状态
  • 一个对象,该对象在动作和减速器的类型之间建立对应关系,每个动作和减速器都处理某种类型的对象。

使用createReducer方法,我们得到以下代码:


 const initialState = { productReleases: [], loadedProductRelease: null, fetchingState: 'none', creatingState: 'none', loadingState: 'none', error: null, }; const counterReducer = createReducer(initialState, { [productReleasesFetching]: (state, action) => { state.fetchingState = 'requesting' }, [productReleasesFetched.type]: (state, action) => { state.productReleases = action.payload.productReleases; state.fetchingState = 'success'; }, [productReleasesFetchingError]: (state, action) => { state.fetchingState = 'failed'; state.error = action.payload.error; }, … [productReleaseUpdating]: (state) => { state.updatingState = 'requesting' }, [productReleaseUpdated]: (state, action) => { state.updatingState = 'success'; state.productReleases = state.productReleases.map(productRelease => { if (productRelease.id === action.payload.productRelease.id) return action.payload.productRelease; return productRelease; }); }, [productReleaseUpdatingError]: (state, action) => { state.updating = 'failed'; state.error = action.payload.error; }, }); 

如我们所见,使用createAction和createReducer函数本质上解决了编写额外代码的问题,但预先创建常量的问题仍然存在。 因此,我们考虑将动作创建者和简化器的生成结合在一起的更强大的选项-createSlice函数。


CreateSlice函数


createSlice函数接受具有以下字段作为输入参数的对象:


  • ${name}/${action.type}创建的动作的名称空间( ${name}/${action.type} );
  • initialState-减速器的初始状态;
  • 减速器-具有处理程序的对象。 每个处理程序都采用带有状态和动作自变量的函数,动作在有效负载属性中包含数据,在名称属性中包含事件的名称。 此外,可以在事件进入缩减程序之前预先更改从事件接收的数据(例如,将id添加到集合的元素中)。 为此,您必须通过一个带有reducer和prepare字段的对象来代替函数,其中reducer是动作处理函数,prepare是返回更新后的有效负载的有效负载处理函数。
  • extraReducers-包含另一个切片的化简器的对象。 如果有必要更新属于另一个片的对象,则可能需要此参数。 您可以从官方文档的相应部分中了解有关此功能的更多信息。

该函数的结果是一个名为“切片”的对象,其中包含以下字段:


  • 名称-切片名称,
  • 减速器-减速器,
  • 动作-一组动作。

使用此函数解决我们的问题,我们得到以下源代码:


 const initialState = { productReleases: [], loadedProductRelease: null, fetchingState: 'none', creatingState: 'none', loadingState: 'none', error: null, }; const productReleases = createSlice({ name: 'productReleases', initialState, reducers: { productReleasesFetching: (state) => { state.fetchingState = 'requesting'; }, productReleasesFetched: (state, action) => { state.productReleases = action.payload.productReleases; state.fetchingState = 'success'; }, productReleasesFetchingError: (state, action) => { state.fetchingState = 'failed'; state.error = action.payload.error; }, … productReleaseUpdating: (state) => { state.updatingState = 'requesting' }, productReleaseUpdated: (state, action) => { state.updatingState = 'success'; state.productReleases = state.productReleases.map(productRelease => { if (productRelease.id === action.payload.productRelease.id) return action.payload.productRelease; return productRelease; }); }, productReleaseUpdatingError: (state, action) => { state.updating = 'failed'; state.error = action.payload.error; }, }, }); 

现在,我们将从创建的切片中提取动作创建者和简化器。


 const { actions, reducer } = productReleases; export const { productReleasesFetched, productReleasesFetching, productReleasesFetchingError, … productReleaseUpdated, productReleaseUpdating, productReleaseUpdatingError } = actions; export default reducer; 

包含API调用的操作创建者的源代码未更改,除了发送操作时传递参数的方法外:


 export const fetchProductReleases = () => (dispatch) => { dispatch(productReleasesFetching()); return productReleasesService .getProductReleases() .then((productReleases) => dispatch(productReleasesFetched({ productReleases }))) .catch((error) => { error.clientMessage = "Can't get product releases"; dispatch(productReleasesFetchingError({ error })); }); }; … export const updateProductRelease = (id, productName, productVersion, releaseDate) => (dispatch) => { dispatch(productReleaseUpdating()); return productReleasesService .updateProductRelease(id, productName, productVersion, releaseDate) .then((productRelease) => dispatch(productReleaseUpdated({ productRelease }))) .catch((error) => { error.clientMessage = "Can't update product releases"; dispatch(productReleaseUpdatingError({ error })); }); 

上面的代码显示,在使用Redux时,createSlice函数可让您摆脱样板的重要部分,这不仅使代码更紧凑,简洁和易于理解,而且花费更少的时间编写代码。


总结


在本文的最后,我想说的是,尽管Redux Toolkit库没有为存储管理添加任何新东西,但它提供了许多比以前更方便的代码编写方式。 这些工具不仅可以使开发过程更加方便,易于理解和更快,而且由于库中包含许多经过验证的工具,因此也可以提高效率。 我们,Inobitek,计划继续在我们的软件产品开发中使用该库,并监视Web技术领域的新发展。


谢谢您的关注。 我们希望我们的文章会有用。 可以从官方文档中获取有关Redux Toolkit库的更多信息。

Source: https://habr.com/ru/post/zh-CN481288/


All Articles