Movatterモバイル変換


[0]ホーム

URL:


Skip to content

Navigation Menu

Sign in
Appearance settings

Search code, repositories, users, issues, pull requests...

Provide feedback

We read every piece of feedback, and take your input very seriously.

Saved searches

Use saved searches to filter your results more quickly

Sign up
Appearance settings

Boilerplate free class-based reducer creator. Built with TypeScript. Works with Redux and NGRX. Has integration with immer.

License

NotificationsYou must be signed in to change notification settings

fxlrnrpt/reducer-class

Repository files navigation

Boilerplate free class-based reducer creator. Built with TypeScript. Works with Redux and NGRX. Has integration withimmer.

Heavily inspired by awesomengrx-actions. It's pretty much a re-write of its reducer-related functionality with stricter typings, usage of reflected typed and leaving aside Angular-only functionality. This library is framework-agnostic and should work with any Redux implementation (Redux, NGRX).

Consider using it withflux-action-class.

Installation

Angular

  1. Run

    npm i reducer-class immer
  2. If you use TypeScript set in you tsconfig.json

    "experimentalDecorators":true,"emitDecoratorMetadata":true,
  3. If you use JavaScript configure your babel to support decorators and class properties

React

  1. Run

    npm i reducer-class immer reflect-metadata
  2. At the top of your project root file (most probablyindex.tsx) add

    import'reflect-metadata'
  3. If you use TypeScript set in you tsconfig.json

    "experimentalDecorators":true,"emitDecoratorMetadata":true,
  4. If you use JavaScript configure your babel to support decorators and class properties

Quick start

Recommended (withflux-action-class)

import{ActionStandard}from'flux-action-class'import{Action,ReducerClass}from'reducer-class'classActionCatEatextendsActionStandard<number>{}classActionCatPlayextendsActionStandard<number>{}classActionCatBeAwesomeextendsActionStandard<number>{}interfaceIReducerCatState{energy:number}classReducerCatextendsReducerClass<IReducerCatState>{initialState={energy:100,}  @ActionaddEnergy(state:IReducerCatState,action:ActionCatEat){return{energy:state.energy+action.payload,}}  @Action(ActionCatPlay,ActionCatBeAwesome)wasteEnegry(state:IReducerCatState,action:ActionCatPlay|ActionCatBeAwesome){return{energy:state.energy-action.payload,}}}constreducer=ReducerCat.create()
JavaScript version
import{ActionStandard}from'flux-action-class'import{Action,ReducerClass}from'reducer-class'classActionCatEatextendsActionStandard{}classActionCatPlayextendsActionStandard{}classActionCatBeAwesomeextendsActionStandard{}classReducerCatextendsReducerClass{initialState={energy:100,}  @Action(ActionCatEat)addEnergy(state,action){return{energy:state.energy+action.payload,}}  @Action(ActionCatPlay,ActionCatBeAwesome)wasteEnegry(state,action){return{energy:state.energy-action.payload,}}}constreducer=ReducerCat.create()

We can not useAction without arguments in JavaScript because there's no compiler which provides us with metadata for type reflection.

Classic NGRX actions

import{Action,ReducerClass}from'reducer-class'classActionCatEat{type='ActionCatEat'constructor(publicpayload:number){}}classActionCatPlay{type='ActionCatPlay'constructor(publicpayload:number){}}classActionCatBeAwesome{type='ActionCatBeAwesome'constructor(publicpayload:number){}}interfaceIReducerCatState{energy:number}classReducerCatextendsReducerClass<IReducerCatState>{initialState={energy:100,}  @ActionaddEnergy(state:IReducerCatState,action:ActionCatEat){return{energy:state.energy+action.payload,}}  @Action(ActionCatPlay,ActionCatBeAwesome)wasteEnegry(state:IReducerCatState,action:ActionCatPlay|ActionCatBeAwesome){return{energy:state.energy-action.payload,}}}constreducer=ReducerCat.create()
JavaScript version
import{Action,ReducerClass}from'reducer-class'classActionCatEat{type='ActionCatEat'constructor(payload){this.payload=payload}}classActionCatPlay{type='ActionCatPlay'constructor(payload){this.payload=payload}}classActionCatBeAwesome{type='ActionCatBeAwesome'constructor(payload){this.payload=payload}}classReducerCatextendsReducerClass{initialState={energy:100,}  @Action(ActionCatEat)addEnergy(state,action){return{energy:state.energy+action.payload,}}  @Action(ActionCatPlay,ActionCatBeAwesome)wasteEnegry(state,action){return{energy:state.energy-action.payload,}}}constreducer=ReducerCat.create()

We can not useAction without arguments in JavaScript because there's no compiler which provides us with metadata for type reflection.

import{Action,ReducerClass}from'reducer-class'import{createAction}from'redux-actions'constactionCatEat=createAction('actionTypeCatEat')constactionCatPlay=createAction('actionTypeCatPlay')constactionCatBeAwesome=createAction('actionTypeCatBeAwesome')interfaceIReducerCatState{energy:number}classReducerCatextendsReducerClass<IReducerCatState>{initialState={energy:100,}  @Action(actionCatEat)addEnergy(state:IReducerCatState,action:{payload:number}){return{energy:state.energy+action.payload,}}  @Action(actionCatPlay,actionCatBeAwesome)wasteEnegry(state:IReducerCatState,action:{payload:number}){return{energy:state.energy-action.payload,}}}constreducer=ReducerCat.create()

You might have noticed that we always pass actions toAction in this version. It's because we no longer use classes for our actions and TypeScript can not provide type metadata.

JavaScript version
import{Action,ReducerClass}from'reducer-class'import{createAction}from'redux-actions'constactionCatEat=createAction('actionTypeCatEat')constactionCatPlay=createAction('actionTypeCatPlay')constactionCatBeAwesome=createAction('actionTypeCatBeAwesome')classReducerCatextendsReducerClass{initialState={energy:100,}  @Action(actionCatEat)addEnergy(state,action:{payload}){return{energy:state.energy+action.payload,}}  @Action(actionCatPlay,actionCatBeAwesome)wasteEnegry(state,action:{payload}){return{energy:state.energy-action.payload,}}}constreducer=ReducerCat.create()

Old school: action type constants

import{Action,ReducerClass}from'reducer-class'constactionTypeCatEat='actionTypeCatEat'constactionTypeCatPlay='actionTypeCatPlay'constactionTypeCatBeAwesome='actionTypeCatBeAwesome'interfaceIReducerCatState{energy:number}classReducerCatextendsReducerClass<IReducerCatState>{initialState={energy:100,}  @Action(actionTypeCatEat)addEnergy(state:IReducerCatState,action:{payload:number}){return{energy:state.energy+action.payload,}}  @Action(actionTypeCatPlay,actionTypeCatBeAwesome)wasteEnegry(state:IReducerCatState,action:{payload:number}){return{energy:state.energy-action.payload,}}}constreducer=ReducerCat.create()

You might have noticed that we always pass actions toAction in this version. It's because we no longer use classes for our actions and TypeScript can not provide type metadata.

JavaScript version
import{Action,ReducerClass}from'reducer-class'constactionTypeCatEat='actionTypeCatEat'constactionTypeCatPlay='actionTypeCatPlay'constactionTypeCatBeAwesome='actionTypeCatBeAwesome'classReducerCat{initialState={energy:100,}  @Action(actionTypeCatEat)addEnergy(state,action){return{energy:state.energy+action.payload,}}  @Action(actionTypeCatPlay,actionTypeCatBeAwesome)wasteEnegry(state,action){return{energy:state.energy-action.payload,}}}constreducer=ReducerCat.create()

Integration withimmer

If your reducer expects 3 argumentsreducer-class automatically wraps it withproduce fromimmer.

  1. Original read-only state
  2. Draft of the new state that you should mutate
  3. Action

Why 3?Read pitfall #3 from immer's official documentation.

import{ActionStandard}from'flux-action-class'import{Action,ReducerClass,Immutable}from'reducer-class'classActionCatEatextendsActionStandard<number>{}classActionCatPlayextendsActionStandard<number>{}classActionCatBeAwesomeextendsActionStandard<number>{}interfaceIReducerCatState{energy:number}classReducerCatextendsReducerClass<IReducerCatState>{initialState={energy:100,}  @ActionaddEnergy(state:Immutable<IReducerCatState>,draft:IReducerCatState,action:ActionCatEat){draft.energy+=action.payload}  @Action(ActionCatPlay,ActionCatBeAwesome)wasteEnegry(state:Immutable<IReducerCatState>,draft:IReducerCatState,action:ActionCatPlay|ActionCatBeAwesome){draft.energy-=action.payload// Unfortunatelly, we can not omit `return` statement here due to how TypeScript handles `void`// https://github.com/Microsoft/TypeScript/wiki/FAQ#why-are-functions-returning-non-void-assignable-to-function-returning-voidreturnundefined}}constreducer=ReducerCat.create()

As you can see we still returnundefined from the reducer even though we useimmer and mutate our draft. Unfortunately, we can not omitreturn statement here due tohow TypeScript handlesvoid. We can not even writereturn (withourundefined), because TypeScript then presumes the method returnsvoid.

You might have noticed a new import -Immutable. It's just a cool name forDeepReadonly type. You don't have to use it. The example above would work just fine if used justIReducerCatState. Yet it's recommended to wrap it withImmutable to ensure that you never mutate it.

Actually it makes total sense to useImmutable for state of regular reducers as well to make sure you never modify state directly.

Reusing reducers

So what if we want to share some logic between reducers?

Step 1

Create a class with shared logic.

import{Action,ReducerClassMixin}from'reducer-class'interfaceIHungryState{hungry:boolean}exportclassReducerHungry<TextendsIHungryState>extendsReducerClassMixin<T>{  @Action(ActionHungry)hugry(state:T){return{      ...state,hungry:true,}}  @Action(ActionFull)full(state:T){return{      ...state,hungry:false,}}}

You might have noticed that made this class generic. We have to do that because we do not know what actual state we going to extend, we can only put a constraint on it to make sure it satisfies the structure we need. In other words, if we usedIHungryState directly and returned{ hungry: true } (not{ ...state, hungry: true }) fromhungry compiler wouldn't complain.

You don't have to useReducerClassMixin class. It's nothing but a convenience wrapper to make sure your class carries an index signature for type-safety. Alternatively you can useIReducerClassConstraint interface andReducerClassMethod type.

How to use `IReducerClassConstraint` interface and `ReducerClassMethod` type instead of `ReducerClassMixin` class
import{Action,IReducerClassConstraint,ReducerClassMethod}from'reducer-class'interfaceIHungryState{hungry:boolean}exportclassReducerHungry<TextendsIHungryState>implementsIReducerClassConstraint<T>{[methodName:string]:ReducerClassMethod<T>  @Action(ActionHungry)hugry(state:T){return{      ...state,hungry:true,}}  @Action(ActionFull)full(state:T){return{      ...state,hungry:false,}}}
JavaScript version
import{Action}from'reducer-class'exportclassReducerHungry{  @Action(ActionHungry)hugry(state){return{      ...state,hungry:true,}}  @Action(ActionFull)full(state){return{      ...state,hungry:false,}}}

Step 2

Use @Extend decorator.

import{Action,Extend,ReducerClass}from'reducer-class'import{ReducerHungry}from'shared'interfaceICatState{hugry:booleanenegry:number}@Extend<ICatState>(ReducerHungry)classCatReducerextendsReducerClass<ICatState>{initialState={energy:100,}  @ActionaddEnergy(state:ICatState,action:ActionCatEat){return{energy:state.energy+action.payload,}}  @Action(ActionCatPlay,ActionCatBeAwesome)wasteEnegry(state:ICatState,action:ActionCatPlay|ActionCatBeAwesome){return{energy:state.energy-action.payload,}}}constreducer=ReducerCat.create()

@Extend can accept as many arguments as you want.

Now our cat reducer useswasteEnegry to handle actionsActionCatPlay,ActionCatBeAwesome,addEnergy to handleActionCatEat and inheritshugry andfull methods to handleActionHungry andActionFull fromReducerHungry.

JavaScript version
import{Action,Extend,ReducerClass}from'reducer-class'import{ReducerHungry}from'shared'@Extend(ReducerHungry)classCatReducerextendsReducerClass{initialState={energy:100,}  @Action(ActionCatEat)addEnergy(state,action){return{energy:state.energy+action.payload,}}  @Action(ActionCatPlay,ActionCatBeAwesome)wasteEnegry(state,action){return{energy:state.energy-action.payload,}}}constreducer=ReducerCat.create()

How can I make shared reducer's logic dynamic?

You can use class factories.

import{Action,Extend,ReducerClass,ReducerClassMixin}from'reducer-class'interfaceIHungryState{hungry:boolean}exportconstmakeReducerHungry=<TextendsIHungryState>(actionHungry,actionFull)=>{classExtender1extendsReducerClassMixin<T>{    @Action(actionHungry)hugry(state:T){return{        ...state,hungry:true,}}    @Action(actionFull)full(state:T){return{        ...state,hungry:false,}}}returnExtender1}interfaceICatState{hugry:booleanenegry:number}@Extend<ICatState>(makeReducerHungry(ActionCatPlay,ActionCatEat))classCatReducerextendsReducerClass<ICatState>{initialState={energy:100,}  @ActionaddEnergy(state:ICatState,action:ActionCatEat){return{energy:state.energy+action.payload,}}  @ActionwasteEnegry(state:ICatState,action:ActionCatPlay){return{energy:state.energy-action.payload,}}}constreducer=ReducerCat.create()
JavaScript version
import{Action,Extend,ReducerClass}from'reducer-class'exportconstmakeReducerHungry=(actionHungry,actionFull)=>class{    @Action(actionHungry)hugry(state){return{        ...state,hungry:true,}}    @Action(actionFull)full(state){return{        ...state,hungry:false,}}}@Extend(makeReducerHungry(ActionCatPlay,ActionCatEat))classCatReducerextendsReducerClass{initialState={energy:100,}  @Action(ActionCatEat)addEnergy(state,action){return{energy:state.energy+action.payload,}}  @Action(ActionCatPlay)wasteEnegry(state,action){return{energy:state.energy-action.payload,}}}constreducer=ReducerCat.create()

Reducer inheritance

Any reducer class is still a class, therefore it can be inherited. It's different way to share some common logic and alter the final behavior for children. There's no runtime information about method visibility (private,protected,public), so if you want to share some common logic without wrapping it with@Action decorator prefix the shared method with_.

interfaceICatState{enegry:number}classCatReducerextendsReducerClass<ICatState>{initialState={energy:10,}  @ActionaddEnergy(state:ICatState,action:ActionCatEat){returnthis._addEnergy(state,action)}// DO NOT FORGET TO PREFIX IT WITH "_"protected_addEnergy(state:ICatState,action:ActionCatEat):ICatState{return{energy:state.energy+action.payload,}}}classKittenReducerextendsCatReducer{// DO NOT FORGET TO PREFIX IT WITH "_"protected_addEnergy(state:ICatState,action:ActionCatEat):ICatState{return{energy:state.energy+action.payload*10,}}}
JavaScript version
classCatReducerextendsReducerClass{initialState={energy:10,}  @Action(ActionCatEat)addEnergy(state,action){returnthis._addEnergy(state,action)}// DO NOT FORGET TO PREFIX IT WITH "_"protected_addEnergy(state,action){return{energy:state.energy+action.payload,}}}classKittenReducerextendsCatReducer{// DO NOT FORGET TO PREFIX IT WITH "_"protected_addEnergy(state,action){return{energy:state.energy+action.payload*10,}}}

In depth

When can we omit list of actions for@Action?

You can omit list of actions for@Action if you want to run a reducer function for a single action.Works with TypeScript only! Action must be a class-based action. It can be a flux-action-class' action, a classic NGRX class-based action or any other class which has either a static propertytype or a propertytype on the instance of the class.

Running several reducers for the same action

If you have declare several reducer functions corresponding to the same actionreducer-class runs all of them serially. It uses its own implementation ofreduce-reducers. The order is defined byObject.keys.

import{ActionStandard}from'flux-action-class'import{Action,ReducerClass}from'reducer-class'classActionCatEatextendsActionStandard<number>{}classActionCatSleepextendsActionStandard<number>{}interfaceIReducerCatState{energy:number}classReducerCatextendsReducerClass<IReducerCatState>{initialState={energy:100,}  @Action(ActionCatEat,ActionCatSleep)addEnergy(state:IReducerCatState,action:ActionCatEat|ActionCatSleep){return{energy:state.energy+action.payload,}}  @ActionaddMoreEnergy(state:IReducerCatState,action:ActionCatSleep){return{energy:state.energy+action.payload*2,}}}constreducer=ReducerCat.create()constres1=reducer(undefined,newActionCatSleep(10))console.log(res1.energy)// logs 130: 100 - initial value, 10 is added by addEnergy, 10 * 2 is added by addMoreEnergyconstres2=reducer(res1,newActionCatEat(5))console.log(res2)// logs 135: 130 - previous value, 5 is added by addEnergy

How does @Extend work?

It iterates over its arguments and copies their methods and corresponding metadata to a prototype of our target reducer class.

How does it compare tongrx-actions?

  1. Stricter typings. Now you'll never forget to add initial state, return a new state from your reducer and accidentally invokeimmer as a result and etc.
  2. @Action can be used to automatically reflect a corresponding action from the type.
  3. ngrx-actions doesn't allow matching several reducers to the same action, whilereducer-class allows you to do that and merges them for you.
  4. reducer-class is built with both worlds, Angular and Redux, in mind. It means equal support for all of them!
  5. reducer-class works with function-based action creators and supportsredux-actions out-of-the-box.

About

Boilerplate free class-based reducer creator. Built with TypeScript. Works with Redux and NGRX. Has integration with immer.

Topics

Resources

License

Stars

Watchers

Forks

Packages

No packages published

[8]ページ先頭

©2009-2025 Movatter.jp