{"id":17138174,"url":"https://github.com/esamattis/immer-reducer","last_synced_at":"2025-10-06T11:20:08.107Z","repository":{"id":53580198,"uuid":"156121502","full_name":"esamattis/immer-reducer","owner":"esamattis","description":"Type-safe and terse reducers with Typescript for React Hooks and Redux","archived":false,"fork":false,"pushed_at":"2022-11-18T13:42:40.000Z","size":293,"stargazers_count":224,"open_issues_count":11,"forks_count":15,"subscribers_count":4,"default_branch":"master","last_synced_at":"2025-04-10T01:08:36.109Z","etag":null,"topics":["react","redux","typescript"],"latest_commit_sha":null,"homepage":"http://npm.im/immer-reducer","language":"TypeScript","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":"mit","status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/esamattis.png","metadata":{"files":{"readme":"README.md","changelog":"CHANGELOG.md","contributing":null,"funding":null,"license":"LICENSE","code_of_conduct":null,"threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":null,"support":null}},"created_at":"2018-11-04T20:24:04.000Z","updated_at":"2025-03-26T13:42:46.000Z","dependencies_parsed_at":"2023-01-22T02:15:53.874Z","dependency_job_id":null,"html_url":"https://github.com/esamattis/immer-reducer","commit_stats":null,"previous_names":["epeli/immer-reducer"],"tags_count":28,"template":false,"template_full_name":null,"repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/esamattis%2Fimmer-reducer","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/esamattis%2Fimmer-reducer/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/esamattis%2Fimmer-reducer/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/esamattis%2Fimmer-reducer/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/esamattis","download_url":"https://codeload.github.com/esamattis/immer-reducer/tar.gz/refs/heads/master","host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":248137887,"owners_count":21053775,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2022-07-04T15:15:14.044Z","host_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub","repositories_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories","repository_names_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repository_names","owners_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners"}},"keywords":["react","redux","typescript"],"created_at":"2024-10-14T20:08:59.246Z","updated_at":"2025-10-06T11:20:03.035Z","avatar_url":"https://github.com/esamattis.png","language":"TypeScript","funding_links":[],"categories":[],"sub_categories":[],"readme":"# immer-reducer\n\nType-safe and terse reducers with Typescript for React Hooks and Redux using [Immer](https://immerjs.github.io/immer/)!\n\n## 📦 Install\n\n    npm install immer-reducer\n\nYou can also install [eslint-plugin-immer-reducer](https://github.com/skoshy/eslint-plugin-immer-reducer) to help you avoid errors when writing your reducer.\n\n## 💪 Motivation\n\nTurn this 💩 💩 💩\n\n```ts\ninterface SetFirstNameAction {\n    type: \"SET_FIRST_NAME\";\n    firstName: string;\n}\n\ninterface SetLastNameAction {\n    type: \"SET_LAST_NAME\";\n    lastName: string;\n}\n\ntype Action = SetFirstNameAction | SetLastNameAction;\n\nfunction reducer(action: Action, state: State): State {\n    switch (action.type) {\n        case \"SET_FIRST_NAME\":\n            return {\n                ...state,\n                user: {\n                    ...state.user,\n                    firstName: action.firstName,\n                },\n            };\n        case \"SET_LAST_NAME\":\n            return {\n                ...state,\n                user: {\n                    ...state.user,\n                    lastName: action.lastName,\n                },\n            };\n        default:\n            return state;\n    }\n}\n```\n\n✨✨ Into this! ✨✨\n\n```ts\nimport {ImmerReducer} from \"immer-reducer\";\n\nclass MyImmerReducer extends ImmerReducer\u003cState\u003e {\n    setFirstName(firstName: string) {\n        this.draftState.user.firstName = firstName;\n    }\n\n    setLastName(lastName: string) {\n        this.draftState.user.lastName = lastName;\n    }\n}\n```\n\n🔥🔥 **Without losing type-safety!** 🔥🔥\n\nOh, and you get the action creators for free! 🤗 🎂\n\n## 📖 Usage\n\nGenerate Action Creators and the actual reducer function for Redux from the class with\n\n```ts\nimport {createStore} from \"redux\";\nimport {createActionCreators, createReducerFunction} from \"immer-reducer\";\n\nconst initialState: State = {\n    user: {\n        firstName: \"\",\n        lastName: \"\",\n    },\n};\n\nconst ActionCreators = createActionCreators(MyImmerReducer);\nconst reducerFunction = createReducerFunction(MyImmerReducer, initialState);\n\nconst store = createStore(reducerFunction);\n```\n\nDispatch some actions\n\n```ts\nstore.dispatch(ActionCreators.setFirstName(\"Charlie\"));\nstore.dispatch(ActionCreators.setLastName(\"Brown\"));\n\nexpect(store.getState().user.firstName).toEqual(\"Charlie\");\nexpect(store.getState().user.lastName).toEqual(\"Brown\");\n```\n\n## 🌟 Typed Action Creators!\n\nThe generated `ActionCreator` object respect the types used in the class\n\n```ts\nconst action = ActionCreators.setFirstName(\"Charlie\");\naction.payload; // Has the type of string\n\nActionCreators.setFirstName(1); // Type error. Needs string.\nActionCreators.setWAT(\"Charlie\"); // Type error. Unknown method\n```\n\nIf the reducer class where to have a method which takes more than one argument\nthe payload would be array of the arguments\n\n```ts\n// In the Reducer class:\n// setName(firstName: string, lastName: string) {}\nconst action = ActionCreators.setName(\"Charlie\", \"Brown\");\naction.payload; // will have value [\"Charlie\", \"Brown\"] and type [string, string]\n```\n\nThe reducer function is also typed properly\n\n```ts\nconst reducer = createReducerFunction(MyImmerReducer);\n\nreducer(initialState, ActionCreators.setFirstName(\"Charlie\")); // OK\nreducer(initialState, {type: \"WAT\"}); // Type error\nreducer({wat: \"bad state\"}, ActionCreators.setFirstName(\"Charlie\")); // Type error\n```\n\n## ⚓ React Hooks\n\nBecause the `useReducer()` API in React Hooks is the same as with Redux\nReducers immer-reducer can be used with as is.\n\n```tsx\nconst initialState = {message: \"\"};\n\nclass ReducerClass extends ImmerReducer\u003ctypeof initialState\u003e {\n    setMessage(message: string) {\n        this.draftState.message = message;\n    }\n}\n\nconst ActionCreators = createActionCreators(ReducerClass);\nconst reducerFunction = createReducerFunction(ReducerClass);\n\nfunction Hello() {\n    const [state, dispatch] = React.useReducer(reducerFunction, initialState);\n\n    return (\n        \u003cbutton\n            data-testid=\"button\"\n            onClick={() =\u003e {\n                dispatch(ActionCreators.setMessage(\"Hello!\"));\n            }}\n        \u003e\n            {state.message}\n        \u003c/button\u003e\n    );\n}\n```\n\nThe returned state and dispatch functions will be typed as you would expect.\n\n## 🤔 How\n\nUnder the hood the class is deconstructed to following actions:\n\n```js\n{\n    type: \"IMMER_REDUCER:MyImmerReducer#setFirstName\",\n    payload: \"Charlie\",\n}\n{\n    type: \"IMMER_REDUCER:MyImmerReducer#setLastName\",\n    payload: \"Brown\",\n}\n{\n    type: \"IMMER_REDUCER:MyImmerReducer#setName\",\n    payload: [\"Charlie\", \"Brown\"],\n    args: true\n}\n```\n\nSo the class and method names become the Redux Action Types and the method\narguments become the action payloads. The reducer function will then match\nthese actions against the class and calls the appropriate methods with the\npayload array spread to the arguments.\n\n🚫 The format of the `action.type` string is internal to immer-reducer. If\nyou need to detect the actions use the provided type guards.\n\nThe generated reducer function executes the methods inside the `produce()`\nfunction of Immer enabling the terse mutatable style updates.\n\n## 🔄 Integrating with the Redux ecosystem\n\nTo integrate for example with the side effects libraries such as\n[redux-observable](https://github.com/redux-observable/redux-observable/) and\n[redux-saga](https://github.com/redux-saga/redux-saga), you can access the\ngenerated action type using the `type` property of the action creator\nfunction.\n\nWith redux-observable\n\n```ts\n// Get the action name to subscribe to\nconst setFirstNameActionTypeName = ActionCreators.setFirstName.type;\n\n// Get the action type to have a type safe Epic\ntype SetFirstNameAction = ReturnType\u003ctypeof ActionCreators.setFirstName\u003e;\n\nconst setFirstNameEpic: Epic\u003cSetFirstNameAction\u003e = action$ =\u003e\n  action$\n    .ofType(setFirstNameActionTypeName)\n    .pipe(\n      // action.payload - recognized as string\n      map(action =\u003e action.payload.toUpperCase()),\n      ...\n    );\n```\n\nWith redux-saga\n\n```ts\nfunction* watchFirstNameChanges() {\n    yield takeEvery(ActionCreators.setFirstName.type, doStuff);\n}\n\n// or use the isActionFrom() to get all actions from a specific ImmerReducer\n// action creators object\nfunction* watchImmerActions() {\n    yield takeEvery(\n        (action: Action) =\u003e isActionFrom(action, MyImmerReducer),\n        handleImmerReducerAction,\n    );\n}\n\nfunction* handleImmerReducerAction(action: Actions\u003ctypeof MyImmerReducer\u003e) {\n    // `action` is a union of action types\n    if (isAction(action, ActionCreators.setFirstName)) {\n        // with action of setFirstName\n    }\n}\n```\n\n**Warning:** Due to how immer-reducers action generation works, adding default\nparameters to the methods will NOT pass it to the action payload, which can\nmake your reducer impure and the values will not be available in middlewares.\n\n```ts\nclass MyImmerReducer extends ImmerReducer\u003cState\u003e {\n    addItem (id: string = uuid()) {\n        this.draftState.ids.push([id])\n    }\n}\n\nimmerActions.addItem() // generates empty payload { payload: [] }\n```\n\nAs a workaround, create custom action creator wrappers that pass the default parameters instead.\n\n```ts\nclass MyImmerReducer extends ImmerReducer\u003cState\u003e {\n    addItem (id) {\n        this.draftState.ids.push([id])\n    }\n}\n\nconst actions = {\n  addItem: () =\u003e immerActions.addItem(id)\n}\n```\n\nIt is also recommended to install the ESLint plugin in the \"Install\" section\nto alert you if you accidentally encounter this issue.\n\n## 📚 Examples\n\nHere's a more complete example with redux-saga and [redux-render-prop](https://github.com/epeli/redux-render-prop):\n\n\u003chttps://github.com/epeli/typescript-redux-todoapp\u003e\n\n## 🃏 Tips and Tricks\n\nYou 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.\n\n```ts\nimport {ImmerReducer} from \"immer-reducer\";\n\nconst initialState: State = {\n    user: {\n        firstName: \"\",\n        lastName: \"\",\n    },\n};\n\nclass MyImmerReducer extends ImmerReducer\u003cState\u003e {\n    // omitting other reducer methods\n    \n    reset() {\n        this.draftState = initialState;\n    }\n}\n```\n\n## 📓 Helpers\n\nThe module exports following helpers\n\n### `function isActionFrom(action, ReducerClass)`\n\nType guard for detecting whether the given action is generated by the given\nreducer class. The detected type will be union of actions the class\ngenerates.\n\nExample\n\n```ts\nif (isActionFrom(someAction, ActionCreators)) {\n    // someAction now has type of\n    // {\n    //     type: \"setFirstName\";\n    //     payload: string;\n    // } | {\n    //     type: \"setLastName\";\n    //     payload: string;\n    // };\n}\n```\n\n### `function isAction(action, actionCreator)`\n\nType guard for detecting specific actions generated by immer-reducer.\n\nExample\n\n```ts\nif (isAction(someAction, ActionCreators.setFirstName)) {\n    someAction.payload; // Type checks to `string`\n}\n```\n\n### `type Actions\u003cImmerReducerClass\u003e`\n\nGet union of the action types generated by the ImmerReducer class\n\nExample\n\n```ts\ntype MyActions = Actions\u003ctypeof MyImmerReducer\u003e;\n\n// Is the same as\ntype MyActions =\n    | {\n          type: \"setFirstName\";\n          payload: string;\n      }\n    | {\n          type: \"setLastName\";\n          payload: string;\n      };\n```\n\n### `function setPrefix(prefix: string)`\n\nThe default prefix in the generated action types is `IMMER_REDUCER`. Call\nthis customize it for your app.\n\nExample\n\n```ts\nsetPrefix(\"MY_APP\");\n```\n\n### `function composeReducers\u003cState\u003e(...reducers)`\n\nUtility that reduces actions by applying them through multiple reducers.\nThis helps in allowing you to split up your reducer logic to multiple `ImmerReducer`s\nif they affect the same part of your state\n\nExample\n\n```ts\nclass MyNameReducer extends ImmerReducer\u003cNamesState\u003e {\n    setFirstName(firstName: string) {\n        this.draftState.firstName = firstName;\n    }\n\n    setLastName(lastName: string) {\n        this.draftState.lastName = lastName;\n    }\n}\n\nclass MyAgeReducer extends ImmerReducer\u003cAgeState\u003e {\n    setAge(age: number) {\n        this.draftState.age = 8;\n    }\n}\n\nexport const reducer = composeReducers(\n  createReducerFunction(MyNameReducer, initialState),\n  createReducerFunction(MyAgeReducer, initialState)\n)\n```\n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fesamattis%2Fimmer-reducer","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Fesamattis%2Fimmer-reducer","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fesamattis%2Fimmer-reducer/lists"}