• Stars
    star
    225
  • Rank 171,216 (Top 4 %)
  • Language
    TypeScript
  • License
    MIT License
  • Created over 5 years ago
  • Updated over 1 year ago

Reviews

There are no reviews yet. Be the first to send feedback to the community and the maintainers!

Repository Details

Type-safe and terse reducers with Typescript for React Hooks and Redux

immer-reducer

Type-safe and terse reducers with Typescript for React Hooks and Redux using Immer!

📦 Install

npm install immer-reducer

You can also install eslint-plugin-immer-reducer to help you avoid errors when writing your reducer.

💪 Motivation

Turn this 💩 💩 💩

interface SetFirstNameAction {
    type: "SET_FIRST_NAME";
    firstName: string;
}

interface SetLastNameAction {
    type: "SET_LAST_NAME";
    lastName: string;
}

type Action = SetFirstNameAction | SetLastNameAction;

function reducer(action: Action, state: State): State {
    switch (action.type) {
        case "SET_FIRST_NAME":
            return {
                ...state,
                user: {
                    ...state.user,
                    firstName: action.firstName,
                },
            };
        case "SET_LAST_NAME":
            return {
                ...state,
                user: {
                    ...state.user,
                    lastName: action.lastName,
                },
            };
        default:
            return state;
    }
}

Into this!

import {ImmerReducer} from "immer-reducer";

class MyImmerReducer extends ImmerReducer<State> {
    setFirstName(firstName: string) {
        this.draftState.user.firstName = firstName;
    }

    setLastName(lastName: string) {
        this.draftState.user.lastName = lastName;
    }
}

🔥🔥 Without losing type-safety! 🔥🔥

Oh, and you get the action creators for free! 🤗 🎂

📖 Usage

Generate Action Creators and the actual reducer function for Redux from the class with

import {createStore} from "redux";
import {createActionCreators, createReducerFunction} from "immer-reducer";

const initialState: State = {
    user: {
        firstName: "",
        lastName: "",
    },
};

const ActionCreators = createActionCreators(MyImmerReducer);
const reducerFunction = createReducerFunction(MyImmerReducer, initialState);

const store = createStore(reducerFunction);

Dispatch some actions

store.dispatch(ActionCreators.setFirstName("Charlie"));
store.dispatch(ActionCreators.setLastName("Brown"));

expect(store.getState().user.firstName).toEqual("Charlie");
expect(store.getState().user.lastName).toEqual("Brown");

🌟 Typed Action Creators!

The generated ActionCreator object respect the types used in the class

const action = ActionCreators.setFirstName("Charlie");
action.payload; // Has the type of string

ActionCreators.setFirstName(1); // Type error. Needs string.
ActionCreators.setWAT("Charlie"); // Type error. Unknown method

If the reducer class where to have a method which takes more than one argument the payload would be array of the arguments

// In the Reducer class:
// setName(firstName: string, lastName: string) {}
const action = ActionCreators.setName("Charlie", "Brown");
action.payload; // will have value ["Charlie", "Brown"] and type [string, string]

The reducer function is also typed properly

const reducer = createReducerFunction(MyImmerReducer);

reducer(initialState, ActionCreators.setFirstName("Charlie")); // OK
reducer(initialState, {type: "WAT"}); // Type error
reducer({wat: "bad state"}, ActionCreators.setFirstName("Charlie")); // Type error

React Hooks

Because the useReducer() API in React Hooks is the same as with Redux Reducers immer-reducer can be used with as is.

const initialState = {message: ""};

class ReducerClass extends ImmerReducer<typeof initialState> {
    setMessage(message: string) {
        this.draftState.message = message;
    }
}

const ActionCreators = createActionCreators(ReducerClass);
const reducerFunction = createReducerFunction(ReducerClass);

function Hello() {
    const [state, dispatch] = React.useReducer(reducerFunction, initialState);

    return (
        <button
            data-testid="button"
            onClick={() => {
                dispatch(ActionCreators.setMessage("Hello!"));
            }}
        >
            {state.message}
        </button>
    );
}

The returned state and dispatch functions will be typed as you would expect.

🤔 How

Under the hood the class is deconstructed to following actions:

{
    type: "IMMER_REDUCER:MyImmerReducer#setFirstName",
    payload: "Charlie",
}
{
    type: "IMMER_REDUCER:MyImmerReducer#setLastName",
    payload: "Brown",
}
{
    type: "IMMER_REDUCER:MyImmerReducer#setName",
    payload: ["Charlie", "Brown"],
    args: true
}

So the class and method names become the Redux Action Types and the method arguments become the action payloads. The reducer function will then match these actions against the class and calls the appropriate methods with the payload array spread to the arguments.

🚫 The format of the action.type string is internal to immer-reducer. If you need to detect the actions use the provided type guards.

The generated reducer function executes the methods inside the produce() function of Immer enabling the terse mutatable style updates.

🔄 Integrating with the Redux ecosystem

To integrate for example with the side effects libraries such as redux-observable and redux-saga, you can access the generated action type using the type property of the action creator function.

With redux-observable

// Get the action name to subscribe to
const setFirstNameActionTypeName = ActionCreators.setFirstName.type;

// Get the action type to have a type safe Epic
type SetFirstNameAction = ReturnType<typeof ActionCreators.setFirstName>;

const setFirstNameEpic: Epic<SetFirstNameAction> = action$ =>
  action$
    .ofType(setFirstNameActionTypeName)
    .pipe(
      // action.payload - recognized as string
      map(action => action.payload.toUpperCase()),
      ...
    );

With redux-saga

function* watchFirstNameChanges() {
    yield takeEvery(ActionCreators.setFirstName.type, doStuff);
}

// or use the isActionFrom() to get all actions from a specific ImmerReducer
// action creators object
function* watchImmerActions() {
    yield takeEvery(
        (action: Action) => isActionFrom(action, MyImmerReducer),
        handleImmerReducerAction,
    );
}

function* handleImmerReducerAction(action: Actions<typeof MyImmerReducer>) {
    // `action` is a union of action types
    if (isAction(action, ActionCreators.setFirstName)) {
        // with action of setFirstName
    }
}

Warning: Due to how immer-reducers action generation works, adding default parameters to the methods will NOT pass it to the action payload, which can make your reducer impure and the values will not be available in middlewares.

class MyImmerReducer extends ImmerReducer<State> {
    addItem (id: string = uuid()) {
        this.draftState.ids.push([id])
    }
}

immerActions.addItem() // generates empty payload { payload: [] }

As a workaround, create custom action creator wrappers that pass the default parameters instead.

class MyImmerReducer extends ImmerReducer<State> {
    addItem (id) {
        this.draftState.ids.push([id])
    }
}

const actions = {
  addItem: () => immerActions.addItem(id)
}

It is also recommended to install the ESLint plugin in the "Install" section to alert you if you accidentally encounter this issue.

📚 Examples

Here's a more complete example with redux-saga and redux-render-prop:

https://github.com/epeli/typescript-redux-todoapp

🃏 Tips and Tricks

You can replace the whole draftState with a new state if you'd like. This could be useful if you'd like to reset back to your initial state.

import {ImmerReducer} from "immer-reducer";

const initialState: State = {
    user: {
        firstName: "",
        lastName: "",
    },
};

class MyImmerReducer extends ImmerReducer<State> {
    // omitting other reducer methods
    
    reset() {
        this.draftState = initialState;
    }
}

📓 Helpers

The module exports following helpers

function isActionFrom(action, ReducerClass)

Type guard for detecting whether the given action is generated by the given reducer class. The detected type will be union of actions the class generates.

Example

if (isActionFrom(someAction, ActionCreators)) {
    // someAction now has type of
    // {
    //     type: "setFirstName";
    //     payload: string;
    // } | {
    //     type: "setLastName";
    //     payload: string;
    // };
}

function isAction(action, actionCreator)

Type guard for detecting specific actions generated by immer-reducer.

Example

if (isAction(someAction, ActionCreators.setFirstName)) {
    someAction.payload; // Type checks to `string`
}

type Actions<ImmerReducerClass>

Get union of the action types generated by the ImmerReducer class

Example

type MyActions = Actions<typeof MyImmerReducer>;

// Is the same as
type MyActions =
    | {
          type: "setFirstName";
          payload: string;
      }
    | {
          type: "setLastName";
          payload: string;
      };

function setPrefix(prefix: string)

The default prefix in the generated action types is IMMER_REDUCER. Call this customize it for your app.

Example

setPrefix("MY_APP");

function composeReducers<State>(...reducers)

Utility that reduces actions by applying them through multiple reducers. This helps in allowing you to split up your reducer logic to multiple ImmerReducers if they affect the same part of your state

Example

class MyNameReducer extends ImmerReducer<NamesState> {
    setFirstName(firstName: string) {
        this.draftState.firstName = firstName;
    }

    setLastName(lastName: string) {
        this.draftState.lastName = lastName;
    }
}

class MyAgeReducer extends ImmerReducer<AgeState> {
    setAge(age: number) {
        this.draftState.age = 8;
    }
}

export const reducer = composeReducers(
  createReducerFunction(MyNameReducer, initialState),
  createReducerFunction(MyAgeReducer, initialState)
)

More Repositories

1

underscore.string

String manipulation helpers for javascript
JavaScript
3,372
star
2

react-zorm

🪱 Zorm - Type-safe <form> for React using Zod
TypeScript
714
star
3

node-hbsfy

Handlebars precompiler plugin for Browserify
JavaScript
257
star
4

slimux

SLIME inspired tmux integration plugin for Vim
Vim Script
216
star
5

piler

Deprecated Asset Manager for Node.js
CoffeeScript
148
star
6

redux-hooks

⚓ React Hooks implementation for Redux
TypeScript
96
star
7

node-promisepipe

Safely pipe node.js streams while capturing all errors to a single promise
JavaScript
80
star
8

requirejs-hbs

Simple Handlebars loader plugin for RequireJS
JavaScript
79
star
9

lean-redux

Redux state like local component state
JavaScript
78
star
10

angry-caching-proxy

Make package downloads lightning fast!
JavaScript
77
star
11

browserify-externalize

Create external Browserify bundles for lazy asynchronous loading
JavaScript
73
star
12

postcss-ts-classnames

PostCSS plugin to generate TypeScript types from your CSS class names.
TypeScript
70
star
13

jslibs

List of Javascript libraries
59
star
14

backbone.viewmaster

Few tested opinions on how to handle deeply nested views in Backbone.js focusing on modularity.
JavaScript
52
star
15

babel-plugin-ts-optchain

Babel plugin for transpiling legacy browser support to ts-optchain by removing Proxy usage.
TypeScript
30
star
16

trpc-cloudflare-worker

TypeScript
29
star
17

carcounter

Asynchronous module loading example with Browserify
JavaScript
24
star
18

redux-render-prop

Redux with render props. Typescript friendly.
TypeScript
22
star
19

source-map-peek

Peek into original source via source maps from the command line when devtools fail.
JavaScript
21
star
20

TextareaServer

Server for opening external text editors from Chrome
CoffeeScript
19
star
21

deno_karabiner

Write Complex Modifications for Karabiner-Elements using Typescript and Deno.
TypeScript
17
star
22

babel-plugin-display-name-custom

display name inference for your custom react component creators
JavaScript
15
star
23

geekslides

Remote controllable Node.js introduction slides
JavaScript
15
star
24

npm-release

Github action for npm (pre)releases
TypeScript
15
star
25

sauna.reload

Development & Issues MOVED to https://github.com/collective/sauna.reload
Python
13
star
26

TextareaConnect

Edit textareas in Google Chrome using any external editor!
CoffeeScript
13
star
27

node-clim

Console.Log IMproved for Node.js
JavaScript
12
star
28

react-simple

simple style only components for the web & native
JavaScript
9
star
29

typescript-redux-todoapp

Type-safe Boilerplate-free Redux Example
TypeScript
9
star
30

jquery.panfullsize

jQuery plugin for panning large images
JavaScript
8
star
31

neovim-config

Vim Script
8
star
32

Projectwatch

Watch file changes and run multiple css/js pre-processors from one watcher
JavaScript
8
star
33

multip

Tiny multi process init for containers written in Rust 🦀
Rust
8
star
34

ts-box

Put exceptions into boxes 📦
TypeScript
8
star
35

vimconfig

Vim and other dotfiles
Vim Script
7
star
36

node-tftp

Streaming TFTP Server for node.js
JavaScript
7
star
37

react-makefile

Makefile boilerplate for React.js
JavaScript
6
star
38

yalr

YALR - Yet Another Live Reload
JavaScript
6
star
39

vscode-unsaved

Makes you actually notice when you have unsaved files
TypeScript
5
star
40

reallyexpress

Really Express extends Express with extraordinary features given by Node.js. Work in progress.
CoffeeScript
5
star
41

shell-cheatsheet

4
star
42

tmpshare

Easily share temporary files over http
JavaScript
4
star
43

ircshare

Simple service for sharing your pictures on IRC
JavaScript
3
star
44

ircshare-android

Share pictures easily in IRC from Android phone
Java
3
star
45

qdomain

Promises from domains
JavaScript
3
star
46

browserify-cs-example

Browserify v2 with CoffeeScript Source Maps
CoffeeScript
3
star
47

flysight-subtitles

Convert FlySight data files (.csv) to SubRip (.srt) subtitles.
JavaScript
3
star
48

node-handlebars-runtime

Handlebars runtime only
JavaScript
2
star
49

node-lights

Instanssi Light Server Simulator
CoffeeScript
2
star
50

revisioncask

Version control management made simple
Python
2
star
51

flips.io

JavaScript
2
star
52

subssh

Small framework for creating SSH public key based shell accounts.
Python
2
star
53

toggl-paster

I need to paste Toggl time entries into things so I made a thing.
TypeScript
2
star
54

utils

Epeli's Javascript / Typescript utilities
TypeScript
2
star
55

MAILNETD

Meta Asynchronous Irc Linked Network Email Transport Daemon
Python
1
star
56

rt

Instant tab-complete for npm scripts and Jakefiles
Rust
1
star
57

lunarvim-config

Lua
1
star
58

awesome

My Personal Awesome WM config
Lua
1
star
59

wp-graphql-todoapp

TypeScript
1
star
60

stylify

JavaScript
1
star
61

subssh_buildout

buildout for subssh development purposes
Python
1
star
62

LightManager

lightmanager tool written for instanssi.org party
Java
1
star
63

rollsum

Just experiments with rolling checksums. Nothing to see here.
C
1
star
64

sh-thunk

Generate promise returning thunks from shell strings.
JavaScript
1
star
65

konf

TypeScript
1
star
66

HAL2012

Home hack using Node.js and Raspberry Pi
CoffeeScript
1
star
67

IRCPost

Simple IRC bot with HTTP POST API
CoffeeScript
1
star
68

blog

JavaScript
1
star
69

aswyg-editor

ASWYG Editor - Also See What You Get Editor
JavaScript
1
star