├── .babelrc ├── .eslintrc ├── .gitignore ├── LICENSE.md ├── README.md ├── package.json ├── src └── index.js ├── test ├── index.js └── reducers.js └── webpack.config.babel.js /.babelrc: -------------------------------------------------------------------------------- 1 | { 2 | "plugins": [ 3 | ["transform-es2015-template-literals", { "loose": true }], 4 | "transform-es2015-literals", 5 | "transform-es2015-function-name", 6 | "transform-es2015-arrow-functions", 7 | "transform-es2015-block-scoped-functions", 8 | ["transform-es2015-classes", { "loose": true }], 9 | "transform-es2015-object-super", 10 | "transform-es2015-shorthand-properties", 11 | ["transform-es2015-computed-properties", { "loose": true }], 12 | ["transform-es2015-for-of", { "loose": true }], 13 | "transform-es2015-sticky-regex", 14 | "transform-es2015-unicode-regex", 15 | "check-es2015-constants", 16 | ["transform-es2015-spread", { "loose": true }], 17 | "transform-es2015-parameters", 18 | ["transform-es2015-destructuring", { "loose": true }], 19 | "transform-es2015-block-scoping", 20 | "transform-es3-member-expression-literals", 21 | "transform-es3-property-literals", 22 | ], 23 | "env": { 24 | "commonjs": { 25 | "plugins": [ 26 | ["transform-es2015-modules-commonjs", { "loose": true }], 27 | ] 28 | }, 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /.eslintrc: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "eslint-config-airbnb", 3 | "env": { 4 | "mocha": true, 5 | "node": true 6 | }, 7 | "globals": { 8 | "expect": true 9 | }, 10 | "rules": { 11 | 'indent': [2, 4, { 'SwitchCase': 1, 'VariableDeclarator': 1 }], 12 | "padded-blocks": 0, 13 | "no-else-return": 0, 14 | "no-use-before-define": [2, "nofunc"], 15 | "no-unused-expressions": 0 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules -------------------------------------------------------------------------------- /LICENSE.md: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2016 Zack Argyle 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | Redux Immutable Reducer 2 | ============= 3 | 4 | Immutable state enabled [reducers](http://redux.js.org/docs/basics/Reducers.html) for Redux. [ImmutableJS](https://github.com/facebook/immutable-js) is a peer dependency of this project. 5 | 6 | ``` 7 | npm install --save redux-immutable-reducer 8 | ``` 9 | How to get it? 10 | ```js 11 | // ES6 Modules 12 | import immutableReducer from 'redux-immutable-reducer' 13 | // CommonJS 14 | var immutableReducer = require('redux-immutable-reducer').default 15 | // UMD 16 | var immutableReducer = window.ReduxImmutableReducer.default 17 | ``` 18 | 19 | ## What is it? 20 | 21 | ReduxImmutableReducer solves the issue of using ImmutableJS with Redux. There are a ton of ugly `.toJS()` and `.fromJS()` calls when using the two together and this allows you to avoid all of it. It also helps keep devtools state clean and readable. 22 | 23 | All you have to do to enable it is to wrap your reducer. This will make the state object within your reducer an Immutable object, making it easy to manipulate and keep the data immutable, but then will transform the output state into a regular Javascript datatype for the rest of your application. Here are a few examples. 24 | 25 | #### With Arrays 26 | 27 | ```js 28 | import immutableReducer from 'redux-immutable-reducer'; 29 | 30 | const list = [1, 2, 3, 4]; 31 | function listReducer(state = list, action) { 32 | switch (action.type) { 33 | case 'UPDATE': 34 | return state.push(action.payload); 35 | default: 36 | return state; 37 | } 38 | } 39 | export default immutableReducer(listReducer); 40 | ``` 41 | 42 | #### With Objects 43 | ```js 44 | import immutableReducer from 'redux-immutable-reducer'; 45 | 46 | const deepState = { foo: { bar: { baz: 'qux' }}}; 47 | export function deepReducer(state = deepState, action) { 48 | switch (action.type) { 49 | case 'UPDATE': 50 | return state.mergeDeep(action.payload); 51 | default: 52 | return state; 53 | } 54 | } 55 | export default immutableReducer(deepReducer); 56 | ``` 57 | 58 | ## Installation 59 | 60 | ``` 61 | npm install --save redux-immutable-reducer 62 | ``` 63 | 64 | ## License 65 | 66 | MIT 67 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "redux-immutable-reducer", 3 | "version": "1.0.0", 4 | "description": "Immutable reducer states with Redux.", 5 | "main": "lib/index.js", 6 | "jsnext:main": "es/index.js", 7 | "files": [ 8 | "lib", 9 | "es", 10 | "src", 11 | "dist" 12 | ], 13 | "scripts": { 14 | "clean": "rimraf lib dist es", 15 | "build": "npm run build:commonjs && npm run build:umd && npm run build:umd:min && npm run build:es", 16 | "prepublish": "npm run clean && npm run test && npm run build", 17 | "posttest": "npm run lint", 18 | "lint": "eslint src test", 19 | "test": "cross-env BABEL_ENV=commonjs mocha --compilers js:babel-core/register --reporter spec test/index.js", 20 | "build:commonjs": "cross-env BABEL_ENV=commonjs babel src --out-dir lib", 21 | "build:es": "cross-env BABEL_ENV=es babel src --out-dir es", 22 | "build:umd": "cross-env BABEL_ENV=commonjs NODE_ENV=development webpack", 23 | "build:umd:min": "cross-env BABEL_ENV=commonjs NODE_ENV=production webpack" 24 | }, 25 | "repository": { 26 | "type": "git", 27 | "url": "https://github.com/zackargyle/redux-immutable-reducer.git" 28 | }, 29 | "homepage": "https://github.com/zackargyle/redux-immutable-reducer", 30 | "keywords": [ 31 | "redux", 32 | "immutable", 33 | "flux" 34 | ], 35 | "author": "Zack Argyle ", 36 | "license": "MIT", 37 | "devDependencies": { 38 | "babel-cli": "^6.6.5", 39 | "babel-core": "^6.6.5", 40 | "babel-eslint": "^5.0.0-beta4", 41 | "babel-loader": "^6.2.4", 42 | "babel-plugin-check-es2015-constants": "^6.6.5", 43 | "babel-plugin-transform-es2015-arrow-functions": "^6.5.2", 44 | "babel-plugin-transform-es2015-block-scoped-functions": "^6.6.5", 45 | "babel-plugin-transform-es2015-block-scoping": "^6.6.5", 46 | "babel-plugin-transform-es2015-classes": "^6.6.5", 47 | "babel-plugin-transform-es2015-computed-properties": "^6.6.5", 48 | "babel-plugin-transform-es2015-destructuring": "^6.6.5", 49 | "babel-plugin-transform-es2015-for-of": "^6.6.0", 50 | "babel-plugin-transform-es2015-function-name": "^6.5.0", 51 | "babel-plugin-transform-es2015-literals": "^6.5.0", 52 | "babel-plugin-transform-es2015-modules-commonjs": "^6.6.5", 53 | "babel-plugin-transform-es2015-object-super": "^6.6.5", 54 | "babel-plugin-transform-es2015-parameters": "^6.6.5", 55 | "babel-plugin-transform-es2015-shorthand-properties": "^6.5.0", 56 | "babel-plugin-transform-es2015-spread": "^6.6.5", 57 | "babel-plugin-transform-es2015-sticky-regex": "^6.5.0", 58 | "babel-plugin-transform-es2015-template-literals": "^6.6.5", 59 | "babel-plugin-transform-es2015-unicode-regex": "^6.5.0", 60 | "babel-plugin-transform-es3-member-expression-literals": "^6.5.0", 61 | "babel-plugin-transform-es3-property-literals": "^6.5.0", 62 | "chai": "^3.2.0", 63 | "cross-env": "^1.0.7", 64 | "eslint": "^1.10.2", 65 | "eslint-config-airbnb": "1.0.2", 66 | "eslint-plugin-react": "^4.1.0", 67 | "immutable": "^3.8.1", 68 | "mocha": "^2.2.5", 69 | "redux": "^3.5.2", 70 | "rimraf": "^2.5.2", 71 | "webpack": "^1.12.14" 72 | }, 73 | "peerDependencies": { 74 | "immutable": "^3.8.1" 75 | } 76 | } 77 | -------------------------------------------------------------------------------- /src/index.js: -------------------------------------------------------------------------------- 1 | import Immutable from 'immutable'; 2 | 3 | export function isImmutable(obj) { 4 | return Immutable.Iterable.isIterable(obj); 5 | } 6 | 7 | export default function ImmutableReducer(reducer) { 8 | return (state, action) => { 9 | // Convert incoming data to an Immutable datatype 10 | let immutableState = state; 11 | if (!isImmutable(state)) { 12 | immutableState = Immutable.fromJS(state); 13 | } 14 | const newState = reducer(immutableState, action); 15 | // Convert back from Immutable to regular. Initial states 16 | // are often written as non-Immutable objects. 17 | if (!isImmutable(newState)) { 18 | return newState; 19 | } else { 20 | return newState.toJS(); 21 | } 22 | }; 23 | } 24 | -------------------------------------------------------------------------------- /test/index.js: -------------------------------------------------------------------------------- 1 | import chai from 'chai'; 2 | import { createStore } from 'redux'; 3 | import immutableReducer, { isImmutable } from '../src/index'; 4 | import { shallowReducer, deepReducer, arrayReducer } from './reducers'; 5 | 6 | describe('immutable reducer', () => { 7 | 8 | function createTest(type, payload, reducer, assertions) { 9 | describe('shallow state', () => { 10 | const store = createStore(immutableReducer(reducer)); 11 | const state = store.getState(); 12 | 13 | it(`should handle ${type} state updates with Immutable`, () => { 14 | const action = { type: 'UPDATE', payload: payload }; 15 | store.dispatch(action); 16 | const newState = store.getState(); 17 | chai.assert.notStrictEqual(state, newState); 18 | chai.assert.isNotTrue(isImmutable(newState)); 19 | assertions(state, newState, action); 20 | }); 21 | }); 22 | } 23 | 24 | createTest('array', 5, arrayReducer, (state, newState, action) => { 25 | chai.assert.strictEqual(newState.length, state.length + 1); 26 | chai.assert.strictEqual(newState[4], action.payload); 27 | }); 28 | 29 | createTest('shallow', 'baz', shallowReducer, (state, newState, action) => { 30 | chai.assert.strictEqual(newState.foo, action.payload); 31 | }); 32 | 33 | const payload = { foo: { bar: { baz: 'quux' } } }; 34 | createTest('deep', payload, deepReducer, (state, newState, action) => { 35 | chai.assert.strictEqual(newState.foo.bar.baz, action.payload.foo.bar.baz); 36 | }); 37 | 38 | }); 39 | -------------------------------------------------------------------------------- /test/reducers.js: -------------------------------------------------------------------------------- 1 | const arrayState = [1, 2, 3, 4]; 2 | export function arrayReducer(state = arrayState, action) { 3 | switch (action.type) { 4 | case 'UPDATE': 5 | return state.push(action.payload); 6 | default: 7 | return state; 8 | } 9 | } 10 | 11 | export function shallowReducer(state = { foo: 'bar' }, action) { 12 | switch (action.type) { 13 | case 'UPDATE': 14 | return state.set('foo', action.payload); 15 | default: 16 | return state; 17 | } 18 | } 19 | 20 | const deepState = { foo: { bar: { baz: 'qux' }}}; 21 | export function deepReducer(state = deepState, action) { 22 | switch (action.type) { 23 | case 'UPDATE': 24 | return state.mergeDeep(action.payload); 25 | default: 26 | return state; 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /webpack.config.babel.js: -------------------------------------------------------------------------------- 1 | import webpack from 'webpack'; 2 | import path from 'path'; 3 | 4 | const { NODE_ENV } = process.env; 5 | 6 | const plugins = [ 7 | new webpack.optimize.OccurenceOrderPlugin(), 8 | new webpack.DefinePlugin({ 9 | 'process.env.NODE_ENV': JSON.stringify(NODE_ENV), 10 | }), 11 | ]; 12 | 13 | const filename = `redux-immutable-reducer${NODE_ENV === 'production' ? '.min' : ''}.js`; 14 | 15 | NODE_ENV === 'production' && plugins.push( 16 | new webpack.optimize.UglifyJsPlugin({ 17 | compressor: { 18 | pure_getters: true, 19 | unsafe: true, 20 | unsafe_comps: true, 21 | screw_ie8: true, 22 | warnings: false, 23 | }, 24 | }) 25 | ); 26 | 27 | export default { 28 | module: { 29 | loaders: [ 30 | { test: /\.js$/, loaders: ['babel-loader'], exclude: /node_modules/ }, 31 | ], 32 | }, 33 | 34 | entry: [ 35 | './src/index', 36 | ], 37 | 38 | output: { 39 | path: path.join(__dirname, 'dist'), 40 | filename, 41 | library: 'ReduxImmutableReducer', 42 | libraryTarget: 'umd', 43 | }, 44 | 45 | plugins, 46 | }; 47 | --------------------------------------------------------------------------------