├── .babelrc ├── .eslintrc ├── .gitignore ├── .npmignore ├── LICENSE ├── README.md ├── examples ├── .gitignore └── universal-counter │ ├── README.md │ ├── package.json │ ├── src │ ├── client │ │ └── index.js │ ├── server │ │ ├── index.js │ │ └── utils │ │ │ ├── getIntentStream.js │ │ │ └── render.js │ └── shared │ │ ├── constants │ │ └── IntentTypes.js │ │ ├── index.js │ │ ├── intents │ │ ├── CounterIntentList.js │ │ └── CounterIntents.js │ │ ├── reducers │ │ └── counter.js │ │ ├── rx-middleware │ │ └── commonMiddleware.js │ │ ├── utils │ │ ├── appendIntentId.js │ │ └── isBrowser.js │ │ └── view │ │ └── components │ │ └── Counter.js │ └── webpack │ ├── dev-server.js │ ├── webpack.config.client.js │ ├── webpack.config.js │ └── webpack.config.server.js ├── package.json ├── src ├── createStore.js ├── index.js └── utils │ ├── combineReducers.js │ └── isPlainObject.js └── webpack.config.js /.babelrc: -------------------------------------------------------------------------------- 1 | { 2 | "stage": 0, 3 | "loose": "all" 4 | } -------------------------------------------------------------------------------- /.eslintrc: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "eslint-config-airbnb", 3 | "env": { 4 | "browser": true, 5 | "mocha": true, 6 | "node": true 7 | }, 8 | "rules": { 9 | //Temporarirly disabled due to a possible bug in babel-eslint (todomvc example) 10 | "block-scoped-var": 0, 11 | // Temporarily disabled for test/* until babel/babel-eslint#33 is resolved 12 | "padded-blocks": 0 13 | } 14 | } -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | node_modules 3 | npm-debug.log 4 | dist 5 | lib 6 | .idea -------------------------------------------------------------------------------- /.npmignore: -------------------------------------------------------------------------------- 1 | src 2 | examples 3 | .idea 4 | .babelrc 5 | .eslintrc 6 | .npmignore 7 | webpack.config.js -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2015 Jas Chen 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 13 | all 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 21 | THE SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | [![npm version](https://img.shields.io/npm/v/redux-core.svg?style=flat-square)](https://www.npmjs.com/package/redux-core) 2 | 3 | redux-core 4 | ========== 5 | 6 | Minimal [redux](https://github.com/gaearon/redux). 7 | 8 | ## Why? 9 | - Only ~60 lines of code to read, easy to understand and customize. 10 | - You can implement your own state subscription and store enhancement system. 11 | 12 | ## Provided APIs 13 | - createStore 14 | - getState 15 | - dispatch (***will return state***) 16 | - getReducer 17 | - replaceReducer 18 | - combineReducers 19 | 20 | ## License 21 | [The MIT License (MIT)](./LICENSE) 22 | -------------------------------------------------------------------------------- /examples/.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | node_modules 3 | npm-debug.log 4 | build -------------------------------------------------------------------------------- /examples/universal-counter/README.md: -------------------------------------------------------------------------------- 1 | Universal Counter Demo 2 | ====================== 3 | 4 | ## redux-core + React + RxJS 5 | 6 | In redux, to make an universal app you have to gather all your Promises returned from dispatch() in server side, this could be painful if you have many Promises. 7 | 8 | With RxJS and [RxMiddleware](https://github.com/jas-chen/rx-redux/#best-practice-to-make-your-app-all-the-way-reactive), subscribe for stream complete event and you are done. 9 | 10 | ## To run this example 11 | 12 | ``` 13 | cd examples/universal-counter 14 | npm install 15 | 16 | # Production mode 17 | npm run build 18 | npm start 19 | open http://localhost:3000 20 | 21 | 22 | # Development mode, start 3 consoles to run each command 23 | npm run build-server-watch 24 | npm run build-client-watch 25 | npm run dev 26 | open http://localhost:3000 27 | ``` 28 | 29 | -------------------------------------------------------------------------------- /examples/universal-counter/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "universal-counter-rx", 3 | "version": "0.1.0", 4 | "description": "Universal Counter with redux-core", 5 | "main": "index.js", 6 | "scripts": { 7 | "clean": "rimraf build", 8 | 9 | "build": "NODE_ENV=production webpack --config ./webpack/webpack.config.js --devtool source-map --progress --display-modules", 10 | "start": "NODE_ENV=production node ./build/server/server.js", 11 | 12 | "build-server-watch": "webpack -d --config ./webpack/webpack.config.server.js --watch", 13 | "build-client-watch": "node ./webpack/dev-server.js", 14 | "dev": "nodemon ./build/server/server.js --watch build/server", 15 | 16 | "test": "echo \"Error: no test specified\" && exit 1" 17 | }, 18 | "author": "Jas Chen", 19 | "license": "MIT", 20 | "devDependencies": { 21 | "babel": "^5.8.19", 22 | "babel-core": "^5.8.19", 23 | "babel-loader": "^5.3.2", 24 | "express": "^4.13.1", 25 | "json-loader": "^0.5.2", 26 | "nodemon": "^1.4.0", 27 | "react": "^0.13.3", 28 | "redux-core": "^0.1.1", 29 | "rimraf": "^2.4.2", 30 | "rx-lite": "^2.5.2", 31 | "rx-lite-joinpatterns": "^2.5.2", 32 | "webpack": "^1.10.5", 33 | "webpack-dev-server": "^1.10.1" 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /examples/universal-counter/src/client/index.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import Rx from 'rx'; 3 | import { createStore } from 'redux-core'; 4 | import { reducer, applyRxMiddleware } from '../shared'; 5 | import Counter from '../shared/view/components/Counter'; 6 | 7 | const root = document.getElementById('root'); 8 | 9 | const store = createStore(reducer, window.__state); 10 | 11 | const intent$ = new Rx.Subject(); 12 | const action$ = applyRxMiddleware(intent$, store); 13 | const state$ = action$.map(store.dispatch).startWith(store.getState()); 14 | 15 | state$.subscribe( 16 | (state) => { React.render(, root); }, 17 | (err) => { throw new Error(err); }, 18 | () => console.log('state$ completed.') 19 | ); 20 | -------------------------------------------------------------------------------- /examples/universal-counter/src/server/index.js: -------------------------------------------------------------------------------- 1 | import express from 'express'; 2 | import path from 'path'; 3 | import { createStore } from 'redux-core'; 4 | import { reducer, applyRxMiddleware } from '../shared'; 5 | import getIntentStream from './utils/getIntentStream'; 6 | import render from './utils/render'; 7 | import Counter from '../shared/view/components/Counter'; 8 | import React from 'react'; 9 | 10 | const isProdMode = process.env.NODE_ENV === 'production'; 11 | const isDevMode = !isProdMode; 12 | 13 | if (isProdMode) { 14 | console.info('Mode: Production'); 15 | } 16 | else { 17 | console.info('Mode: Development'); 18 | } 19 | 20 | var app = express(); 21 | 22 | // main 23 | app.get('/', function (req, res) { 24 | const intentIds = req.query.intentId; 25 | 26 | console.log('\n\nReceive request with intent ids: ' + intentIds); 27 | 28 | const store = createStore(reducer); 29 | const intent$ = getIntentStream(intentIds); 30 | const action$ = applyRxMiddleware(intent$, store); 31 | const state$ = action$.map(store.dispatch).startWith(store.getState()); 32 | 33 | let finalState = store.getState(); 34 | 35 | state$.subscribe( 36 | (state) => { 37 | if (isDevMode) { 38 | console.log(state); 39 | } 40 | 41 | finalState = state; 42 | }, 43 | (err) => { throw new Error(err); }, 44 | () => { 45 | if (isDevMode) { 46 | console.log('state$ completed.'); 47 | } 48 | 49 | const fakeIntent$ = {}; 50 | const view = React.renderToString(); 51 | res.send(render(view, finalState)); 52 | } 53 | ); 54 | 55 | 56 | }); 57 | 58 | // static path for browser to get bundle.js 59 | app.use('/assets', express.static(path.join('.', 'build', 'assets'))); 60 | 61 | app.get('*', function (req, res) { 62 | res.status(404).end('404 - Page Not Found'); 63 | }); 64 | 65 | app.listen(3000, function () { 66 | console.log('Listening on port 3000, root: ' + path.resolve('.')); 67 | }); -------------------------------------------------------------------------------- /examples/universal-counter/src/server/utils/getIntentStream.js: -------------------------------------------------------------------------------- 1 | import Rx from 'rx'; 2 | import list from '../../shared/intents/CounterIntentList'; 3 | 4 | // Transform intent ids to intent function 5 | export default function getIntentStream(intentIds) { 6 | const intents = []; 7 | 8 | if (intentIds) { 9 | intentIds.split('').forEach(id => { 10 | const intentCreator = list[id]; 11 | 12 | // in case user modify query string to invalid id like '11asdasdas' 13 | if (intentCreator) { 14 | const intent = intentCreator(); 15 | intents.push(intent); 16 | } 17 | }); 18 | } 19 | 20 | return Rx.Observable.from(intents); 21 | } -------------------------------------------------------------------------------- /examples/universal-counter/src/server/utils/render.js: -------------------------------------------------------------------------------- 1 | const script = process.env.NODE_ENV === 'production' ? 2 | '': 3 | '\n'+ 4 | '' 5 | 6 | export default function (view, state) { 7 | return ` 8 | 9 | 10 | 11 | redux-core universal counter 12 | 17 | 18 | 19 |

Universal counter demo with redux-core

20 |
${view}
21 | 22 | ${script} 23 | 24 | `; 25 | } -------------------------------------------------------------------------------- /examples/universal-counter/src/shared/constants/IntentTypes.js: -------------------------------------------------------------------------------- 1 | export const INCREMENT_COUNTER = 'INCREMENT_COUNTER'; 2 | export const DECREMENT_COUNTER = 'DECREMENT_COUNTER'; -------------------------------------------------------------------------------- /examples/universal-counter/src/shared/index.js: -------------------------------------------------------------------------------- 1 | import commonMiddleware from './rx-middleware/commonMiddleware'; 2 | import {combineReducers} from 'redux-core'; 3 | import * as reducers from './reducers/counter'; 4 | 5 | const middleware = [commonMiddleware]; 6 | 7 | export function applyRxMiddleware(intent$, store) { 8 | return middleware.reduce((prev, cur) => { 9 | return prev.flatMap(cur(store.getState)); 10 | }, intent$); 11 | } 12 | 13 | export const reducer = combineReducers(reducers); 14 | -------------------------------------------------------------------------------- /examples/universal-counter/src/shared/intents/CounterIntentList.js: -------------------------------------------------------------------------------- 1 | import * as CounterIntents from './CounterIntents'; 2 | 3 | const list = [ 4 | CounterIntents.increment, 5 | CounterIntents.decrement, 6 | CounterIntents.incrementIfOdd, 7 | CounterIntents.incrementTimeout, 8 | CounterIntents.incrementPromise 9 | ]; 10 | 11 | export default list; 12 | -------------------------------------------------------------------------------- /examples/universal-counter/src/shared/intents/CounterIntents.js: -------------------------------------------------------------------------------- 1 | import { INCREMENT_COUNTER, DECREMENT_COUNTER } from '../constants/IntentTypes'; 2 | import list from './CounterIntentList.js'; 3 | import appendIntentId from '../utils/appendIntentId'; 4 | import Rx from 'rx'; 5 | 6 | const incrementAction = { type: INCREMENT_COUNTER }; 7 | 8 | export function increment() { 9 | appendIntentId(list.indexOf(increment)); 10 | 11 | return incrementAction; 12 | } 13 | 14 | export function decrement() { 15 | appendIntentId(list.indexOf(decrement)); 16 | 17 | return { 18 | type: DECREMENT_COUNTER 19 | }; 20 | } 21 | 22 | export function incrementIfOdd() { 23 | appendIntentId(list.indexOf(incrementIfOdd)); 24 | 25 | return (getState) => { 26 | const { counter } = getState(); 27 | 28 | if (counter % 2 === 0) { 29 | return Rx.Observable.empty(); 30 | } 31 | 32 | return incrementAction; 33 | }; 34 | } 35 | 36 | export function incrementTimeout() { 37 | appendIntentId(list.indexOf(incrementTimeout)); 38 | 39 | return Rx.Observable.just(incrementAction).delay(1000); 40 | } 41 | 42 | export function incrementPromise() { 43 | appendIntentId(list.indexOf(incrementPromise)); 44 | 45 | function getRandomTime() { return Math.floor(Math.random()*10%5)*100+800; } 46 | 47 | const promise = new Promise((resolve) => { 48 | setTimeout(() => { 49 | resolve(incrementAction); 50 | }, getRandomTime()); 51 | }); 52 | 53 | return Rx.Observable.fromPromise(promise); 54 | } -------------------------------------------------------------------------------- /examples/universal-counter/src/shared/reducers/counter.js: -------------------------------------------------------------------------------- 1 | import { INCREMENT_COUNTER, DECREMENT_COUNTER } from '../constants/IntentTypes'; 2 | 3 | export function counter(state = 0, action = {}) { 4 | switch (action.type) { 5 | case INCREMENT_COUNTER: 6 | return state + 1; 7 | case DECREMENT_COUNTER: 8 | return state - 1; 9 | default: 10 | return state; 11 | } 12 | } -------------------------------------------------------------------------------- /examples/universal-counter/src/shared/rx-middleware/commonMiddleware.js: -------------------------------------------------------------------------------- 1 | import Rx from 'rx'; 2 | 3 | export default function commonMiddleware(getState) { 4 | return intent => { 5 | let action = intent; 6 | 7 | if (typeof action === 'function') { 8 | action = action(getState); 9 | } 10 | 11 | if (action instanceof Rx.Observable) { 12 | return action; 13 | } 14 | 15 | return Rx.Observable.just(action); 16 | }; 17 | } 18 | -------------------------------------------------------------------------------- /examples/universal-counter/src/shared/utils/appendIntentId.js: -------------------------------------------------------------------------------- 1 | import isBrowser from './isBrowser'; 2 | 3 | function appendIntentId(intentId) { 4 | if(isBrowser) { 5 | const base = window.location.search.length ? window.location.search : '?intentId='; 6 | window.history.replaceState(null, null, base + intentId); 7 | } 8 | } 9 | 10 | export default appendIntentId; -------------------------------------------------------------------------------- /examples/universal-counter/src/shared/utils/isBrowser.js: -------------------------------------------------------------------------------- 1 | export default typeof window !== 'undefined'; -------------------------------------------------------------------------------- /examples/universal-counter/src/shared/view/components/Counter.js: -------------------------------------------------------------------------------- 1 | import React, { Component, PropTypes } from 'react'; 2 | import * as CounterIntents from '../../intents/CounterIntents'; 3 | 4 | class Counter extends Component { 5 | shouldComponentUpdate(nextProps) { 6 | return nextProps.state.counter !== this.props.state.counter; 7 | } 8 | 9 | render() { 10 | const { state, intent$ } = this.props; 11 | return ( 12 |

13 | 14 | {state.counter} 15 | 16 | 17 | 18 | 19 |

20 | ); 21 | } 22 | } 23 | 24 | Counter.propTypes = { 25 | state: PropTypes.object.isRequired, 26 | intent$: PropTypes.object.isRequired 27 | }; 28 | 29 | export default Counter; -------------------------------------------------------------------------------- /examples/universal-counter/webpack/dev-server.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | var WebpackDevServer = require("webpack-dev-server"); 4 | var webpack = require("webpack"); 5 | var config = require('./webpack.config.client'); 6 | 7 | var compiler = webpack(config); 8 | 9 | var server = new WebpackDevServer(compiler, { 10 | watchOptions: { 11 | aggregateTimeout: 1500, 12 | poll: 1000 13 | }, 14 | stats: { colors: true } 15 | }); 16 | 17 | server.listen(8080, "localhost", function() {}); -------------------------------------------------------------------------------- /examples/universal-counter/webpack/webpack.config.client.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | var webpack = require('webpack'); 4 | var path = require('path'); 5 | 6 | var plugins = [ 7 | new webpack.DefinePlugin({ 8 | 'process.env.NODE_ENV': JSON.stringify(process.env.NODE_ENV) 9 | }) 10 | ]; 11 | 12 | if (process.env.NODE_ENV === 'production') { 13 | plugins.push( 14 | new webpack.optimize.UglifyJsPlugin({ 15 | compressor: { 16 | screw_ie8: true 17 | } 18 | }) 19 | ); 20 | 21 | plugins.push(new webpack.optimize.OccurenceOrderPlugin()); 22 | } 23 | 24 | var client = { 25 | entry: { 26 | client: './src/client/' 27 | }, 28 | output: { 29 | path: path.join(__dirname, '..', 'build', 'assets', 'js'), 30 | filename: 'bundle.js' 31 | }, 32 | resolve: { 33 | alias: { 34 | 'rx': 'rx-lite' 35 | }, 36 | extensions: ['', '.js'] 37 | }, 38 | module: { 39 | loaders: [ 40 | { 41 | test: /\.js$/, 42 | loaders: ['babel-loader'], 43 | exclude: /node_modules/ 44 | } 45 | ] 46 | }, 47 | plugins: plugins 48 | }; 49 | 50 | module.exports = client; -------------------------------------------------------------------------------- /examples/universal-counter/webpack/webpack.config.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | var server = require('./webpack.config.server'); 4 | var client = require('./webpack.config.client'); 5 | 6 | module.exports = [server, client]; -------------------------------------------------------------------------------- /examples/universal-counter/webpack/webpack.config.server.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | var webpack = require('webpack'); 4 | var path = require('path'); 5 | var fs = require('fs'); 6 | 7 | var plugins = [ 8 | new webpack.DefinePlugin({ 9 | 'process.env.NODE_ENV': JSON.stringify(process.env.NODE_ENV) 10 | }) 11 | ]; 12 | 13 | if (process.env.NODE_ENV === 'production') { 14 | plugins.push( 15 | new webpack.optimize.UglifyJsPlugin({ 16 | compressor: { 17 | screw_ie8: true 18 | } 19 | }) 20 | ); 21 | 22 | plugins.push(new webpack.optimize.OccurenceOrderPlugin()); 23 | } 24 | 25 | var nodeModules = {}; 26 | fs.readdirSync('node_modules') 27 | .filter(function(x) { 28 | return ['.bin'].indexOf(x) === -1; 29 | }) 30 | .forEach(function(mod) { 31 | nodeModules[mod] = 'commonjs ' + mod; 32 | }); 33 | 34 | var server = { 35 | entry: { 36 | server: './src/server/' 37 | }, 38 | target: 'node', 39 | output: { 40 | path: path.join(__dirname, '..', 'build', 'server'), 41 | filename: '[name].js' 42 | }, 43 | resolve: { 44 | alias: { 45 | 'rx': 'rx-lite' 46 | }, 47 | extensions: ['', '.js'] 48 | }, 49 | module: { 50 | loaders: [ 51 | { 52 | test: /\.js$/, 53 | loaders: ['babel-loader'], 54 | exclude: /node_modules/ 55 | }, 56 | { 57 | test: /\.json$/, loader: 'json-loader' 58 | } 59 | ] 60 | }, 61 | plugins: plugins, 62 | externals: nodeModules 63 | }; 64 | 65 | module.exports = server; -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "redux-core", 3 | "version": "0.1.1", 4 | "description": "Redux core", 5 | "main": "lib/index.js", 6 | "scripts": { 7 | "clean": "rimraf lib dist", 8 | "build:lib": "babel src --out-dir lib", 9 | "build:umd": "webpack src/index.js dist/redux-core.js --display-modules --progress && NODE_ENV=production webpack src/index.js dist/redux-core.min.js --display-modules --progress", 10 | "build": "npm run build:lib && npm run build:umd", 11 | "lint": "eslint src", 12 | "test": "echo \"Error: no test specified\" && exit 1" 13 | }, 14 | "repository": { 15 | "type": "git", 16 | "url": "git+https://github.com/jas-chen/redux-core.git" 17 | }, 18 | "keywords": [ 19 | "redux", 20 | "flux" 21 | ], 22 | "author": "Jas Chen", 23 | "license": "MIT", 24 | "bugs": { 25 | "url": "https://github.com/jas-chen/redux-core/issues" 26 | }, 27 | "homepage": "https://github.com/jas-chen/redux-core#readme", 28 | "devDependencies": { 29 | "babel-core": "^5.8.3", 30 | "babel-eslint": "^4.0.5", 31 | "babel-loader": "^5.3.2", 32 | "eslint": "^0.24.1", 33 | "eslint-config-airbnb": "0.0.6", 34 | "rimraf": "^2.4.2", 35 | "webpack": "^1.10.3" 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /src/createStore.js: -------------------------------------------------------------------------------- 1 | import isPlainObject from './utils/isPlainObject'; 2 | 3 | export default function createStore(reducer, initState) { 4 | if (typeof reducer !== 'function') { 5 | throw new Error('Expected the reducer to be a function.'); 6 | } 7 | 8 | const initAction = {type: '@@redux-core/INIT_' + (new Date()).getTime()}; 9 | let currentReducer = reducer; 10 | let state = currentReducer(initState, initAction); 11 | 12 | function dispatch(action) { 13 | if (!isPlainObject(action)) { 14 | throw new Error('Action must be a plain object.'); 15 | } 16 | 17 | state = currentReducer(state, action); 18 | return state; 19 | } 20 | 21 | function replaceReducer(newReducer) { 22 | currentReducer = newReducer; 23 | dispatch(initAction); 24 | } 25 | 26 | return { 27 | getState: () => state, 28 | dispatch, 29 | getReducer: () => currentReducer, 30 | replaceReducer 31 | }; 32 | } 33 | -------------------------------------------------------------------------------- /src/index.js: -------------------------------------------------------------------------------- 1 | import createStore from './createStore'; 2 | import combineReducers from './utils/combineReducers'; 3 | 4 | export default { 5 | createStore, 6 | combineReducers 7 | }; 8 | -------------------------------------------------------------------------------- /src/utils/combineReducers.js: -------------------------------------------------------------------------------- 1 | function pickReducer(reducers) { 2 | return Object.keys(reducers).reduce((result, key) => { 3 | const reducer = reducers[key]; 4 | if (typeof reducer === 'function') { 5 | result[key] = reducer; 6 | } 7 | 8 | return result; 9 | }, {}); 10 | } 11 | 12 | export default function combineReducers(reducers) { 13 | const finalReducers = pickReducer(reducers); 14 | const keys = Object.keys(finalReducers); 15 | 16 | return (state = {}, action = undefined) => { 17 | return keys.reduce((result, key) => { 18 | result[key] = finalReducers[key](state[key], action); 19 | return result; 20 | }, {}); 21 | }; 22 | } 23 | -------------------------------------------------------------------------------- /src/utils/isPlainObject.js: -------------------------------------------------------------------------------- 1 | /* 2 | https://github.com/gaearon/redux/blob/breaking-changes-1.0/src/utils/isPlainObject.js 3 | */ 4 | 5 | function fnToString(fn) { return Function.prototype.toString.call(fn); } 6 | 7 | /** 8 | * @param {any} obj The object to inspect. 9 | * @returns {boolean} True if the argument appears to be a plain object. 10 | */ 11 | export default function isPlainObject(obj) { 12 | if (!obj || typeof obj !== 'object') { 13 | return false; 14 | } 15 | 16 | const proto = typeof obj.constructor === 'function' ? Object.getPrototypeOf(obj) : Object.prototype; 17 | 18 | if (proto === null) { 19 | return true; 20 | } 21 | 22 | const constructor = proto.constructor; 23 | 24 | return typeof constructor === 'function' 25 | && constructor instanceof constructor 26 | && fnToString(constructor) === fnToString(Object); 27 | } 28 | -------------------------------------------------------------------------------- /webpack.config.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | var webpack = require('webpack'); 4 | 5 | var plugins = [ 6 | new webpack.DefinePlugin({ 7 | 'process.env.NODE_ENV': JSON.stringify(process.env.NODE_ENV) 8 | }), 9 | new webpack.optimize.OccurenceOrderPlugin() 10 | ]; 11 | 12 | if (process.env.NODE_ENV === 'production') { 13 | plugins.push( 14 | new webpack.optimize.UglifyJsPlugin({ 15 | compressor: { 16 | screw_ie8: true, 17 | warnings: false 18 | } 19 | }) 20 | ); 21 | } 22 | 23 | module.exports = { 24 | entry: './src/index', 25 | output: { 26 | library: 'RxRedux', 27 | libraryTarget: 'umd' 28 | }, 29 | module: { 30 | loaders: [ 31 | {test: /\.js$/, loaders: ['babel-loader'], exclude: /node_modules/} 32 | ] 33 | }, 34 | plugins: plugins, 35 | resolve: { 36 | extensions: ['', '.js'] 37 | } 38 | }; --------------------------------------------------------------------------------