npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2024 – Pkg Stats / Ryan Hefner

reducer-class

v1.4.0

Published

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

Downloads

189

Readme

reducer-class Build Status Coverage Status Tweet

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

Heavily inspired by awesome ngrx-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 with flux-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 probably index.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 (with flux-action-class)

import { ActionStandard } from 'flux-action-class'
import { Action, ReducerClass } from 'reducer-class'

class ActionCatEat extends ActionStandard<number> {}
class ActionCatPlay extends ActionStandard<number> {}
class ActionCatBeAwesome extends ActionStandard<number> {}

interface IReducerCatState {
  energy: number
}
class ReducerCat extends ReducerClass<IReducerCatState> {
  initialState = {
    energy: 100,
  }

  @Action
  addEnergy(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,
    }
  }
}

const reducer = ReducerCat.create()
import { ActionStandard } from 'flux-action-class'
import { Action, ReducerClass } from 'reducer-class'

class ActionCatEat extends ActionStandard {}
class ActionCatPlay extends ActionStandard {}
class ActionCatBeAwesome extends ActionStandard {}

class ReducerCat extends ReducerClass {
  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,
    }
  }
}

const reducer = ReducerCat.create()

We can not use Action 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'

class ActionCatEat {
  type = 'ActionCatEat'
  constructor(public payload: number) {}
}
class ActionCatPlay {
  type = 'ActionCatPlay'
  constructor(public payload: number) {}
}
class ActionCatBeAwesome {
  type = 'ActionCatBeAwesome'
  constructor(public payload: number) {}
}

interface IReducerCatState {
  energy: number
}
class ReducerCat extends ReducerClass<IReducerCatState> {
  initialState = {
    energy: 100,
  }

  @Action
  addEnergy(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,
    }
  }
}

const reducer = ReducerCat.create()
import { Action, ReducerClass } from 'reducer-class'

class ActionCatEat {
  type = 'ActionCatEat'
  constructor(payload) {
    this.payload = payload
  }
}
class ActionCatPlay {
  type = 'ActionCatPlay'
  constructor(payload) {
    this.payload = payload
  }
}
class ActionCatBeAwesome {
  type = 'ActionCatBeAwesome'
  constructor(payload) {
    this.payload = payload
  }
}

class ReducerCat extends ReducerClass {
  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,
    }
  }
}

const reducer = ReducerCat.create()

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

With redux-actions

import { Action, ReducerClass } from 'reducer-class'
import { createAction } from 'redux-actions'

const actionCatEat = createAction('actionTypeCatEat')
const actionCatPlay = createAction('actionTypeCatPlay')
const actionCatBeAwesome = createAction('actionTypeCatBeAwesome')

interface IReducerCatState {
  energy: number
}
class ReducerCat extends ReducerClass<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,
    }
  }
}

const reducer = ReducerCat.create()

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

import { Action, ReducerClass } from 'reducer-class'
import { createAction } from 'redux-actions'

const actionCatEat = createAction('actionTypeCatEat')
const actionCatPlay = createAction('actionTypeCatPlay')
const actionCatBeAwesome = createAction('actionTypeCatBeAwesome')

class ReducerCat extends ReducerClass {
  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,
    }
  }
}

const reducer = ReducerCat.create()

Old school: action type constants

import { Action, ReducerClass } from 'reducer-class'

const actionTypeCatEat = 'actionTypeCatEat'
const actionTypeCatPlay = 'actionTypeCatPlay'
const actionTypeCatBeAwesome = 'actionTypeCatBeAwesome'

interface IReducerCatState {
  energy: number
}
class ReducerCat extends ReducerClass<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,
    }
  }
}

const reducer = ReducerCat.create()

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

import { Action, ReducerClass } from 'reducer-class'

const actionTypeCatEat = 'actionTypeCatEat'
const actionTypeCatPlay = 'actionTypeCatPlay'
const actionTypeCatBeAwesome = 'actionTypeCatBeAwesome'

class ReducerCat {
  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,
    }
  }
}

const reducer = ReducerCat.create()

Integration with immer

If your reducer expects 3 arguments reducer-class automatically wraps it with produce from immer.

  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'

class ActionCatEat extends ActionStandard<number> {}
class ActionCatPlay extends ActionStandard<number> {}
class ActionCatBeAwesome extends ActionStandard<number> {}

interface IReducerCatState {
  energy: number
}
class ReducerCat extends ReducerClass<IReducerCatState> {
  initialState = {
    energy: 100,
  }

  @Action
  addEnergy(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-void
    return undefined
  }
}

const reducer = ReducerCat.create()

As you can see we still return undefined from the reducer even though we use immer and mutate our draft. Unfortunately, we can not omit return statement here due to how TypeScript handles void. We can not even write return (withour undefined), because TypeScript then presumes the method returns void.

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

Actually it makes total sense to use Immutable 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'

interface IHungryState {
  hungry: boolean
}
export class ReducerHungry<T extends IHungryState> extends ReducerClassMixin<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 used IHungryState directly and returned { hungry: true } (not { ...state, hungry: true }) from hungry compiler wouldn't complain.

You don't have to use ReducerClassMixin class. It's nothing but a convenience wrapper to make sure your class carries an index signature for type-safety. Alternatively you can use IReducerClassConstraint interface and ReducerClassMethod type.

import { Action, IReducerClassConstraint, ReducerClassMethod } from 'reducer-class'

interface IHungryState {
  hungry: boolean
}
export class ReducerHungry<T extends IHungryState> implements IReducerClassConstraint<T> {
  [methodName: string]: ReducerClassMethod<T>

  @Action(ActionHungry)
  hugry(state: T) {
    return {
      ...state,
      hungry: true,
    }
  }

  @Action(ActionFull)
  full(state: T) {
    return {
      ...state,
      hungry: false,
    }
  }
}
import { Action } from 'reducer-class'

export class ReducerHungry {
  @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'

interface ICatState {
  hugry: boolean
  enegry: number
}
@Extend<ICatState>(ReducerHungry)
class CatReducer extends ReducerClass<ICatState> {
  initialState = {
    energy: 100,
  }

  @Action
  addEnergy(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,
    }
  }
}

const reducer = ReducerCat.create()

@Extend can accept as many arguments as you want.

Now our cat reducer uses wasteEnegry to handle actions ActionCatPlay, ActionCatBeAwesome, addEnergy to handle ActionCatEat and inherits hugry and full methods to handle ActionHungry and ActionFull from ReducerHungry.

import { Action, Extend, ReducerClass } from 'reducer-class'

import { ReducerHungry } from 'shared'

@Extend(ReducerHungry)
class CatReducer extends ReducerClass {
  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,
    }
  }
}

const reducer = ReducerCat.create()

How can I make shared reducer's logic dynamic?

You can use class factories.

import { Action, Extend, ReducerClass, ReducerClassMixin } from 'reducer-class'

interface IHungryState {
  hungry: boolean
}
export const makeReducerHungry = <T extends IHungryState>(actionHungry, actionFull) => {
  class Extender1 extends ReducerClassMixin<T> {
    @Action(actionHungry)
    hugry(state: T) {
      return {
        ...state,
        hungry: true,
      }
    }

    @Action(actionFull)
    full(state: T) {
      return {
        ...state,
        hungry: false,
      }
    }
  }
  return Extender1
}

interface ICatState {
  hugry: boolean
  enegry: number
}
@Extend<ICatState>(makeReducerHungry(ActionCatPlay, ActionCatEat))
class CatReducer extends ReducerClass<ICatState> {
  initialState = {
    energy: 100,
  }

  @Action
  addEnergy(state: ICatState, action: ActionCatEat) {
    return {
      energy: state.energy + action.payload,
    }
  }

  @Action
  wasteEnegry(state: ICatState, action: ActionCatPlay) {
    return {
      energy: state.energy - action.payload,
    }
  }
}

const reducer = ReducerCat.create()
import { Action, Extend, ReducerClass } from 'reducer-class'

export const makeReducerHungry = (actionHungry, actionFull) =>
  class {
    @Action(actionHungry)
    hugry(state) {
      return {
        ...state,
        hungry: true,
      }
    }

    @Action(actionFull)
    full(state) {
      return {
        ...state,
        hungry: false,
      }
    }
  }

@Extend(makeReducerHungry(ActionCatPlay, ActionCatEat))
class CatReducer extends ReducerClass {
  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,
    }
  }
}

const reducer = 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 _.

interface ICatState {
  enegry: number
}
class CatReducer extends ReducerClass<ICatState> {
  initialState = {
    energy: 10,
  }

  @Action
  addEnergy(state: ICatState, action: ActionCatEat) {
    return this._addEnergy(state, action)
  }

  // DO NOT FORGET TO PREFIX IT WITH "_"
  protected _addEnergy(state: ICatState, action: ActionCatEat): ICatState {
    return {
      energy: state.energy + action.payload,
    }
  }
}

class KittenReducer extends CatReducer {
  // DO NOT FORGET TO PREFIX IT WITH "_"
  protected _addEnergy(state: ICatState, action: ActionCatEat): ICatState {
    return {
      energy: state.energy + action.payload * 10,
    }
  }
}
class CatReducer extends ReducerClass {
  initialState = {
    energy: 10,
  }

  @Action(ActionCatEat)
  addEnergy(state, action) {
    return this._addEnergy(state, action)
  }

  // DO NOT FORGET TO PREFIX IT WITH "_"
  protected _addEnergy(state, action) {
    return {
      energy: state.energy + action.payload,
    }
  }
}

class KittenReducer extends CatReducer {
  // 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 property type or a property type on the instance of the class.

Running several reducers for the same action

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

import { ActionStandard } from 'flux-action-class'
import { Action, ReducerClass } from 'reducer-class'

class ActionCatEat extends ActionStandard<number> {}
class ActionCatSleep extends ActionStandard<number> {}

interface IReducerCatState {
  energy: number
}
class ReducerCat extends ReducerClass<IReducerCatState> {
  initialState = {
    energy: 100,
  }

  @Action(ActionCatEat, ActionCatSleep)
  addEnergy(state: IReducerCatState, action: ActionCatEat | ActionCatSleep) {
    return {
      energy: state.energy + action.payload,
    }
  }

  @Action
  addMoreEnergy(state: IReducerCatState, action: ActionCatSleep) {
    return {
      energy: state.energy + action.payload * 2,
    }
  }
}

const reducer = ReducerCat.create()

const res1 = reducer(undefined, new ActionCatSleep(10))
console.log(res1.energy) // logs 130: 100 - initial value, 10 is added by addEnergy, 10 * 2 is added by addMoreEnergy
const res2 = reducer(res1, new ActionCatEat(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 to ngrx-actions?

  1. Stricter typings. Now you'll never forget to add initial state, return a new state from your reducer and accidentally invoke immer 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, while reducer-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 supports redux-actions out-of-the-box.