├── .babelrc ├── .eslintrc ├── .gitignore ├── .travis.yml ├── CONTRIBUTING.md ├── README.md ├── config ├── constants.js ├── karma.config.js ├── webpack.config.js ├── webpack.config.publish.js └── webpack.config.test.js ├── package.json ├── script └── build └── src ├── index.js ├── index.spec.js └── specs.context.js /.babelrc: -------------------------------------------------------------------------------- 1 | { 2 | env: { 3 | development: { 4 | plugins: [ 5 | "typecheck" 6 | ] 7 | } 8 | }, 9 | optional: [ 10 | "es7.classProperties", 11 | "runtime" 12 | ], 13 | stage: 1 14 | } 15 | -------------------------------------------------------------------------------- /.eslintrc: -------------------------------------------------------------------------------- 1 | { 2 | "parser": "babel-eslint", 3 | "extends": [ 4 | "standard", 5 | "standard-react" 6 | ], 7 | "env": { 8 | "mocha": true 9 | }, 10 | "globals": { 11 | "sinon": true 12 | }, 13 | "rules": { 14 | // overrides of the standard style 15 | "curly": [2, "all"], 16 | "max-len": [2, 100, 4], 17 | "semi": [2, "always"], 18 | "space-before-function-paren": [2, {"anonymous": "always", "named": "never"}], 19 | "wrap-iife": [2, "outside"], 20 | // overrides of the standard-react style 21 | "react/jsx-sort-props": 2, 22 | "react/jsx-sort-prop-types": 2 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | dist/ 2 | node_modules/ 3 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | sudo: false 2 | language: node_js 3 | cache: 4 | directories: 5 | - node_modules 6 | notifications: 7 | email: false 8 | node_js: 9 | - stable 10 | before_install: 11 | - npm i -g npm@^2.0.0 12 | before_script: 13 | - npm prune 14 | - export DISPLAY=:99.0 15 | - sh -e /etc/init.d/xvfb start 16 | after_success: 17 | - npm run semantic-release 18 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # Contributing to this project 2 | 3 | The issue tracker is the preferred channel for [bug reports](#bugs), 4 | [features requests](#features) and [submitting pull 5 | requests](#pull-requests). 6 | 7 | 8 | ## Bug reports 9 | 10 | A bug is a _demonstrable problem_ that is caused by the code in the repository. 11 | Good bug reports are extremely helpful - thank you! 12 | 13 | Guidelines for bug reports: 14 | 15 | 1. **Use the GitHub issue search** — check if the issue has already been 16 | reported. 17 | 18 | 2. **Check if the issue has been fixed** — try to reproduce it using the 19 | latest `master` or development branch in the repository. 20 | 21 | 3. **Isolate the problem** — create a [reduced test 22 | case](http://css-tricks.com/reduced-test-cases/) and a live example. 23 | 24 | A good bug report contains as much detail as possible. What is your 25 | environment? What steps will reproduce the issue? What browser(s) and OS 26 | experience the problem? What would you expect to be the outcome? All these 27 | details really help! 28 | 29 | Example: 30 | 31 | > Short and descriptive example bug report title 32 | > 33 | > A summary of the issue and the browser/OS environment in which it occurs. If 34 | > suitable, include the steps required to reproduce the bug. 35 | > 36 | > 1. This is the first step 37 | > 2. This is the second step 38 | > 3. Further steps, etc. 39 | > 40 | > `` - a link to the reduced test case 41 | > 42 | > Any other information you want to share that is relevant to the issue being 43 | > reported. This might include the lines of code that you have identified as 44 | > causing the bug, and potential solutions (and your opinions on their 45 | > merits). 46 | 47 | 48 | 49 | ## Feature requests 50 | 51 | Feature requests are welcome. But take a moment to find out whether your idea 52 | fits with the scope and aims of the project. It's up to *you* to make a strong 53 | case to convince the project's developers of the merits of this feature. Please 54 | provide as much detail and context as possible. 55 | 56 | 57 | 58 | ## Pull requests 59 | 60 | Good pull requests - patches, improvements, new features - are a fantastic 61 | help. Please keep them focused in scope and avoid containing unrelated commits. 62 | 63 | **Please ask first** before embarking on any significant pull request (e.g. 64 | implementing new features or components, refactoring code), otherwise you risk 65 | spending a lot of time working on something that the project's developers might 66 | not want to merge into the project. 67 | 68 | Development commands: 69 | 70 | * `npm run build` – build the library 71 | * `npm run dev` – start the dev server and develop against live examples 72 | * `npm run lint` – run the linter 73 | * `npm run specs:watch` – run and watch the unit tests 74 | 75 | Please follow this process for submitting a patch: 76 | 77 | 1. [Fork](http://help.github.com/fork-a-repo/) the project, clone your fork, 78 | and configure the remotes: 79 | 80 | ```bash 81 | # Clone your fork of the repo into the current directory 82 | git clone https://github.com//react-native-web 83 | # Navigate to the newly cloned directory 84 | cd react-native-web 85 | # Assign the original repo to a remote called "upstream" 86 | git remote add upstream https://github.com/phuu/immutable-reducers 87 | ``` 88 | 89 | 2. If you cloned a while ago, get the latest changes from upstream: 90 | 91 | ```bash 92 | git checkout master 93 | git pull upstream master 94 | ``` 95 | 96 | 3. Create a new topic branch (off the main project development branch) to 97 | contain your feature, change, or fix: 98 | 99 | ```bash 100 | git checkout -b 101 | ``` 102 | 103 | 4. Commit your changes in logical chunks. Please adhere to these [git commit 104 | message guidelines](http://tbaggery.com/2008/04/19/a-note-about-git-commit-messages.html) 105 | or your code is unlikely be merged into the main project. Use Git's 106 | [interactive rebase](https://help.github.com/articles/interactive-rebase) 107 | feature to tidy up your commits before making them public. 108 | 109 | 5. Locally merge (or rebase) the upstream development branch into your topic branch: 110 | 111 | ```bash 112 | git pull [--rebase] upstream master 113 | ``` 114 | 115 | 6. Push your topic branch up to your fork: 116 | 117 | ```bash 118 | git push origin 119 | ``` 120 | 121 | 7. [Open a Pull Request](https://help.github.com/articles/using-pull-requests/) 122 | with a clear title and description. 123 | 124 | **IMPORTANT**: By submitting a patch, you agree to allow the project owner to 125 | license your work under the same license as that used by the project. 126 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # immutable-reducers 2 | 3 | [![semantic-release](https://img.shields.io/badge/%20%20%F0%9F%93%A6%F0%9F%9A%80-semantic--release-e10079.svg)](https://github.com/semantic-release/semantic-release) [![travis-ci](https://travis-ci.org/phuu/immutable-reducers.svg?branch=master)](https://travis-ci.org/phuu/immutable-reducers) 4 | 5 | Create reducers for [immutable][immutable] data structures. Useful for [redux][redux]. 6 | 7 | ## Table of contents 8 | 9 | * [Install](#install) 10 | * [Use](#use) 11 | * [`createReducer`](#createreducer) 12 | * [`combineReducers`](#combinereducers) 13 | * [Use](#use) 14 | * [Contributing](#contributing) 15 | * [Thanks](#thanks) 16 | * [License](#license) 17 | 18 | ## Install 19 | 20 | ``` 21 | npm install --save immutable-reducers 22 | ``` 23 | 24 | ## Use 25 | 26 | ```js 27 | import { fromJS } from 'immutable'; 28 | import { combineReducers, createReducer } from 'immutable-reducers'; 29 | 30 | // Setup some state (probably your app state) 31 | 32 | const initialState = fromJS({ 33 | artist: { 34 | name: { 35 | first: 'Sean', 36 | last: 'Combs' 37 | }, 38 | fans: 0 39 | } 40 | }); 41 | 42 | // Create some reducers 43 | 44 | const artistNameReducer = createReducer(['artist', 'name'], (state, action) => { 45 | switch (action.type) { 46 | case 'RENAME': 47 | return fromJS(action.value); 48 | } 49 | return state; 50 | }); 51 | 52 | // You can scope them by combining with an object 53 | 54 | const artistFansReducer = createReducer(['artis', 'fans'], (state, action) => { 55 | switch (action.type) { 56 | case 'NEW_FAN': 57 | return state + action.count; 58 | } 59 | return state; 60 | }); 61 | 62 | // Combine 'em up 63 | 64 | const reducer = combineReducers(artistNameReducer, artistFansReducer); 65 | 66 | // Step the state 67 | 68 | reducer(initialState, { type: 'NEW_FAN', count: 1 }); 69 | 70 | ``` 71 | 72 | ## `createReducer` 73 | 74 | `createReducer` helps you make a reducer that operates on a small area of an immutable data structure. 75 | 76 | ```js 77 | createReducer( 78 | keyPath: Array, 79 | updater: (targetState: any, action: Object, state: any) => any 80 | ): any 81 | ``` 82 | 83 | For example, given some initial state: 84 | 85 | ```js 86 | const initialState = fromJS({ 87 | user: { 88 | favorites: Immutable.OrderedSet() 89 | } 90 | }); 91 | ``` 92 | 93 | We can create a reducer that looks out for favorite actions and remembers the id: 94 | 95 | ```js 96 | const favoriteReducer = createReducer(['user', 'favorites'], (favorites, action) => { 97 | switch (action.type) { 98 | case 'FAVORITE': 99 | return favorites.add(action.id); 100 | } 101 | return favorites; 102 | }); 103 | ``` 104 | 105 | `createReducer` handles reaching into the data structure and updating the value. 106 | 107 | Good to know: 108 | 109 | - If the updater function returns the same value it was called with, then no change will occur. 110 | - If the `keyPath` you specify does not exist, an Immutable `Map` will be created at each intermediary key. 111 | - The keys can be immutable data structures too #winning 112 | - The third argument to the reducer is the whole state object, allowing you consult other areas of state when handling an action 113 | 114 | ## `combineReducers` 115 | 116 | `combineReducers` is a less opinionated version of redux's default [`combineReducers`][redux-combinereducers] utility. 117 | 118 | ```js 119 | type Reducer = (state: any, action: Object) => any; 120 | type ReducerObject = Object; 121 | 122 | combineReducers(...Reducer|ReducerObject): Reducer 123 | ``` 124 | 125 | It has two useful forms: applied to a list of reducers or to an object. When applied to the list, it acts like redux's combineReducers. 126 | 127 | When applied to an object, it's slightly different. `immutable-reducers` will use the key of the object to 'scope' the reducer further. 128 | 129 | For example, in the following example: 130 | You could scope it to the `user` key of your (immutable) state using `combineReducers`: 131 | 132 | ```js 133 | const combineReducer = combineReducers({ 134 | user: createReducer(['favorites'], (favorites, action) => { 135 | switch (action.type) { 136 | case 'FAVORITE': 137 | return favorites.add(action.id); 138 | } 139 | return favorites; 140 | }); 141 | }); 142 | ``` 143 | 144 | The end result is the same as the `createReducers` example, where the `favoritesReducer` will actually operate on the `['user', 'favorites']` key path. 145 | 146 | ## Contributing 147 | 148 | Please read the [contribution guidelines][contributing-url]. Contributions are 149 | welcome! 150 | 151 | ## Thanks 152 | 153 | Thanks to [rackt][rackt] for [redux][redux] and all those who work on [immutable][immutable]. 154 | 155 | ## License 156 | 157 | Copyright (c) 2015 Tom Ashworth. Released under the [MIT 158 | license](http://www.opensource.org/licenses/mit-license.php). 159 | 160 | [contributing-url]: https://github.com/phuu/immutable-reducers/blob/master/CONTRIBUTING.md 161 | [immutable]: https://facebook.github.io/immutable-js/ 162 | [rackt]: https://github.com/rackt 163 | [redux]: http://rackt.github.io/redux/ 164 | [redux-combinereducers]: http://rackt.github.io/redux/docs/api/combineReducers.html 165 | -------------------------------------------------------------------------------- /config/constants.js: -------------------------------------------------------------------------------- 1 | var path = require('path'); 2 | 3 | var ROOT_DIRECTORY = path.resolve(__dirname, '..'); 4 | var BUILD_DIRECTORY = path.resolve(ROOT_DIRECTORY, 'dist'); 5 | 6 | module.exports = { 7 | ROOT_DIRECTORY: ROOT_DIRECTORY, 8 | BUILD_DIRECTORY: BUILD_DIRECTORY 9 | }; 10 | -------------------------------------------------------------------------------- /config/karma.config.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | var constants = require('./constants'); 4 | var webpackConfig = require('./webpack.config.test'); 5 | // entry is determined by karma config 'files' array 6 | webpackConfig.entry = {}; 7 | 8 | module.exports = function (config) { 9 | config.set({ 10 | basePath: constants.ROOT_DIRECTORY, 11 | browsers: [ process.env.TRAVIS ? 'Firefox' : 'Chrome' ], 12 | browserNoActivityTimeout: 60000, 13 | client: { 14 | captureConsole: true, 15 | mocha: { 16 | ui: 'bdd' 17 | }, 18 | useIframe: true 19 | }, 20 | files: [ 21 | 'src/specs.context.js' 22 | ], 23 | frameworks: [ 24 | 'mocha' 25 | ], 26 | plugins: [ 27 | 'karma-chrome-launcher', 28 | 'karma-firefox-launcher', 29 | 'karma-mocha', 30 | 'karma-sourcemap-loader', 31 | 'karma-webpack' 32 | ], 33 | preprocessors: { 34 | 'src/specs.context.js': [ 'webpack', 'sourcemap' ] 35 | }, 36 | reporters: [ 'dots' ], 37 | singleRun: true, 38 | webpack: webpackConfig, 39 | webpackMiddleware: { 40 | stats: { 41 | assetsSort: 'name', 42 | colors: true, 43 | children: false, 44 | chunks: false, 45 | modules: false 46 | } 47 | } 48 | }); 49 | }; 50 | -------------------------------------------------------------------------------- /config/webpack.config.js: -------------------------------------------------------------------------------- 1 | var webpack = require('webpack'); 2 | 3 | var DedupePlugin = webpack.optimize.DedupePlugin; 4 | var OccurenceOrderPlugin = webpack.optimize.OccurenceOrderPlugin; 5 | var UglifyJsPlugin = webpack.optimize.UglifyJsPlugin; 6 | 7 | var plugins = [ 8 | new DedupePlugin(), 9 | new OccurenceOrderPlugin() 10 | ]; 11 | 12 | if (process.env.NODE_ENV === 'production') { 13 | plugins.push( 14 | new UglifyJsPlugin({ 15 | compress: { 16 | dead_code: true, 17 | drop_console: true, 18 | screw_ie8: true, 19 | warnings: true 20 | } 21 | }) 22 | ); 23 | } 24 | 25 | module.exports = { 26 | entry: './src', 27 | module: { 28 | loaders: [ 29 | { 30 | test: /\.jsx?$/, 31 | exclude: /node_modules/, 32 | loader: 'babel-loader' 33 | } 34 | ] 35 | }, 36 | output: { 37 | path: './dist', 38 | filename: 'immutable-reducers.js' 39 | }, 40 | resolve: { 41 | extensions: [ 42 | '', 43 | '.js', 44 | '.jsx' 45 | ] 46 | }, 47 | plugins: plugins 48 | }; 49 | -------------------------------------------------------------------------------- /config/webpack.config.publish.js: -------------------------------------------------------------------------------- 1 | var assign = require('object-assign'); 2 | var constants = require('./constants'); 3 | var baseConfig = require('./webpack.config'); 4 | 5 | module.exports = assign(baseConfig, { 6 | output: { 7 | filename: 'immutable-reducers.js', 8 | library: 'ImmutableReducers', 9 | libraryTarget: 'commonjs2', 10 | path: constants.BUILD_DIRECTORY 11 | } 12 | }); 13 | -------------------------------------------------------------------------------- /config/webpack.config.test.js: -------------------------------------------------------------------------------- 1 | var assign = require('object-assign'); 2 | var baseConfig = require('./webpack.config'); 3 | 4 | module.exports = assign(baseConfig, { 5 | // fixes sourcemaps in karma 6 | devtool: 'inline-source-map', 7 | // builds are faster when css is excluded 8 | features: { 9 | removeStylesheet: true 10 | } 11 | }); 12 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "immutable-reducers", 3 | "description": "Create reducers for immutable data structures. Useful for redux.", 4 | "scripts": { 5 | "build": "rm -rf ./dist && NODE_ENV=publish webpack --config config/webpack.config.publish.js --sort-assets-by --progress", 6 | "lint": "eslint config src", 7 | "prepublish": "npm run build", 8 | "specs": "NODE_ENV=test karma start config/karma.config.js", 9 | "specs:watch": "npm run specs -- --no-single-run", 10 | "test": "npm run specs && npm run lint", 11 | "semantic-release": "semantic-release pre && npm publish && semantic-release post" 12 | }, 13 | "main": "dist/immutable-reducers.js", 14 | "files": [ 15 | "dist" 16 | ], 17 | "author": "Tom Ashworth ", 18 | "license": "MIT", 19 | "devDependencies": { 20 | "babel-core": "^5.8.24", 21 | "babel-eslint": "^4.1.1", 22 | "babel-loader": "^5.3.2", 23 | "babel-plugin-typecheck": "^3.6.1", 24 | "babel-runtime": "^5.8.20", 25 | "chai": "^3.2.0", 26 | "eslint": "^1.3.1", 27 | "eslint-config-standard": "^4.3.1", 28 | "eslint-config-standard-react": "^2.0.0", 29 | "eslint-plugin-react": "^3.3.1", 30 | "eslint-plugin-standard": "^1.3.0", 31 | "immutable": "^3.7.5", 32 | "karma": "^0.13.9", 33 | "karma-chrome-launcher": "^0.2.0", 34 | "karma-cli": "^0.1.0", 35 | "karma-firefox-launcher": "^0.1.6", 36 | "karma-mocha": "^0.2.0", 37 | "karma-sourcemap-loader": "^0.3.5", 38 | "karma-webpack": "^1.7.0", 39 | "mocha": "^2.3.2", 40 | "object-assign": "^4.0.1", 41 | "webpack": "^1.12.1", 42 | "semantic-release": "^4.3.4" 43 | }, 44 | "dependencies": {}, 45 | "repository": { 46 | "type": "git", 47 | "url": "https://github.com/phuu/immutable-reducers.git" 48 | } 49 | } 50 | -------------------------------------------------------------------------------- /script/build: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | # 3 | # Generate build artifacts 4 | 5 | webpack --config config/webpack.config.js 6 | -------------------------------------------------------------------------------- /src/index.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Combine multiple reducers into a single, with optional scoping 3 | * 4 | * Example: 5 | 6 | combineReducers(userReducer, followersReducer) 7 | 8 | // Scoped to key 10 9 | item10Reducer = combineReducers({ 10 | 10: itemReducer 11 | }) 12 | 13 | // Compose 'em up 14 | basketItem10Reducer = combineReducers({ 15 | basket: item10Reducer 16 | }) 17 | 18 | * Returns a reducing function. 19 | */ 20 | const combineReducers = (...rawReducers) => { 21 | const reducers = rawReducers.reduce((rs, reducer) => { 22 | // Keep the plain 'ol functions 23 | if (typeof reducer === 'function') { 24 | return rs.concat(reducer); 25 | } 26 | 27 | // Scope the reducers by their keys, if keyed they are 28 | if (typeof reducer === 'object' && !Array.isArray(reducer)) { 29 | return Object.keys(reducer).reduce((rs, k) => { 30 | // Value is a function, so we can make it into a reducer! 31 | if (typeof reducer[k] === 'function') { 32 | return rs.concat( 33 | createReducer([k], reducer[k]) 34 | ); 35 | } 36 | 37 | // Ignore it 38 | return rs; 39 | }, rs); 40 | } 41 | 42 | // Otherwise, ignore this one 43 | return rs; 44 | }, []); 45 | 46 | // Return a function that iterates over the reducers and reduces from some initial state, 47 | // with an action as input. Easy now. 48 | return (initialState, action) => 49 | reducers.reduce( 50 | (state, reducer) => reducer(state, action), 51 | initialState 52 | ); 53 | }; 54 | 55 | /** 56 | * Create a reducer scoped to the specified keys. 57 | * 58 | * Example: 59 | 60 | createReducer(['user', 'followers'], (state, action) => { 61 | if (action.type === 'FOLLOW') { 62 | return state + 1; 63 | } 64 | return state; 65 | }) 66 | 67 | * Returns a reducing function. 68 | */ 69 | const createReducer = (path, reducer) => (initialState, action, globalState = initialState) => 70 | initialState.updateIn(path, v => reducer(v, action, globalState)); 71 | 72 | export default { 73 | combineReducers, 74 | createReducer 75 | }; 76 | -------------------------------------------------------------------------------- /src/index.spec.js: -------------------------------------------------------------------------------- 1 | import { assert } from 'chai'; 2 | import { fromJS } from 'immutable'; 3 | 4 | import { combineReducers, createReducer } from '.'; 5 | 6 | const initialState = fromJS({ 7 | artist: { 8 | name: { 9 | first: 'Sean', 10 | last: 'Combs' 11 | }, 12 | followers: 0 13 | }, 14 | albums: [ 15 | { name: 'No Way Out', year: '1997' }, 16 | { name: 'Forever', year: '1999' } 17 | ], 18 | related: { 19 | artist: { 20 | name: { 21 | first: 'Mary J.', 22 | last: 'Blige' 23 | }, 24 | followers: 0 25 | } 26 | } 27 | }); 28 | 29 | const nameReducer = createReducer(['artist', 'name'], (name, action) => { 30 | switch (action.type) { 31 | case 'RENAME': 32 | return fromJS(action.value); 33 | } 34 | return name; 35 | }); 36 | 37 | const followersReducer = createReducer(['artist', 'followers'], (followers, action) => { 38 | switch (action.type) { 39 | case 'FOLLOW': 40 | return followers + action.value; 41 | } 42 | return followers; 43 | }); 44 | 45 | const albumsReducer = createReducer(['albums'], (albums, action) => { 46 | switch (action.type) { 47 | case 'RELEASE': 48 | return albums.push(fromJS(action.value)); 49 | } 50 | return albums; 51 | }); 52 | 53 | const fakePathReducer = createReducer(['this', 'path', 'is', 'fake'], (_, action) => { 54 | return fromJS({ no: 'thanks' }); 55 | }); 56 | 57 | const actions = { 58 | FOLLOW: { type: 'FOLLOW', value: 1 }, 59 | RENAME: { type: 'RENAME', value: { first: 'Puff', last: 'Daddy' } }, 60 | NOOP: { type: 'NOOP', value: {} }, 61 | RELEASE: { type: 'RELEASE', value: { name: 'The Saga Continues...', year: '2001' } } 62 | }; 63 | 64 | describe('createReducer', () => { 65 | it('creates reducers than can reach into data structures', () => { 66 | assert( 67 | followersReducer(initialState, actions.FOLLOW).getIn(['artist', 'followers']) === 1, 68 | 'followers state is incremented by 1' 69 | ); 70 | }); 71 | 72 | it('does not modify the data if nothing changes', () => { 73 | assert( 74 | nameReducer(initialState, actions.NOOP) === initialState, 75 | 'data is untouched' 76 | ); 77 | }); 78 | 79 | it('can handle non-existant paths', () => { 80 | assert( 81 | fakePathReducer(initialState, actions.NOOP).hasIn(['this', 'path', 'is', 'fake']), 82 | 'non-existant path is created' 83 | ); 84 | }); 85 | 86 | it('passes whole state object', () => { 87 | const reducer = createReducer(['artist'], (artist, action, state) => { 88 | assert( 89 | state === initialState, 90 | 'is passed initialState' 91 | ); 92 | assert( 93 | artist === state.get('artist'), 94 | 'gets target state object' 95 | ); 96 | }); 97 | 98 | reducer(initialState, {}); 99 | }); 100 | 101 | it('the state object is passed through composition', () => { 102 | const nameReducer = createReducer(['name'], (name, action, state) => { 103 | assert( 104 | state === initialState, 105 | 'is passed initialState' 106 | ); 107 | assert( 108 | name === initialState.getIn(['artist', 'name']), 109 | 'gets target state object' 110 | ); 111 | return name; 112 | }); 113 | 114 | const reducer = combineReducers({ 115 | artist: nameReducer 116 | }); 117 | 118 | reducer(initialState, {}); 119 | }); 120 | }); 121 | 122 | describe('combineReducers', () => { 123 | it('can take an object to scope reducer', () => { 124 | const combinedReducer = combineReducers({ 125 | related: followersReducer 126 | }); 127 | const result = combinedReducer(initialState, actions.FOLLOW); 128 | 129 | assert( 130 | result.getIn(['related', 'artist', 'followers']) === 1, 131 | 'scoped data is updated' 132 | ); 133 | }); 134 | 135 | it('can combine reducers in a sequence', () => { 136 | const combinedReducer = combineReducers(nameReducer, albumsReducer); 137 | const result = combinedReducer(initialState, actions.RELEASE); 138 | 139 | assert( 140 | result.get('albums').count() === 3, 141 | 'albums is updated' 142 | ); 143 | assert( 144 | result.get('artist') === initialState.get('artist'), 145 | 'artist is untouched' 146 | ); 147 | }); 148 | 149 | it('can combine combined reducers', () => { 150 | const artistReducer = combineReducers(nameReducer, followersReducer); 151 | const combinedReducer = combineReducers(artistReducer, albumsReducer); 152 | const result = combinedReducer(initialState, actions.RENAME); 153 | 154 | assert( 155 | result.get('albums').count() === 2, 156 | 'albums is updated' 157 | ); 158 | assert( 159 | result.get('artist') !== initialState.get('artist'), 160 | 'artist is touched' 161 | ); 162 | }); 163 | }); 164 | -------------------------------------------------------------------------------- /src/specs.context.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Since we use webpack-specific features in our modules (e.g., loaders, 3 | * plugins, adding CSS to the dependency graph), we must use webpack to build a 4 | * test bundle. 5 | * 6 | * This module creates a context of all the unit test files (as per the unit 7 | * test naming convention). It's used as the webpack entry file for unit tests. 8 | * 9 | * See: https://github.com/webpack/docs/wiki/context 10 | */ 11 | 12 | const specsContext = require.context('.', true, /.+\.spec\.js$/); 13 | specsContext.keys().forEach(specsContext); 14 | module.exports = specsContext; 15 | --------------------------------------------------------------------------------