另一个Redux样板减少指南(NGRX)


那会是什么?


我们将讨论几种(具体来说是五种)方法,技巧,对企业之神的流血牺牲,这些似乎有助于我们在Redux(和NGRX!)应用程序中编写更简洁,更具表现力的代码。 汗水和咖啡困扰着道路。 请猛烈批评。 我们将学习一起更好地编码。


老实说,起初,我只是想向世界介绍我的新微库(35行代码!) Flux-action-class ,但关注的是不断增长的惊叹号,Habr很快将成为Twitter,在大多数情况下同意他们的意见,我决定尝试做些更宽泛的阅读。 因此,我们提供了5种升级Redux应用程序的方法!


样板出来


考虑一个如何向Redux发送AJAX请求的典型示例。 让我们想象一下,我们确实需要服务器提供的密封清单。


import { createSelector } from 'reselect' const actionTypeCatsGetInit = 'CATS_GET_INIT' const actionTypeCatsGetSuccess = 'CATS_GET_SUCCESS' const actionTypeCatsGetError = 'CATS_GET_ERROR' const actionCatsGetInit = () => ({ type: actionTypeCatsGetInit }) const actionCatsGetSuccess = (payload) => ({ type: actionTypeCatsGetSuccess, payload, }) const actionCatsGetError = (error) => ({ type: actionTypeCatsGetError, payload: error, }) const reducerCatsInitialState = { error: undefined, data: undefined, loading: false, } const reducerCats = (state = reducerCatsInitialState, action) => { switch (action.type) { case actionTypeCatsGetInit: return { ...state, loading: true, } case actionCatsGetSuccess: return { error: undefined, data: action.payload, loading: false, } case actionCatsGetError: return { ...data, error: action.payload, loading: false, } default: return state } } const makeSelectorCatsData = () => createSelector( (state) => state.cats.data, (cats) => cats, ) const makeSelectorCatsLoading = () => createSelector( (state) => state.cats.loading, (loading) => loading, ) const makeSelectorCatsError = () => createSelector( (state) => state.cats.error, (error) => error, ) 

如果您不太了解为什么这里需要选择器工厂,则可以在此处阅读


我不会在这里故意考虑副作用。 这是另一篇充满青少年愤怒和对现有生态系统的批评的文章的主题:D


此代码有几个弱点:


  • 动作工厂本身具有独特性,但我们仍使用动作类型。
  • 添加新实体后,我们将继续复制相同的逻辑来设置loading标志。 我们存储在data中的data及其格式可能因请求而异,但下载指示符( loading标志)仍将相同。
  • 开关运行时间为O(n)( 差不多 )。 这本身并不是一个很强的论据,因为从原则上讲,Redux与性能无关。 令我更加恼怒的是,对于每种case您都需要编写几行额外的服务代码行,而且一个switch不能轻易且精美地分成几行。
  • 我们是否真的需要分别存储每个实体的错误状态?
  • 选择器很酷。 记住的选择器非常酷。 它们为我们提供了一种抽象,因此以后我们不必在更改应用程序形式时重做一半的应用程序。 我们只是更改选择器本身。 令人不悦的是,仅由于reselct的记忆特性的特殊性,才需要一组原始工厂。

方法1:摆脱操作类型


好吧,不是真的。 我们只是让JS为我们创建它们。


让我们再思考一下为什么我们通常需要操作类型。 好吧,显然,要在我们的reducer中启动所需的逻辑分支并相应地更改应用程序的状态。 真正的问题是,类型必须是字符串吗? 但是,如果我们使用类并按类型switch怎么办?


 class CatsGetInit {} class CatsGetSuccess { constructor(responseData) { this.payload = responseData } } class CatsGetError { constructor(error) { this.payload = error this.error = true } } const reducerCatsInitialState = { error: undefined, data: undefined, loading: false, } const reducerCats = (state = reducerCatsInitialState, action) => { switch (action.constructor) { case CatsGetInit: return { ...state, loading: true, } case CatsGetSuccess: return { error: undefined, data: action.payload, loading: false, } case CatsGetError: return { ...data, error: action.payload, loading: false, } default: return state } } 

一切似乎都很棒,但是有一个问题:我们丢失了动作的序列化。 这些不再是我们可以转换为字符串的简单对象,反之亦然。 现在,我们依靠每个动作都有自己独特的原型这一事实,实际上,它允许诸如action.constructorswitch类的设计起作用。 您知道,我真的很喜欢将操作序列化为字符串并将其与错误报告一起发送的想法,但我还没有准备好拒绝它。


因此,每个动作都应该有一个type字段( 在这里您可以看到每个尊重动作的动作还应该包含什么)。 幸运的是,每个类都有一个类似于字符串的名称。 让我们type每个类添加一个getter typetype将返回该类的名称。


 class CatsGetInit { constructor() { this.type = this.constructor.name } } const reducerCats = (state, action) => { switch (action.type) { case CatsGetInit.name: return { ...state, loading: true, } //... } } 

它甚至可以工作,但是我想为每种类型添加前缀,就像Eric先生在ducks-modular-redux中建议的那样(我建议看一下re- duck的fork,对我来说,它甚至更酷)。 为了添加前缀,我们将不得不直接停止使用类名,并添加另一个getter。 现在是静态的。


 class CatsGetInit { get static type () { return `prefix/${this.name}` } constructor () { this.type = this.constructor.type } } const reducerCats = (state, action) => { switch (action.type) { case CatsGetInit.type: return { ...state, loading: true, } //... } } 

让我们梳理一下整个过程。 将复制粘贴降至最低,并添加另一个条件:如果操作出现错误,则其payload必须为Error类型。


 class ActionStandard { get static type () { return `prefix/${this.name}` } constructor(payload) { this.type = this.constructor.type this.payload = payload this.error = payload instanceof Error } } class CatsGetInit extends ActionStandard {} class CatsGetSuccess extends ActionStandard {} class CatsGetError extends ActionStandard {} const reducerCatsInitialState = { error: undefined, data: undefined, loading: false, } const reducerCats = (state = reducerCatsInitialState, action) => { switch (action.type) { case CatsGetInit.type: return { ...state, loading: true, } case CatsGetSuccess.type: return { error: undefined, data: action.payload, loading: false, } case CatsGetError.type: return { ...data, error: action.payload, loading: false, } default: return state } } 

在此阶段,此代码可与NGRX配合使用,但是Redux无法对其进行检查。 他发誓动作应该是简单的对象。 幸运的是,JS允许我们从设计师那里返回几乎所有东西,但是在创建动作之后,我们确实并不需要原型链。


 class ActionStandard { get static type () { return `prefix/${this.name}` } constructor(payload) { return { type: this.constructor.type, payload, error: payload instanceof Error } } } class CatsGetInit extends ActionStandard {} class CatsGetSuccess extends ActionStandard {} class CatsGetError extends ActionStandard {} const reducerCatsInitialState = { error: undefined, data: undefined, loading: false, } const reducerCats = (state = reducerCatsInitialState, action) => { switch (action.type) { case CatsGetInit.type: return { ...state, loading: true, } case CatsGetSuccess.type: return { error: undefined, data: action.payload, loading: false, } case CatsGetError.type: return { ...data, error: action.payload, loading: false, } default: return state } } 

基于以上考虑,编写了磁通作用类微库 。 有测试,100%的测试覆盖率以及几乎相同的ActionStandard类,并用泛型来满足TypeScript的需求。 与TypeScript和JavaScript一起使用。


方法2:我们不惧怕使用CombineReducer


这个想法很容易被羞辱:不仅对顶级减速器使用combinedReducers ,而且还要进一步分解逻辑并创建单独的减速器以进行loading


 const reducerLoading = (actionInit, actionSuccess, actionError) => ( state = false, action, ) => { switch (action.type) { case actionInit.type: return true case actionSuccess.type: return false case actionError.type: return false } } class CatsGetInit extends ActionStandard {} class CatsGetSuccess extends ActionStandard {} class CatsGetError extends ActionStandard {} const reducerCatsData = (state = undefined, action) => { switch (action.type) { case CatsGetSuccess.type: return action.payload default: return state } } const reducerCatsError = (state = undefined, action) => { switch (action.type) { case CatsGetError.type: return action.payload default: return state } } const reducerCats = combineReducers({ data: reducerCatsData, loading: reducerLoading(CatsGetInit, CatsGetSuccess, CatsGetError), error: reducerCatsError, }) 

方法3:摆脱开关


同样,这是一个非常简单的想法:使用switch-case对象而不是switch-case通过键从该对象中选择所需的字段。 按键可以访问对象的字段为O(1),根据我的拙见,它看起来有点干净。


 const createReducer = (initialState, reducerMap) => ( state = initialState, action, ) => { //       const reducer = state[action.type] if (!reducer) { return state } //  ,    return reducer(state, action) } const reducerLoading = (actionInit, actionSuccess, actionError) => createReducer(false, { [actionInit.type]: () => true, [actionSuccess.type]: () => false, [actionError.type]: () => false, }) class CatsGetInit extends ActionStandard {} class CatsGetSuccess extends ActionStandard {} class CatsGetError extends ActionStandard {} const reducerCatsData = createReducer(undefined, { [CatsGetSuccess.type]: () => action.payload, }) const reducerCatsError = createReducer(undefined, { [CatsGetError.type]: () => action.payload, }) const reducerCats = combineReducers({ data: reducerCatsData, loading: reducerLoading(CatsGetInit, CatsGetSuccess, CatsGetError), error: reducerCatsError, }) 

让我们重构reducerLoading 。 现在,了解了reducer的映射(对象)后,我们可以从reducerLoading返回此映射,而不是返回整个reducer。 潜在地,这为扩展功能开辟了无限的范围。


 const createReducer = (initialState, reducerMap) => ( state = initialState, action, ) => { //       const reducer = state[action.type] if (!reducer) { return state } //  ,    return reducer(state, action) } const reducerLoadingMap = (actionInit, actionSuccess, actionError) => ({ [actionInit.type]: () => true, [actionSuccess.type]: () => false, [actionError.type]: () => false, }) class CatsGetInit extends ActionStandard {} class CatsGetSuccess extends ActionStandard {} class CatsGetError extends ActionStandard {} const reducerCatsLoading = createReducer( false, reducerLoadingMap(CatsGetInit, CatsGetSuccess, CatsGetError), ) /*       reducerCatsLoading: const reducerCatsLoading = createReducer( false, { ...reducerLoadingMap(CatsGetInit, CatsGetSuccess, CatsGetError), ... some custom stuff } ) */ const reducerCatsData = createReducer(undefined, { [CatsGetSuccess.type]: () => action.payload, }) const reducerCatsError = createReducer(undefined, { [CatsGetError.type]: () => action.payload, }) const reducerCats = combineReducers({ data: reducerCatsData, loading: reducerCatsLoading), error: reducerCatsError, }) 

关于Redux的官方文档也讨论了这种方法 ,但是,由于某些未知的原因,我继续看到很多使用switch-case的项目。 根据官方文档中的代码,Moshe先生为我们为createReducer编译了一个库。


方法4:使用全局错误处理程序


我们绝对不必为每个实体单独保留错误。 在大多数情况下,我们只想显示对话。 所有实体都带有动态文本的相同对话框。


创建一个全局错误处理程序。 在最简单的情况下,它可能看起来像这样:


 class GlobalErrorInit extends ActionStandard {} class GlobalErrorClear extends ActionStandard {} const reducerError = createReducer(undefined, { [GlobalErrorInit.type]: (state, action) => action.payload, [GlobalErrorClear.type]: (state, action) => undefined, }) 

然后,由于副作用,我们将在catch发送ErrorInit操作。 使用redux-thunk时可能看起来像这样:


 const catsGetAsync = async (dispatch) => { dispatch(new CatsGetInit()) try { const res = await fetch('https://cats.com/api/v1/cats') const body = await res.json() dispatch(new CatsGetSuccess(body)) } catch (error) { dispatch(new CatsGetError(error)) dispatch(new GlobalErrorInit(error)) } } 

现在我们可以摆脱cat存储区中的error字段,而仅使用CatsGetError来切换loading标志。


 class CatsGetInit extends ActionStandard {} class CatsGetSuccess extends ActionStandard {} class CatsGetError extends ActionStandard {} const reducerCatsLoading = createReducer( false, reducerLoadingMap(CatsGetInit, CatsGetSuccess, CatsGetError), ) const reducerCatsData = createReducer(undefined, { [CatsGetSuccess.type]: () => action.payload, }) const reducerCats = combineReducers({ data: reducerCatsData, loading: reducerCatsLoading) }) 

方法5:记忆前思考


让我们再看一堆用于选择器的工厂。


我抛出了makeSelectorCatsError因为不再需要它,正如我们在上一章中发现的那样。


 const makeSelectorCatsData = () => createSelector( (state) => state.cats.data, (cats) => cats, ) const makeSelectorCatsLoading = () => createSelector( (state) => state.cats.loading, (loading) => loading, ) 

为什么我们在这里需要记住的选择器? 我们到底想记住什么? 通过键访问对象字段的情况是O(1)。 我们可以使用普通的非记忆功能。 仅在要在将数据提供给组件之前从存储中更改数据时才使用备忘录。


 const selectorCatsData = (state) => state.cats.data const selectorCatsLoading = (state) => state.cats.loading 

在即时计算结果的情况下,记忆是有意义的。 对于下面的示例,让我们假设每只猫都是具有name字段的对象,并且我们想要获取一个包含所有猫的名称的字符串。


 const makeSelectorCatNames = () => createSelector( (state) => state.cats.data, (cats) => cats.data.reduce((accum, { name }) => `${accum} ${name}`, ''), ) 

结论


让我们再次看看我们从哪里开始:


 import { createSelector } from 'reselect' const actionTypeCatsGetInit = 'CATS_GET_INIT' const actionTypeCatsGetSuccess = 'CATS_GET_SUCCESS' const actionTypeCatsGetError = 'CATS_GET_ERROR' const actionCatsGetInit = () => ({ type: actionTypeCatsGetInit }) const actionCatsGetSuccess = () => ({ type: actionTypeCatsGetSuccess }) const actionCatsGetError = () => ({ type: actionTypeCatsGetError }) const reducerCatsInitialState = { error: undefined, data: undefined, loading: false, } const reducerCats = (state = reducerCatsInitialState, action) => { switch (action.type) { case actionTypeCatsGetInit: return { ...state, loading: true, } case actionCatsGetSuccess: return { error: undefined, data: action.payload, loading: false, } case actionCatsGetError: return { ...data, error: action.payload, loading: false, } default: return state } } const makeSelectorCatsData = () => createSelector( (state) => state.cats.data, (cats) => cats, ) const makeSelectorCatsLoading = () => createSelector( (state) => state.cats.loading, (loading) => loading, ) const makeSelectorCatsError = () => createSelector( (state) => state.cats.error, (error) => error, ) 

结果:


 class CatsGetInit extends ActionStandard {} class CatsGetSuccess extends ActionStandard {} class CatsGetError extends ActionStandard {} const reducerCatsLoading = createReducer( false, reducerLoadingMap(CatsGetInit, CatsGetSuccess, CatsGetError), ) const reducerCatsData = createReducer(undefined, { [CatsGetSuccess.type]: () => action.payload, }) const reducerCats = combineReducers({ data: reducerCatsData, loading: reducerCatsLoading) }) const selectorCatsData = (state) => state.cats.data const selectorCatsLoading = (state) => state.cats.loading 

希望您不要白白浪费时间,并且这篇文章至少对您有用。 就像我刚开始所说的那样,请猛烈抨击和批评。 我们将学习一起更好地编码。

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


All Articles