├── examples ├── .babelrc ├── README.md ├── nextjs-preact-code-splitting │ ├── pages │ │ ├── about.js │ │ └── index.js │ ├── next.config.js │ ├── config │ │ └── redux.js │ ├── README.md │ ├── package.json │ ├── server.js │ └── containers │ │ ├── homepage.js │ │ └── about.js ├── async │ ├── .gitignore │ ├── src │ │ ├── stores │ │ │ ├── selectedReddit.js │ │ │ └── postsByReddit.js │ │ ├── components │ │ │ ├── Posts.js │ │ │ └── Picker.js │ │ ├── index.js │ │ └── containers │ │ │ └── App.js │ ├── public │ │ └── index.html │ ├── package.json │ └── README.md ├── buildAll.js └── testAll.js ├── test ├── .eslintrc ├── combineReducers.spec.js ├── namespaceConfig.spec.js ├── actionCreator.js ├── createStore.spec.js └── bindActionCreators.spec.js ├── .gitignore ├── src ├── index.js ├── rootReducer.js ├── namespace.js └── object.js ├── .babelrc ├── lib ├── index.js ├── rootReducer.js ├── namespace.js └── object.js ├── webpack.config.js ├── LICENSE-redux.md ├── LICENSE.md ├── README.md └── package.json /examples/.babelrc: -------------------------------------------------------------------------------- 1 | { 2 | "babelrc": false 3 | } -------------------------------------------------------------------------------- /test/.eslintrc: -------------------------------------------------------------------------------- 1 | { 2 | "env": { 3 | "jest": true 4 | } 5 | } 6 | -------------------------------------------------------------------------------- /examples/README.md: -------------------------------------------------------------------------------- 1 | # Examples 2 | 3 | Examples are based on examples from Redux repo. -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | *.log 3 | node_modules 4 | .next 5 | .vscode 6 | dist 7 | es 8 | coverage 9 | _book 10 | -------------------------------------------------------------------------------- /examples/nextjs-preact-code-splitting/pages/about.js: -------------------------------------------------------------------------------- 1 | import {reduxPage} from '../config/redux' 2 | import About from '../containers/about' 3 | 4 | export default reduxPage(About) 5 | -------------------------------------------------------------------------------- /src/index.js: -------------------------------------------------------------------------------- 1 | export {namespaceConfig} from './namespace' 2 | export {dynamicPropertyConfig, staticPropertyConfig} from './object' 3 | export {rootReducer} from './rootReducer' 4 | -------------------------------------------------------------------------------- /examples/nextjs-preact-code-splitting/pages/index.js: -------------------------------------------------------------------------------- 1 | import {reduxPage} from '../config/redux' 2 | import Homepage from '../containers/homepage' 3 | 4 | export default reduxPage(Homepage) 5 | -------------------------------------------------------------------------------- /.babelrc: -------------------------------------------------------------------------------- 1 | { 2 | "presets": ["es2015"], 3 | "plugins": [ 4 | "transform-object-rest-spread", 5 | "transform-class-properties", 6 | "transform-es3-property-literals" 7 | ] 8 | } 9 | -------------------------------------------------------------------------------- /examples/async/.gitignore: -------------------------------------------------------------------------------- 1 | # See http://help.github.com/ignore-files/ for more about ignoring files. 2 | 3 | # dependencies 4 | node_modules 5 | 6 | # production 7 | build 8 | 9 | # misc 10 | .DS_Store 11 | npm-debug.log 12 | -------------------------------------------------------------------------------- /examples/async/src/stores/selectedReddit.js: -------------------------------------------------------------------------------- 1 | import {namespaceConfig} from 'fast-redux' 2 | 3 | const DEFAULT_STATE = 'reactjs' 4 | export const { 5 | action, 6 | getState: getSelectedReddit 7 | } = namespaceConfig('selectedReddit', DEFAULT_STATE) 8 | 9 | export const selectReddit = action('selectReddit', 10 | (state, reddit) => reddit 11 | ) 12 | -------------------------------------------------------------------------------- /examples/async/src/components/Posts.js: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import PropTypes from 'proptypes' 3 | 4 | const Posts = ({posts}) => ( 5 | 10 | ) 11 | 12 | Posts.propTypes = { 13 | posts: PropTypes.array.isRequired 14 | } 15 | 16 | export default Posts 17 | -------------------------------------------------------------------------------- /examples/nextjs-preact-code-splitting/next.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | webpack: function (config, { dev }) { 3 | // For the development version, we'll use React. 4 | // Because, it support react hot loading and so on. 5 | if (dev) { 6 | return config 7 | } 8 | 9 | config.resolve.alias = { 10 | 'react': 'preact-compat/dist/preact-compat', 11 | 'react-dom': 'preact-compat/dist/preact-compat' 12 | } 13 | 14 | return config 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /examples/nextjs-preact-code-splitting/config/redux.js: -------------------------------------------------------------------------------- 1 | import { createStore, applyMiddleware } from 'redux' 2 | import { composeWithDevTools } from 'redux-devtools-extension' 3 | import thunkMiddleware from 'redux-thunk' 4 | import withRedux from 'next-redux-wrapper' 5 | import { rootReducer } from 'fast-redux' 6 | 7 | export const initStore = (initialState = {}) => { 8 | return createStore(rootReducer, initialState, 9 | composeWithDevTools(applyMiddleware(thunkMiddleware))) 10 | } 11 | 12 | export const reduxPage = (comp) => withRedux(initStore)(comp) 13 | -------------------------------------------------------------------------------- /examples/nextjs-preact-code-splitting/README.md: -------------------------------------------------------------------------------- 1 | # Fast Redux with code-splitting and async components loading demo 2 | 3 | This demo is based on [Next.js](https://github.com/zeit/next.js) using-preact [example](https://github.com/zeit/next.js/tree/master/examples/using-preact). 4 | 5 | ## The idea behind the example 6 | 7 | This example uses [Preact](https://github.com/developit/preact) instead of React for production mode. For development mode is still used React to allow builtin Next.js hot-reloading. 8 | 9 | `next.config.js` customizes default webpack config to support [preact-compat](https://github.com/developit/preact-compat). 10 | -------------------------------------------------------------------------------- /src/rootReducer.js: -------------------------------------------------------------------------------- 1 | const DEFAULT_STATE = {} 2 | 3 | export function rootReducer (state, action) { 4 | // init Redux with empty state 5 | if (state === undefined) return DEFAULT_STATE 6 | let {creator, reducer, payload} = action 7 | if (creator && payload && typeof reducer === 'function') { 8 | // handle fast-redux action 9 | let {ns, getState} = creator 10 | let nsState = getState(state) 11 | let newNsState = reducer(nsState, ...payload) 12 | if (newNsState === nsState) return state // nothing changed 13 | return {...state, [ns]: newNsState} 14 | } 15 | // return unchanged state for all unknown actions 16 | return state 17 | } 18 | -------------------------------------------------------------------------------- /examples/async/src/index.js: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import { render } from 'react-dom' 3 | import { createStore, applyMiddleware } from 'redux' 4 | import { Provider } from 'react-redux' 5 | import { composeWithDevTools } from 'redux-devtools-extension' 6 | import thunkMiddleware from 'redux-thunk' 7 | import { rootReducer } from 'fast-redux' 8 | 9 | import App from './containers/App' 10 | 11 | const preloadedState = {selectedReddit: 'javascript'} 12 | const store = createStore(rootReducer, preloadedState, composeWithDevTools(applyMiddleware(thunkMiddleware))) 13 | 14 | render( 15 | 16 | 17 | , 18 | document.getElementById('root') 19 | ) 20 | -------------------------------------------------------------------------------- /examples/async/src/components/Picker.js: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import PropTypes from 'proptypes' 3 | 4 | const Picker = ({ value, onChange, options }) => ( 5 | 6 |

{value}

7 | 15 |
16 | ) 17 | 18 | Picker.propTypes = { 19 | options: PropTypes.arrayOf( 20 | PropTypes.string.isRequired 21 | ).isRequired, 22 | value: PropTypes.string.isRequired, 23 | onChange: PropTypes.func.isRequired 24 | } 25 | 26 | export default Picker 27 | -------------------------------------------------------------------------------- /examples/async/public/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | Redux Async Example 7 | 8 | 9 |
10 | 20 | 21 | 22 | -------------------------------------------------------------------------------- /examples/nextjs-preact-code-splitting/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "nextjs-preact-code-splitting", 3 | "version": "1.0.0", 4 | "scripts": { 5 | "dev": "node server.js", 6 | "build": "next build", 7 | "start": "NODE_ENV=production node server.js" 8 | }, 9 | "dependencies": { 10 | "fast-redux": "latest", 11 | "module-alias": "^2.0.0", 12 | "next": "~4.2.2", 13 | "next-redux-wrapper": "~1.3.2", 14 | "preact": "^8.2.1", 15 | "preact-compat": "^3.17.0", 16 | "react": "~16.2.0", 17 | "react-dom": "~16.2.0", 18 | "react-redux": "~5.0.5", 19 | "redux": "~3.7.2", 20 | "redux-devtools-extension": "~2.13.2", 21 | "redux-thunk": "~2.2.0" 22 | }, 23 | "author": "", 24 | "license": "ISC" 25 | } 26 | -------------------------------------------------------------------------------- /examples/async/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "async", 3 | "version": "0.0.1", 4 | "private": true, 5 | "devDependencies": { 6 | "gh-pages": "~1.1.0", 7 | "react-scripts": "^1.0.17", 8 | "redux-logger": "^3.0.6" 9 | }, 10 | "homepage": "https://dogada.github.io/fast-redux", 11 | "dependencies": { 12 | "fast-redux": "latest", 13 | "proptypes": "~1.1.0", 14 | "react": "~16.2.0", 15 | "react-dom": "~16.2.0", 16 | "react-redux": "~5.0.6", 17 | "redux": "~3.7.2", 18 | "redux-devtools-extension": "~2.13.2", 19 | "redux-thunk": "~2.2.0" 20 | }, 21 | "scripts": { 22 | "start": "react-scripts start", 23 | "build": "react-scripts build", 24 | "eject": "react-scripts eject", 25 | "predeploy": "npm run build", 26 | "deploy": "gh-pages -d build" 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /test/combineReducers.spec.js: -------------------------------------------------------------------------------- 1 | /* global describe, expect, it */ 2 | 3 | import { createStore, combineReducers } from 'redux' 4 | import { rootReducer } from '../src' 5 | 6 | describe('combineReducers', () => { 7 | it('should allow to use together fast-redux root reducer and Redux reducers', () => { 8 | const counter = (state = 0, action) => state 9 | const reducer = combineReducers({testNs: rootReducer, counter}) 10 | const store = createStore(reducer) 11 | 12 | expect(store.getState()).toEqual({ 13 | counter: 0, 14 | testNs: {} 15 | }) 16 | 17 | const preloadedStore = createStore(reducer, { 18 | counter: 42, 19 | testNs: {name: 'value'} 20 | }) 21 | 22 | expect(preloadedStore.getState()).toEqual({ 23 | counter: 42, 24 | testNs: {name: 'value'} 25 | }) 26 | }) 27 | }) 28 | -------------------------------------------------------------------------------- /examples/nextjs-preact-code-splitting/server.js: -------------------------------------------------------------------------------- 1 | const dev = process.env.NODE_ENV !== 'production' 2 | const moduleAlias = require('module-alias') 3 | 4 | // For the development version, we'll use React. 5 | // Because, it support react hot loading and so on. 6 | if (!dev) { 7 | moduleAlias.addAlias('react', 'preact-compat') 8 | moduleAlias.addAlias('react-dom', 'preact-compat') 9 | } 10 | 11 | const { createServer } = require('http') 12 | const { parse } = require('url') 13 | const next = require('next') 14 | 15 | const app = next({ dev }) 16 | const handle = app.getRequestHandler() 17 | 18 | app.prepare() 19 | .then(() => { 20 | createServer((req, res) => { 21 | const parsedUrl = parse(req.url, true) 22 | handle(req, res, parsedUrl) 23 | }) 24 | .listen(3000, (err) => { 25 | if (err) throw err 26 | console.log('> Ready on http://localhost:3000') 27 | }) 28 | }) 29 | -------------------------------------------------------------------------------- /test/namespaceConfig.spec.js: -------------------------------------------------------------------------------- 1 | /* global describe, expect, it */ 2 | 3 | import { namespaceConfig } from '../src' 4 | 5 | const INITIAL = { todos: [] } 6 | 7 | describe('namespaceConfig', () => { 8 | 9 | it('returns action function bound to namespace', () => { 10 | const DEFAULT_STATE = 0 11 | const { action } = namespaceConfig('my', DEFAULT_STATE) 12 | expect(typeof action).toBe('function') 13 | 14 | let addReducer = (state = DEFAULT_STATE, x) => state + x 15 | let add = action('addReducer', addReducer) 16 | expect(typeof add).toBe('function') 17 | let addAction = add(2) 18 | 19 | expect(addAction).toEqual({ 20 | type: 'my/addReducer', 21 | payload: [2], 22 | reducer: addReducer, 23 | creator: action 24 | }) 25 | 26 | expect(Object.keys(addAction)).toEqual([ 27 | 'type', 28 | 'payload', 29 | 'creator', 30 | 'reducer' 31 | ]) 32 | }) 33 | }) 34 | -------------------------------------------------------------------------------- /lib/index.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | Object.defineProperty(exports, "__esModule", { 4 | value: true 5 | }); 6 | 7 | var _namespace = require('./namespace'); 8 | 9 | Object.defineProperty(exports, 'namespaceConfig', { 10 | enumerable: true, 11 | get: function get() { 12 | return _namespace.namespaceConfig; 13 | } 14 | }); 15 | 16 | var _object = require('./object'); 17 | 18 | Object.defineProperty(exports, 'dynamicPropertyConfig', { 19 | enumerable: true, 20 | get: function get() { 21 | return _object.dynamicPropertyConfig; 22 | } 23 | }); 24 | Object.defineProperty(exports, 'staticPropertyConfig', { 25 | enumerable: true, 26 | get: function get() { 27 | return _object.staticPropertyConfig; 28 | } 29 | }); 30 | 31 | var _rootReducer = require('./rootReducer'); 32 | 33 | Object.defineProperty(exports, 'rootReducer', { 34 | enumerable: true, 35 | get: function get() { 36 | return _rootReducer.rootReducer; 37 | } 38 | }); -------------------------------------------------------------------------------- /webpack.config.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | var webpack = require('webpack') 4 | 5 | var env = process.env.NODE_ENV 6 | var config = { 7 | module: { 8 | loaders: [ 9 | { test: /\.js$/, loaders: ['babel-loader'], exclude: /node_modules/ } 10 | ] 11 | }, 12 | output: { 13 | library: 'fast-', 14 | libraryTarget: 'umd' 15 | }, 16 | plugins: [ 17 | new webpack.optimize.OccurrenceOrderPlugin(), 18 | new webpack.DefinePlugin({ 19 | 'process.env.NODE_ENV': JSON.stringify(env) 20 | }) 21 | ] 22 | } 23 | 24 | if (env === 'production') { 25 | config.plugins.push( 26 | new webpack.optimize.UglifyJsPlugin({ 27 | compressor: { 28 | pure_getters: true, 29 | unsafe: true, 30 | unsafe_comps: true, 31 | warnings: false, 32 | screw_ie8: false 33 | }, 34 | mangle: { 35 | screw_ie8: false 36 | }, 37 | output: { 38 | screw_ie8: false 39 | } 40 | }) 41 | ) 42 | } 43 | 44 | module.exports = config 45 | -------------------------------------------------------------------------------- /test/actionCreator.js: -------------------------------------------------------------------------------- 1 | /* global describe, expect, it */ 2 | 3 | import { namespaceConfig } from '../src' 4 | 5 | describe('action', () => { 6 | const DEFAULT_STATE = 0 7 | const { action } = namespaceConfig('my', DEFAULT_STATE) 8 | const addReducer = (state = DEFAULT_STATE, x) => state + x 9 | 10 | it('accepts reducer and returns function to create actions', () => { 11 | expect(typeof action).toBe('function') 12 | 13 | let add = action(addReducer) 14 | expect(typeof add).toBe('function') 15 | let addAction = add(2) 16 | 17 | expect(addAction).toEqual({ 18 | ns: 'my', 19 | reducer: addReducer, 20 | type: '@@fast-redux/my/addReducer', 21 | payload: [2] 22 | }) 23 | }) 24 | 25 | it('creates actions with shape {ns, reducer, type, payload}', () => { 26 | let add = action(addReducer) 27 | let addAction = add(2) 28 | 29 | expect(addAction).toEqual({ 30 | ns: 'my', 31 | reducer: addReducer, 32 | type: '@@fast-redux/my/addReducer', 33 | payload: [2] 34 | }) 35 | }) 36 | }) 37 | -------------------------------------------------------------------------------- /examples/nextjs-preact-code-splitting/containers/homepage.js: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import {bindActionCreators} from 'redux' 3 | import {connect} from 'react-redux' 4 | import {namespaceConfig} from 'fast-redux' 5 | import Link from 'next/link' 6 | 7 | const DEFAULT_STATE = {build: 1} 8 | 9 | const {action, getState: getHomepageState} = namespaceConfig('homepage', DEFAULT_STATE) 10 | 11 | const bumpBuild = action('bumpBuild', 12 | (state, increment) => ({...state, build: state.build + increment}) 13 | ) 14 | 15 | const Homepage = ({ build, bumpBuild }) => ( 16 |
17 |

Homepage

18 |

Current build: {build}

19 |

20 | About Us 21 |
22 | ) 23 | 24 | function mapStateToProps (state) { 25 | return getHomepageState(state) 26 | } 27 | 28 | function mapDispatchToProps (dispatch) { 29 | return bindActionCreators({ bumpBuild }, dispatch) 30 | } 31 | 32 | export default connect(mapStateToProps, mapDispatchToProps)(Homepage) 33 | -------------------------------------------------------------------------------- /examples/nextjs-preact-code-splitting/containers/about.js: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import {bindActionCreators} from 'redux' 3 | import {connect} from 'react-redux' 4 | import {namespaceConfig} from 'fast-redux' 5 | import Link from 'next/link' 6 | 7 | const DEFAULT_STATE = {version: 1} 8 | 9 | const {action, getState: getAboutState} = namespaceConfig('about', DEFAULT_STATE) 10 | 11 | const bumpVersion = action('bumpVersion', 12 | (state, increment) => ({...state, version: state.version + increment}) 13 | ) 14 | 15 | const About = ({ version, bumpVersion }) => ( 16 |
17 |

About us

18 |

Current version: {version}

19 |

20 | Homepage 21 |
22 | ) 23 | 24 | function mapStateToProps (state) { 25 | return getAboutState(state, 'version') 26 | } 27 | 28 | function mapDispatchToProps (dispatch) { 29 | return bindActionCreators({ bumpVersion }, dispatch) 30 | } 31 | 32 | export default connect(mapStateToProps, mapDispatchToProps)(About) 33 | -------------------------------------------------------------------------------- /test/createStore.spec.js: -------------------------------------------------------------------------------- 1 | /* global describe, expect, it */ 2 | 3 | import { createStore } from 'redux' 4 | import { rootReducer } from '../src' 5 | 6 | function initStore (preloadedState) { 7 | const store = createStore(rootReducer, preloadedState) 8 | return store 9 | } 10 | 11 | describe('createStore', () => { 12 | it('should restore primitive state from initial value', () => { 13 | const state = { 14 | ns1: 1, 15 | ns2: 'name' 16 | } 17 | const store = initStore(state) 18 | expect(store.getState()).toEqual(state) 19 | }) 20 | 21 | it('should restore state from initial value', () => { 22 | const state = { 23 | ns1: {id: 1, text: 'First'}, 24 | ns2: {id: 2, text: 'Second'} 25 | } 26 | const store = initStore(state) 27 | expect(store.getState()).toEqual(state) 28 | }) 29 | 30 | it('should restore 2-level state from initial value', () => { 31 | const state = { 32 | ns1: {data: [{id: 1, value: 'First'}]}, 33 | ns2: {data: [{id: 2, value: 'Second'}]} 34 | } 35 | const store = initStore(state) 36 | expect(store.getState()).toEqual(state) 37 | }) 38 | }) 39 | -------------------------------------------------------------------------------- /LICENSE-redux.md: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2015-present Dan Abramov 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 | -------------------------------------------------------------------------------- /src/namespace.js: -------------------------------------------------------------------------------- 1 | function namespaceAction (ns, defaultState) { 2 | function creator (name, reducer) { 3 | if (typeof reducer !== 'function') throw new Error('Reducer must be a function.') 4 | return (...args) => ({ 5 | type: `${ns}/${name}`, 6 | payload: args, 7 | creator, 8 | reducer 9 | }) 10 | } 11 | creator.ns = ns 12 | creator.defaultState = defaultState 13 | creator.getState = (state) => (ns in state ? state[ns] : defaultState) 14 | return creator 15 | } 16 | 17 | const getNamespaceState = (ns, defaultState) => (state, ...keys) => { 18 | let nsState = ns in state ? state[ns] : defaultState 19 | if (keys.length === 0) return nsState 20 | let res = {} 21 | for (let i = keys.length; --i >= 0;) { 22 | let key = keys[i] 23 | res[key] = nsState[key] 24 | } 25 | return res 26 | } 27 | 28 | /** 29 | * Return config for the fast redux namespace. 30 | * @param {String} ns 31 | * @param {*} defaultState 32 | */ 33 | export function namespaceConfig (ns, defaultState) { 34 | return { 35 | action: namespaceAction(ns, defaultState), 36 | getState: getNamespaceState(ns, defaultState) 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /examples/buildAll.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Runs an ordered set of commands within each of the build directories. 3 | */ 4 | 5 | var fs = require('fs') 6 | var path = require('path') 7 | var { spawnSync } = require('child_process') 8 | 9 | var exampleDirs = fs.readdirSync(__dirname).filter((file) => { 10 | return fs.statSync(path.join(__dirname, file)).isDirectory() 11 | }) 12 | 13 | // Ordering is important here. `npm install` must come first. 14 | var cmdArgs = [ 15 | { cmd: 'npm', args: [ 'install' ] }, 16 | { cmd: 'webpack', args: [ 'index.js' ] } 17 | ] 18 | 19 | for (const dir of exampleDirs) { 20 | for (const cmdArg of cmdArgs) { 21 | // declare opts in this scope to avoid https://github.com/joyent/node/issues/9158 22 | const opts = { 23 | cwd: path.join(__dirname, dir), 24 | stdio: 'inherit' 25 | } 26 | let result = {} 27 | if (process.platform === 'win32') { 28 | result = spawnSync(cmdArg.cmd + '.cmd', cmdArg.args, opts) 29 | } else { 30 | result = spawnSync(cmdArg.cmd, cmdArg.args, opts) 31 | } 32 | if (result.status !== 0) { 33 | throw new Error('Building examples exited with non-zero') 34 | } 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /examples/testAll.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Runs an ordered set of commands within each of the build directories. 3 | */ 4 | 5 | var fs = require('fs') 6 | var path = require('path') 7 | var { spawnSync } = require('child_process') 8 | 9 | var exampleDirs = fs.readdirSync(__dirname).filter((file) => { 10 | return fs.statSync(path.join(__dirname, file)).isDirectory() 11 | }) 12 | 13 | // Ordering is important here. `npm install` must come first. 14 | var cmdArgs = [ 15 | { cmd: 'npm', args: [ 'install' ] }, 16 | { cmd: 'npm', args: [ 'test' ] } 17 | ] 18 | 19 | for (const dir of exampleDirs) { 20 | for (const cmdArg of cmdArgs) { 21 | // declare opts in this scope to avoid https://github.com/joyent/node/issues/9158 22 | const opts = { 23 | cwd: path.join(__dirname, dir), 24 | stdio: 'inherit' 25 | } 26 | 27 | let result = {} 28 | if (process.platform === 'win32') { 29 | result = spawnSync(cmdArg.cmd + '.cmd', cmdArg.args, opts) 30 | } else { 31 | result = spawnSync(cmdArg.cmd, cmdArg.args, opts) 32 | } 33 | if (result.status !== 0) { 34 | throw new Error('Building examples exited with non-zero') 35 | } 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /LICENSE.md: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2017-present Dmytro Dogadailo (https://dogada.org) 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 | -------------------------------------------------------------------------------- /lib/rootReducer.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | Object.defineProperty(exports, "__esModule", { 4 | value: true 5 | }); 6 | 7 | var _extends = Object.assign || function (target) { for (var i = 1; i < arguments.length; i++) { var source = arguments[i]; for (var key in source) { if (Object.prototype.hasOwnProperty.call(source, key)) { target[key] = source[key]; } } } return target; }; 8 | 9 | exports.rootReducer = rootReducer; 10 | 11 | function _defineProperty(obj, key, value) { if (key in obj) { Object.defineProperty(obj, key, { value: value, enumerable: true, configurable: true, writable: true }); } else { obj[key] = value; } return obj; } 12 | 13 | function _toConsumableArray(arr) { if (Array.isArray(arr)) { for (var i = 0, arr2 = Array(arr.length); i < arr.length; i++) { arr2[i] = arr[i]; } return arr2; } else { return Array.from(arr); } } 14 | 15 | var DEFAULT_STATE = {}; 16 | 17 | function rootReducer(state, action) { 18 | // init Redux with empty state 19 | if (state === undefined) return DEFAULT_STATE; 20 | var creator = action.creator, 21 | reducer = action.reducer, 22 | payload = action.payload; 23 | 24 | if (creator && payload && typeof reducer === 'function') { 25 | // handle fast-redux action 26 | var ns = creator.ns, 27 | getState = creator.getState; 28 | 29 | var nsState = getState(state); 30 | var newNsState = reducer.apply(undefined, [nsState].concat(_toConsumableArray(payload))); 31 | if (newNsState === nsState) return state; // nothing changed 32 | return _extends({}, state, _defineProperty({}, ns, newNsState)); 33 | } 34 | // return unchanged state for all unknown actions 35 | return state; 36 | } -------------------------------------------------------------------------------- /examples/async/README.md: -------------------------------------------------------------------------------- 1 | # fast-redux Async Example 2 | 3 | This project template was built with [Create React App](https://github.com/facebookincubator/create-react-app). 4 | 5 | ## Available Scripts 6 | 7 | In the project directory, you can run: 8 | 9 | ### `npm start` 10 | 11 | Runs the app in the development mode.
12 | Open [http://localhost:3000](http://localhost:3000) to view it in the browser. 13 | 14 | The page will reload if you make edits.
15 | You will also see any lint errors in the console. 16 | 17 | ### `npm run build` 18 | 19 | Builds the app for production to the `build` folder.
20 | It correctly bundles React in production mode and optimizes the build for the best performance. 21 | 22 | The build is minified and the filenames include the hashes.
23 | Your app is ready to be deployed! 24 | 25 | ### `npm run eject` 26 | 27 | **Note: this is a one-way operation. Once you `eject`, you can’t go back!** 28 | 29 | If you aren’t satisfied with the build tool and configuration choices, you can `eject` at any time. This command will remove the single build dependency from your project. 30 | 31 | Instead, it will copy all the configuration files and the transitive dependencies (Webpack, Babel, ESLint, etc) right into your project so you have full control over them. All of the commands except `eject` will still work, but they will point to the copied scripts so you can tweak them. At this point you’re on your own. 32 | 33 | You don’t have to ever use `eject`. The curated feature set is suitable for small and middle deployments, and you shouldn’t feel obligated to use this feature. However we understand that this tool wouldn’t be useful if you couldn’t customize it when you are ready for it. 34 | 35 | -------------------------------------------------------------------------------- /lib/namespace.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | Object.defineProperty(exports, "__esModule", { 4 | value: true 5 | }); 6 | exports.namespaceConfig = namespaceConfig; 7 | function namespaceAction(ns, defaultState) { 8 | function creator(name, reducer) { 9 | if (typeof reducer !== 'function') throw new Error('Reducer must be a function.'); 10 | return function () { 11 | for (var _len = arguments.length, args = Array(_len), _key = 0; _key < _len; _key++) { 12 | args[_key] = arguments[_key]; 13 | } 14 | 15 | return { 16 | type: ns + '/' + name, 17 | payload: args, 18 | creator: creator, 19 | reducer: reducer 20 | }; 21 | }; 22 | } 23 | creator.ns = ns; 24 | creator.defaultState = defaultState; 25 | creator.getState = function (state) { 26 | return ns in state ? state[ns] : defaultState; 27 | }; 28 | return creator; 29 | } 30 | 31 | var getNamespaceState = function getNamespaceState(ns, defaultState) { 32 | return function (state) { 33 | for (var _len2 = arguments.length, keys = Array(_len2 > 1 ? _len2 - 1 : 0), _key2 = 1; _key2 < _len2; _key2++) { 34 | keys[_key2 - 1] = arguments[_key2]; 35 | } 36 | 37 | var nsState = ns in state ? state[ns] : defaultState; 38 | if (keys.length === 0) return nsState; 39 | var res = {}; 40 | for (var i = keys.length; --i >= 0;) { 41 | var key = keys[i]; 42 | res[key] = nsState[key]; 43 | } 44 | return res; 45 | }; 46 | }; 47 | 48 | /** 49 | * Return config for the fast redux namespace. 50 | * @param {String} ns 51 | * @param {*} defaultState 52 | */ 53 | function namespaceConfig(ns, defaultState) { 54 | return { 55 | action: namespaceAction(ns, defaultState), 56 | getState: getNamespaceState(ns, defaultState) 57 | }; 58 | } -------------------------------------------------------------------------------- /examples/async/src/stores/postsByReddit.js: -------------------------------------------------------------------------------- 1 | import { namespaceConfig, dynamicPropertyConfig } from 'fast-redux' 2 | 3 | const DEFAULT_STATE = {} 4 | const {action} = namespaceConfig('postsByReddit', DEFAULT_STATE) 5 | 6 | const DEFAULT_REDDIT_STATE = { 7 | isFetching: false, 8 | didInvalidate: false, 9 | items: [] 10 | } 11 | 12 | const { 13 | propertyAction: redditAction, 14 | getPropertyState: getRedditState 15 | } = dynamicPropertyConfig(action, DEFAULT_REDDIT_STATE) 16 | 17 | export {getRedditState} 18 | 19 | export const invalidateReddit = redditAction('invalidateReddit', 20 | (state) => ({ 21 | ...state, 22 | didInvalidate: true 23 | }) 24 | ) 25 | 26 | export const requestPosts = redditAction('requestPosts', 27 | (state) => ({ 28 | ...state, 29 | items: [], 30 | isFetching: true, 31 | didInvalidate: false 32 | }) 33 | ) 34 | 35 | export const receivePosts = redditAction('receivePosts', 36 | (state, json) => ({ 37 | ...state, 38 | isFetching: false, 39 | didInvalidate: false, 40 | items: json.data.children.map(child => child.data), 41 | lastUpdated: Date.now() 42 | }) 43 | ) 44 | 45 | export const fetchPosts = (reddit) => (dispatch) => { 46 | dispatch(requestPosts(reddit)) 47 | window.fetch(`https://www.reddit.com/r/${reddit}.json`) 48 | .then(response => response.json()) 49 | .then(json => dispatch(receivePosts(reddit, json))) 50 | } 51 | 52 | const shouldFetchPosts = (posts) => { 53 | if (!posts.items || posts.items.length === 0) { 54 | return true 55 | } 56 | if (posts.isFetching) { 57 | return false 58 | } 59 | return posts.didInvalidate 60 | } 61 | 62 | export const fetchPostsIfNeed = (reddit) => (dispatch, getState) => { 63 | const state = getRedditState(getState(), reddit) 64 | if (shouldFetchPosts(state)) { 65 | return dispatch(fetchPosts(reddit)) 66 | } 67 | } 68 | -------------------------------------------------------------------------------- /test/bindActionCreators.spec.js: -------------------------------------------------------------------------------- 1 | /* global describe, expect, it, beforeEach */ 2 | 3 | import { bindActionCreators, createStore } from 'redux' 4 | import { namespaceConfig, rootReducer } from '../src' 5 | 6 | const DEFAULT_STATE = [] 7 | const {action, getState: getTestState} = namespaceConfig('test', DEFAULT_STATE) 8 | 9 | function nextId (state) { 10 | return Math.max(0, ...state.map(todo => todo.id)) + 1 11 | } 12 | 13 | const addTodo = action('addTodo', 14 | function (state = DEFAULT_STATE, text) { 15 | return [...state, {id: nextId(state), text}] 16 | }) 17 | 18 | function addTodoAsync (text) { 19 | return dispatch => setTimeout(() => { 20 | dispatch(addTodo(text)) 21 | }, 1000) 22 | } 23 | 24 | const actions = {addTodo, addTodoAsync} 25 | 26 | function cloneOnlyFunctions (obj) { 27 | let clone = { ...obj } 28 | Object.keys(clone).forEach(key => { 29 | if (typeof clone[key] !== 'function') { 30 | delete clone[key] 31 | } 32 | }) 33 | return clone 34 | } 35 | 36 | function initStore () { 37 | return createStore(rootReducer) 38 | } 39 | 40 | describe('bindActionCreators', () => { 41 | let store, actionFunctions 42 | 43 | beforeEach(() => { 44 | store = initStore() 45 | actionFunctions = cloneOnlyFunctions(actions) 46 | }) 47 | 48 | it('wraps the action creators with the dispatch function', () => { 49 | const boundActionCreators = bindActionCreators(actions, store.dispatch) 50 | expect( 51 | Object.keys(boundActionCreators) 52 | ).toEqual( 53 | Object.keys(actionFunctions) 54 | ) 55 | 56 | const action = boundActionCreators.addTodo('Hello') 57 | expect(action).toEqual( 58 | actions.addTodo('Hello') 59 | ) 60 | expect(getTestState(store.getState())).toEqual([ 61 | { id: 1, text: 'Hello' } 62 | ]) 63 | }) 64 | 65 | it('skips non-function values in the passed object', () => { 66 | const boundActionCreators = bindActionCreators({ 67 | ...actions, 68 | foo: 42, 69 | bar: 'baz', 70 | wow: undefined, 71 | much: {}, 72 | test: null 73 | }, store.dispatch) 74 | expect( 75 | Object.keys(boundActionCreators) 76 | ).toEqual( 77 | Object.keys(actionFunctions) 78 | ) 79 | }) 80 | 81 | it('supports wrapping a single function only', () => { 82 | const action = actions.addTodo 83 | const boundActionCreator = bindActionCreators(action, store.dispatch) 84 | 85 | const descriptor = boundActionCreator('Hello') 86 | expect(descriptor).toEqual(action('Hello')) 87 | expect(getTestState(store.getState())).toEqual([ 88 | { id: 1, text: 'Hello' } 89 | ]) 90 | }) 91 | }) 92 | -------------------------------------------------------------------------------- /src/object.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Actions for working directly with the properties of an object stored in a 3 | * state. For example object 'posts' that holds various reddits states. 4 | * You can find example of usage in: 5 | * examples/async/src/stores/postsByReddit.js 6 | */ 7 | 8 | const makePropertyAction = (action, propertyName) => (name, reducer) => { 9 | return action(name, 10 | (state, ...args) => { 11 | return { 12 | ...state, 13 | [propertyName]: reducer(state[propertyName], ...args) 14 | } 15 | } 16 | ) 17 | } 18 | 19 | /** 20 | * Return utility functions to work directly with a single property of a parent object. 21 | * Property name is provided on config stage and can't be changed during action call. 22 | * Default value for the property shoult be set in a namespaceConfig 23 | * @param {function} action an action that accepts the object as a state 24 | * @param {function} getObjectState function to obtain state of parent namespace 25 | * @param {string} propertyName name of object's property 26 | * @param {*} defaultPropertyState initial value of a property 27 | */ 28 | export function staticPropertyConfig (action, propertyName) { 29 | const getPropertyState = (state) => action.getState(state)[propertyName] 30 | return { 31 | propertyAction: makePropertyAction(action, propertyName), 32 | getPropertyState 33 | } 34 | } 35 | 36 | /** 37 | * Actions for working directly with the properties of an object stored in a 38 | * state. For example object 'posts' that holds various reddits states. 39 | * You can find example of usage in: 40 | * examples/async/src/actions/postsByReddit.js 41 | */ 42 | 43 | const makeObjectAction = (action, defaultPropertyState) => (name, reducer) => { 44 | return action(name, 45 | (state, key, ...args) => { 46 | let nestedState = state[key] || defaultPropertyState 47 | return { 48 | ...state, 49 | [key]: reducer(nestedState, ...args) 50 | } 51 | }) 52 | } 53 | 54 | /** 55 | * Return utility functions to work directly with properties of a parent object. 56 | * Property name is provided dynamically as first argument of action. 57 | * @param {function} action an action that accepts the object as a state 58 | * @param {function} getObjectState function to obtain state of parent namespace 59 | * @param {*} defaultPropertyState initial value of a property 60 | */ 61 | export function dynamicPropertyConfig (action, defaultPropertyState) { 62 | const getPropertyState = (state, key) => action.getState(state)[key] || defaultPropertyState 63 | return { 64 | propertyAction: makeObjectAction(action, defaultPropertyState), 65 | getPropertyState 66 | } 67 | } 68 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # fast-redux - O(1) speed and dynamic importing of actions/reducers 2 | 3 | Fully compatible with Redux but works with O(1) speed when Redux itself works with O(N) speed (N is number of reducers). You can use standard Redux devtools with fast-redux actions. 4 | 5 | When you dispatch an action, Redux invokes all reducers and passes the state and the action to each reducer. 6 | Usually it's not a problem but in complex applications when you have hundreds of reducers and will dispatch an action for every `onChange` of an input field in a form, 7 | you may observe performance issues. fast-redux solves this problem by using actions bound directly to reducers. Using this approach, for every action is executed exactly 8 | one reducer and you don't need to use constants for action types to match actions with reducers. You may see such fast-redux actions using well-known Redux DevTools and use 9 | its time traveling capabilities. [More about performance issues](https://github.com/dogada/fast-redux/issues/1#issuecomment-320465448) that fast-redux aims to solve. 10 | 11 | Plays well with code splitting. You can dynamically import actions/reducers to the store during lifetime of the applications. 12 | 13 | Don't repeat yourself. Constants for action types aren't need (say goodbye to `switch` statements as well). 14 | 15 | 16 | ### Installation 17 | 18 | To install the stable version: 19 | 20 | ``` 21 | npm install --save fast-redux 22 | ``` 23 | 24 | The Redux source code is written in ES2015 but we precompile both CommonJS and UMD builds to ES5 so they work in [any modern browser](http://caniuse.com/#feat=es5). 25 | 26 | 27 | You can use fast-redux together with [React](https://facebook.github.io/react/), or with any other view library. 28 | It is tiny (1kB, including dependencies). 29 | 30 | fast-redux is simpler and IMO better version of [Edux](https://github.com/dogada/edux) (my first attempt to make Redux more developer friendly). 31 | 32 | ### Example 33 | ``` 34 | // examples/async/src/stores/selectedReddit.js 35 | 36 | import {namespaceConfig} from 'fast-redux' 37 | 38 | const DEFAULT_STATE = 'reactjs' 39 | export const { 40 | action, 41 | getState: getSelectedReddit 42 | } = namespaceConfig('selectedReddit', DEFAULT_STATE) 43 | 44 | export const selectReddit = action('selectReddit', 45 | (state, reddit) => reddit 46 | ) 47 | ``` 48 | 49 | Please look at `examples` directory for more complex use cases. 50 | 51 | You can compare 2 versions of same application: [Redux version](https://github.com/reactjs/redux/tree/master/examples/async) and [FastRedux version](https://github.com/dogada/fast-redux/tree/master/examples/async). 52 | 53 | More examples of FastRedux stores you can find in [microchain demo app](https://github.com/dogada/microchain/tree/master/webapp/store). 54 | 55 | ### License 56 | 57 | MIT 58 | 59 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "fast-redux", 3 | "version": "0.7.1", 4 | "description": "DRY version of Redux with O(1) speed and dynamic actions/reducers importing.", 5 | "browser": "dist/fast-redux.js", 6 | "main": "lib/index.js", 7 | "module": "es/index.js", 8 | "jsnext:main": "es/index.js", 9 | "files": [ 10 | "dist", 11 | "lib", 12 | "es", 13 | "src" 14 | ], 15 | "scripts": { 16 | "clean": "rimraf lib dist es coverage", 17 | "lint": "standard", 18 | "lint:src": "standard \"src/**/*.js\"", 19 | "lint:examples": "standard \"examples/**/*.js\"", 20 | "test": "cross-env BABEL_ENV=commonjs jest", 21 | "test:watch": "npm test -- --watch", 22 | "test:cov": "npm test -- --coverage", 23 | "test:examples": "babel-node examples/testAll.js", 24 | "check:src": "npm run lint:src && npm run test", 25 | "check:examples": "npm run build:examples", 26 | "check:build": "check-es3-syntax lib/ dist/ --kill --print", 27 | "build:commonjs": "cross-env BABEL_ENV=commonjs babel src --out-dir lib", 28 | "build:es": "cross-env BABEL_ENV=es babel src --out-dir es", 29 | "build:umd": "cross-env BABEL_ENV=commonjs NODE_ENV=development webpack src/index.js dist/fast-redux.js", 30 | "build:umd:min": "cross-env BABEL_ENV=commonjs NODE_ENV=production webpack src/index.js dist/fast-redux.min.js", 31 | "build:examples": "babel-node examples/buildAll.js", 32 | "build": "npm run build:commonjs && npm run build:es && npm run build:umd && npm run build:umd:min", 33 | "prepare": "npm run clean && npm run check:src && npm run build && npm run check:build" 34 | }, 35 | "author": "Dmytro V. Dogadailo (https://dogada.org)", 36 | "license": "MIT", 37 | "devDependencies": { 38 | "babel-cli": "~6.26.0", 39 | "babel-eslint": "~8.2.1", 40 | "babel-jest": "~22.0.4", 41 | "babel-loader": "~7.1.2", 42 | "babel-plugin-transform-class-properties": "~6.24.1", 43 | "babel-plugin-transform-es3-property-literals": "~6.22.0", 44 | "babel-plugin-transform-object-rest-spread": "~6.26.0", 45 | "babel-preset-es2015": "~6.24.1", 46 | "check-es3-syntax-cli": "^0.2.1", 47 | "cross-env": "^5.1.3", 48 | "jest": "^22.0.5", 49 | "node-libs-browser": "~2.1.0", 50 | "redux": "~3.7.2", 51 | "rimraf": "^2.3.4", 52 | "webpack": "~1.9.6" 53 | }, 54 | "jest": { 55 | "testRegex": "(/test/.*\\.spec.js)$" 56 | }, 57 | "standard": { 58 | "ignore": [ 59 | "dist/", 60 | "build/", 61 | "lib/" 62 | ], 63 | "parser": "babel-eslint" 64 | }, 65 | "dependencies": {}, 66 | "peerDependencies": { 67 | "react": "^15.0.0-0 || ^16.0.0-0", 68 | "redux": "^2.0.0 || ^3.0.0" 69 | }, 70 | "repository": { 71 | "type": "git", 72 | "url": "https://github.com/dogada/fast-redux.git" 73 | }, 74 | "keywords": [ 75 | "redux", 76 | "reducer", 77 | "state", 78 | "flux", 79 | "react", 80 | "dry" 81 | ] 82 | } 83 | -------------------------------------------------------------------------------- /examples/async/src/containers/App.js: -------------------------------------------------------------------------------- 1 | import React, { Component } from 'react' 2 | import PropTypes from 'proptypes' 3 | 4 | import { connect } from 'react-redux' 5 | import { 6 | fetchPostsIfNeed, 7 | invalidateReddit, 8 | getRedditState 9 | } from '../stores/postsByReddit' 10 | import { 11 | selectReddit, 12 | getSelectedReddit 13 | } from '../stores/selectedReddit' 14 | 15 | import Picker from '../components/Picker' 16 | import Posts from '../components/Posts' 17 | 18 | class App extends Component { 19 | // eslint-disable-line 20 | static propTypes = { // eslint-disable-line 21 | selectedReddit: PropTypes.string.isRequired, 22 | posts: PropTypes.array.isRequired, 23 | isFetching: PropTypes.bool.isRequired, 24 | lastUpdated: PropTypes.number, 25 | dispatch: PropTypes.func.isRequired 26 | } 27 | 28 | componentDidMount () { 29 | const { dispatch, selectedReddit } = this.props 30 | dispatch(fetchPostsIfNeed(selectedReddit)) 31 | } 32 | 33 | componentWillReceiveProps (nextProps) { 34 | if (nextProps.selectedReddit !== this.props.selectedReddit) { 35 | const { dispatch, selectedReddit } = nextProps 36 | dispatch(fetchPostsIfNeed(selectedReddit)) 37 | } 38 | } 39 | 40 | handleChange = nextReddit => { 41 | this.props.dispatch(selectReddit(nextReddit)) 42 | } 43 | 44 | handleRefreshClick = e => { 45 | e.preventDefault() 46 | 47 | const { dispatch, selectedReddit } = this.props 48 | dispatch(invalidateReddit(selectedReddit)) 49 | dispatch(fetchPostsIfNeed(selectedReddit)) 50 | } 51 | 52 | render () { 53 | const { selectedReddit, posts, isFetching, lastUpdated } = this.props 54 | const isEmpty = posts.length === 0 55 | return ( 56 |
57 | 60 |

61 | {lastUpdated && 62 | 63 | Last updated at {new Date(lastUpdated).toLocaleTimeString()}. 64 | {' '} 65 | 66 | } 67 | {!isFetching && 68 | 70 | Refresh 71 | 72 | } 73 |

74 | {isEmpty 75 | ? (isFetching ?

Loading...

:

Empty.

) 76 | :
77 | 78 |
79 | } 80 |
81 | ) 82 | } 83 | } 84 | 85 | const mapStateToProps = state => { 86 | const selectedReddit = getSelectedReddit(state) 87 | const { 88 | isFetching, 89 | lastUpdated, 90 | items: posts 91 | } = getRedditState(state, selectedReddit) 92 | 93 | return { 94 | selectedReddit, 95 | posts, 96 | isFetching, 97 | lastUpdated 98 | } 99 | } 100 | 101 | export default connect(mapStateToProps)(App) 102 | -------------------------------------------------------------------------------- /lib/object.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | 3 | Object.defineProperty(exports, "__esModule", { 4 | value: true 5 | }); 6 | 7 | var _extends = Object.assign || function (target) { for (var i = 1; i < arguments.length; i++) { var source = arguments[i]; for (var key in source) { if (Object.prototype.hasOwnProperty.call(source, key)) { target[key] = source[key]; } } } return target; }; 8 | 9 | exports.staticPropertyConfig = staticPropertyConfig; 10 | exports.dynamicPropertyConfig = dynamicPropertyConfig; 11 | 12 | function _defineProperty(obj, key, value) { if (key in obj) { Object.defineProperty(obj, key, { value: value, enumerable: true, configurable: true, writable: true }); } else { obj[key] = value; } return obj; } 13 | 14 | /** 15 | * Actions for working directly with the properties of an object stored in a 16 | * state. For example object 'posts' that holds various reddits states. 17 | * You can find example of usage in: 18 | * examples/async/src/actions/postsByReddit.js 19 | */ 20 | 21 | var makePropertyAction = function makePropertyAction(action, propertyName) { 22 | return function (name, reducer) { 23 | return action(name, function (state) { 24 | for (var _len = arguments.length, args = Array(_len > 1 ? _len - 1 : 0), _key = 1; _key < _len; _key++) { 25 | args[_key - 1] = arguments[_key]; 26 | } 27 | 28 | return _extends({}, state, _defineProperty({}, propertyName, reducer.apply(undefined, [state[propertyName]].concat(args)))); 29 | }); 30 | }; 31 | }; 32 | 33 | /** 34 | * Return utility functions to work directly with a single property of a parent object. 35 | * Property name is provided on config stage and can't be changed during action call. 36 | * Default value for the property shoult be set in a namespaceConfig 37 | * @param {function} action an action that accepts the object as a state 38 | * @param {function} getObjectState function to obtain state of parent namespace 39 | * @param {string} propertyName name of object's property 40 | * @param {*} defaultPropertyState initial value of a property 41 | */ 42 | function staticPropertyConfig(action, propertyName) { 43 | var getPropertyState = function getPropertyState(state) { 44 | return action.getState(state)[propertyName]; 45 | }; 46 | return { 47 | propertyAction: makePropertyAction(action, propertyName), 48 | getPropertyState: getPropertyState 49 | }; 50 | } 51 | 52 | /** 53 | * Actions for working directly with the properties of an object stored in a 54 | * state. For example object 'posts' that holds various reddits states. 55 | * You can find example of usage in: 56 | * examples/async/src/actions/postsByReddit.js 57 | */ 58 | 59 | var makeObjectAction = function makeObjectAction(action, defaultPropertyState) { 60 | return function (name, reducer) { 61 | return action(name, function (state, key) { 62 | for (var _len2 = arguments.length, args = Array(_len2 > 2 ? _len2 - 2 : 0), _key2 = 2; _key2 < _len2; _key2++) { 63 | args[_key2 - 2] = arguments[_key2]; 64 | } 65 | 66 | var nestedState = state[key] || defaultPropertyState; 67 | return _extends({}, state, _defineProperty({}, key, reducer.apply(undefined, [nestedState].concat(args)))); 68 | }); 69 | }; 70 | }; 71 | 72 | /** 73 | * Return utility functions to work directly with properties of a parent object. 74 | * Property name is provided dynamically as first argument of action. 75 | * @param {function} action an action that accepts the object as a state 76 | * @param {function} getObjectState function to obtain state of parent namespace 77 | * @param {*} defaultPropertyState initial value of a property 78 | */ 79 | function dynamicPropertyConfig(action, defaultPropertyState) { 80 | var getPropertyState = function getPropertyState(state, key) { 81 | return action.getState(state)[key] || defaultPropertyState; 82 | }; 83 | return { 84 | propertyAction: makeObjectAction(action, defaultPropertyState), 85 | getPropertyState: getPropertyState 86 | }; 87 | } --------------------------------------------------------------------------------