├── .editorconfig ├── .gitignore ├── .npmignore ├── .travis.yml ├── README.md ├── index.d.ts ├── index.js ├── package.json ├── test.js └── ts-test.ts /.editorconfig: -------------------------------------------------------------------------------- 1 | # http://editorconfig.org 2 | root = true 3 | 4 | [*] 5 | indent_style = space 6 | indent_size = 2 7 | charset = utf-8 8 | trim_trailing_whitespace = true 9 | insert_final_newline = true 10 | 11 | [*.md] 12 | trim_trailing_whitespace = false 13 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules/ 2 | coverage/ 3 | tester 4 | -------------------------------------------------------------------------------- /.npmignore: -------------------------------------------------------------------------------- 1 | coverage/ 2 | node_modules/ 3 | .travis.yml 4 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: node_js 2 | node_js: 3 | - 10 4 | - 12 5 | - 13 6 | - 14 7 | - lts/* 8 | script: "npm run-script test-travis" 9 | # Send coverage data to Coveralls 10 | after_script: "cat ./coverage/lcov.info | ./node_modules/coveralls/bin/coveralls.js" 11 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # redux-create-reducer 2 | 3 | [![NPM version][npm-image]][npm-url] 4 | [![Build status][travis-image]][travis-url] 5 | [![Test coverage][coveralls-image]][coveralls-url] 6 | [![Downloads][downloads-image]][downloads-url] 7 | 8 | [This code packaged as a node module](http://redux.js.org/docs/recipes/ReducingBoilerplate.html#generating-reducers) 9 | 10 | Usage: 11 | 12 | ```js 13 | import { createReducer } from 'redux-create-reducer'; 14 | import * as ActionTypes from '../constants/ActionTypes'; 15 | 16 | const initialState = []; 17 | 18 | export const todos = createReducer(initialState, { 19 | [ActionTypes.ADD_TODO](state, action) { 20 | const text = action.text.trim(); 21 | return [...state, text]; 22 | }, 23 | 24 | [ActionTypes.REMOVE_TODO](state, action) { 25 | return state.filter((_, i) => i !== action.index); 26 | } 27 | 28 | // All other action types result in state being returned 29 | }) 30 | ``` 31 | 32 | ## Typescript typings 33 | 34 | This library also provides powerful typescript typings when using Action classes: 35 | 36 | ```ts 37 | interface Action { 38 | type: string; 39 | } 40 | 41 | interface State { 42 | value: number; 43 | } 44 | 45 | class Reset implements Action { 46 | readonly type = 'Reset Action'; 47 | } 48 | class AddOne implements Action { 49 | readonly type = 'AddOne Action'; 50 | } 51 | class AddCustom implements Action { 52 | readonly type = 'AddCustom Action'; 53 | constructor(public readonly value: number) { } 54 | } 55 | 56 | type Actions = Reset | AddOne | AddCustom; 57 | 58 | const reducer = createReducer({ value: 0 }, { 59 | 'Reset Action': (state, action) => ({ value: 0 }), 60 | 'AddOne Action': (state, action) => ({ value: state.value + 1 }), 61 | 'AddCustom Action': (state, action) => ({ value: state.value + action.value }), 62 | }); 63 | 64 | // If you wanted to exclude some actions you can use the `Exclude` type. 65 | type ActionsWithoutAddOne = Exclude 66 | const reducerThatDoesNotHandleAddOne = createReducer({ value: 0 }, { 67 | 'Reset Action': (state, action) => ({ value: 0 }), 68 | 'AddCustom Action': (state, action) => ({ value: state.value + action.value }), 69 | }); 70 | ``` 71 | [Stackblitz](https://stackblitz.com/edit/typescript-b5ekwc?file=index.ts) 72 | 73 | Removing `'Reset Action': (state, action) => ({ value: 0 }),` in the reducer causes the error: `Property '"Reset Action"' is missing in type ...`. Similarly, adding `'Nonexisting Action': (state, action) => ({ value: 0 }),` causes the type error: `Object literal may only specify known properties, and ''Nonexisting Action'' does not exist in type 'Handlers'.ts(2345)` 74 | 75 | [npm-image]: https://img.shields.io/npm/v/redux-create-reducer.svg?style=flat-square 76 | [npm-url]: https://npmjs.org/package/redux-create-reducer 77 | [travis-image]: https://img.shields.io/travis/kolodny/redux-create-reducer.svg?style=flat-square 78 | [travis-url]: https://travis-ci.org/kolodny/redux-create-reducer 79 | [coveralls-image]: https://img.shields.io/coveralls/kolodny/redux-create-reducer.svg?style=flat-square 80 | [coveralls-url]: https://coveralls.io/r/kolodny/redux-create-reducer 81 | [downloads-image]: http://img.shields.io/npm/dm/redux-create-reducer.svg?style=flat-square 82 | [downloads-url]: https://npmjs.org/package/redux-create-reducer 83 | -------------------------------------------------------------------------------- /index.d.ts: -------------------------------------------------------------------------------- 1 | interface Action { 2 | type: T; 3 | } 4 | interface AnyAction extends Action { 5 | [extraProps: string]: any; 6 | } 7 | declare type Reducer = (state: S, action: A) => S; 8 | declare type Handlers = { 9 | [K in A['type']]: Reducer ? A : never>; 10 | }; 11 | 12 | export declare function createReducer( 13 | initialState: S, 14 | handlers: Handlers, 15 | ): (state: S | undefined, action: A) => S; 16 | export {} 17 | -------------------------------------------------------------------------------- /index.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | 3 | var __DEV__ = false; 4 | try { 5 | __DEV__ = process.env.NODE_ENV !== 'production'; 6 | } catch (e) {} 7 | 8 | exports.createReducer = function createReducer(initialState, handlers) { 9 | if (__DEV__ && handlers['undefined']) { 10 | console.warn( 11 | 'Reducer contains an \'undefined\' action type. ' + 12 | 'Have you misspelled a constant?' 13 | ) 14 | } 15 | 16 | return function reducer(state, action) { 17 | if (state === undefined) state = initialState; 18 | 19 | if (handlers.hasOwnProperty(action.type)) { 20 | return handlers[action.type](state, action); 21 | } else { 22 | return state; 23 | } 24 | }; 25 | }; 26 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "redux-create-reducer", 3 | "version": "2.0.1", 4 | "description": "Publishing createReducer from https://redux.js.org/recipes/reducing-boilerplate#generating-reducers", 5 | "main": "./index.js", 6 | "scripts": { 7 | "test-cov": "node ./node_modules/istanbul/lib/cli.js cover -x test.js node_modules/mocha/bin/_mocha 'test.js' -- --reporter dot", 8 | "test-travis": "node ./node_modules/istanbul/lib/cli.js cover -x test.js ./node_modules/mocha/bin/_mocha 'test.js' -- -R spec && npm run ts-test", 9 | "ts-test": "tsc --noEmit ts-test", 10 | "test": "mocha && npm run ts-test" 11 | }, 12 | "author": "Moshe Kolodny", 13 | "license": "MIT", 14 | "repository": { 15 | "type": "git", 16 | "url": "git+https://github.com/kolodny/redux-create-reducer.git" 17 | }, 18 | "keywords": [ 19 | "redux", 20 | "reducer" 21 | ], 22 | "bugs": { 23 | "url": "https://github.com/kolodny/redux-create-reducer/issues" 24 | }, 25 | "types": "index.d.ts", 26 | "homepage": "https://github.com/kolodny/redux-create-reducer#readme", 27 | "devDependencies": { 28 | "babel-core": "^6.26.3", 29 | "coveralls": "^3.0.2", 30 | "expect": "^1.14.0", 31 | "istanbul": "^0.4.5", 32 | "mocha": "^5.2.0", 33 | "redux": "^4.0.1", 34 | "typescript": "^3.3.3333" 35 | }, 36 | "peerDependencies": { 37 | "redux": "*" 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /test.js: -------------------------------------------------------------------------------- 1 | var expect = require('expect'); 2 | var createReducer = require('./').createReducer; 3 | 4 | describe('createReducer', function() { 5 | 6 | it('returns the inital state on the first call with no matches', function() { 7 | var reducerMap = {}; 8 | var reducer = createReducer('theintialstate', reducerMap); 9 | expect(reducer(undefined, {type: 'YOLO'})).toEqual('theintialstate'); 10 | }); 11 | 12 | it('returns the correct state on the first call with a matche', function() { 13 | var reducerMap = { 14 | YOLO: function() { 15 | return 'theproperstate'; 16 | } 17 | }; 18 | var reducer = createReducer('theintialstate', reducerMap); 19 | expect(reducer(undefined, {type: 'YOLO'})).toEqual('theproperstate'); 20 | }); 21 | 22 | it('returns the same state if no value matched', function() { 23 | var reducerMap = {}; 24 | var reducer = createReducer({someObj: true}, reducerMap); 25 | var state = reducer(undefined, {type: 'YOLO'}); 26 | expect(reducer(state, {type: 'YOLO'})).toEqual(state); 27 | }); 28 | 29 | it('returns a new state if a value matched', function() { 30 | var reducerMap = { 31 | YOLO: function() { 32 | return {someObj: 2}; 33 | } 34 | }; 35 | var reducer = createReducer({someObj: 1}, reducerMap); 36 | var state = reducer(undefined, {}); 37 | expect(reducer(state, {type: 'YOLO'})).toEqual({someObj: 2}); 38 | }); 39 | 40 | it('warns about undefined action type names, but keeps them intact', function() { 41 | var YOLO = undefined; 42 | var reducerMap = { 43 | [YOLO]: function() { 44 | return 'theproperstate'; 45 | } 46 | }; 47 | var spy = expect.spyOn(console, 'warn') 48 | var reducer = createReducer({}, reducerMap); 49 | spy.restore(); 50 | expect(spy).toHaveBeenCalled() 51 | expect(reducer(undefined, {type: 'undefined'})).toEqual('theproperstate'); 52 | }); 53 | 54 | it('does not warn about undefined action type names if not in development mode', function() { 55 | var NODE_ENV = process.env.NODE_ENV; 56 | process.env.NODE_ENV = 'production'; 57 | delete require.cache[require.resolve('./')]; 58 | 59 | // locally scoped version, should be fine 60 | var createReducer = require('./').createReducer; 61 | 62 | var YOLO = undefined; 63 | var reducerMap = { 64 | [YOLO]: function() { 65 | return 'theproperstate'; 66 | } 67 | }; 68 | var spy = expect.spyOn(console, 'warn') 69 | var reducer = createReducer({}, reducerMap); 70 | process.env.NODE_ENV = NODE_ENV; 71 | spy.restore(); 72 | expect(spy).toNotHaveBeenCalled() 73 | }); 74 | 75 | }); 76 | -------------------------------------------------------------------------------- /ts-test.ts: -------------------------------------------------------------------------------- 1 | import { createReducer } from './' 2 | 3 | interface Action { 4 | type: string; 5 | } 6 | 7 | interface State { 8 | value: number; 9 | } 10 | 11 | class Reset implements Action { 12 | readonly type = 'Reset Action'; 13 | } 14 | class AddOne implements Action { 15 | readonly type = 'AddOne Action'; 16 | } 17 | class AddCustom implements Action { 18 | readonly type = 'AddCustom Action'; 19 | constructor(public readonly value: number) { } 20 | } 21 | 22 | type Actions = Reset | AddOne | AddCustom; 23 | 24 | const reducer = createReducer({ value: 0 }, { 25 | 'Reset Action': (state, action) => ({ value: 0 }), 26 | 'AddOne Action': (state, action) => ({ value: state.value + 1 }), 27 | 'AddCustom Action': (state, action) => ({ value: state.value + action.value }), 28 | }); 29 | 30 | type ActionsWithoutAddOne = Exclude 31 | const reducerThatDoesNotHandleAddOne = createReducer({ value: 0 }, { 32 | 'Reset Action': (state, action) => ({ value: 0 }), 33 | 'AddCustom Action': (state, action) => ({ value: state.value + action.value }), 34 | }); 35 | 36 | 37 | 38 | 39 | // Make sure the old usage still works: 40 | 41 | const ActionTypes = { 42 | ADD_TODO: 'ADD TODO', 43 | REMOVE_TODO: 'REMOVE TODO' 44 | } 45 | const initialState = []; 46 | 47 | export const todos = createReducer(initialState, { 48 | [ActionTypes.ADD_TODO](state, action) { 49 | const text = action.text.trim(); 50 | return [...state, text]; 51 | }, 52 | 53 | [ActionTypes.REMOVE_TODO](state, action) { 54 | return state.filter((_, i) => i !== action.index); 55 | } 56 | 57 | // All other action types result in state being returned 58 | }); 59 | --------------------------------------------------------------------------------