├── 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 |
6 | {posts.map((post, i) =>
7 | - {post.title}
8 | )}
9 |
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 | :
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 | }
--------------------------------------------------------------------------------