From e426039152272393f80d7f3b9e63814890a4119d Mon Sep 17 00:00:00 2001 From: Dan Abramov Date: Tue, 30 Jun 2015 23:10:53 +0200 Subject: [PATCH 01/84] Breaking API changes for 1.0 MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Naming: * “Stateless Stores” are now called reducers. (https://github.com/gaearon/redux/issues/137#issuecomment-114178411) * The “Redux instance” is now called “The Store”. (https://github.com/gaearon/redux/issues/137#issuecomment-113252359) * The dispatcher is removed completely. (https://github.com/gaearon/redux/pull/166#issue-90113962) API changes: * `composeStores` is now `composeReducers`. * `createDispatcher` is gone. * `createRedux` is now `createStore`. * `` now accepts `store` prop instead of `redux`. * The new `createStore` signature is `createStore(reducer: Function | Object, initialState: any, middlewares: Array | ({ getState, dispatch }) => Array)`. * If the first argument to `createStore` is an object, `composeReducers` is automatically applied to it. * The “smart” middleware signature changed. It now accepts an object instead of a single `getState` function. The `dispatch` function lets you “recurse” the middleware chain and is useful for async: https://github.com/gaearon/redux/issues/113#issuecomment-112603386. Correctness changes: * The `dispatch` provided by the default thunk middleware now walks the whole middleware chain. * It is enforced now that raw Actions at the end of the middleware chain have to be plain objects. * Nested dispatches are now handled gracefully. (https://github.com/gaearon/redux/pull/110) Internal changes: * The object in React context is renamed from `redux` to `store`. * Some tests are rewritten for clarity, focus and edge cases. * Redux in examples is now aliased to the source code for easier work on master. --- examples/counter/containers/App.js | 8 +- examples/counter/containers/CounterApp.js | 2 +- .../counter/{stores => reducers}/counter.js | 0 .../counter/{stores => reducers}/index.js | 0 examples/counter/webpack.config.js | 3 + examples/todomvc/containers/App.js | 11 +- examples/todomvc/containers/TodoApp.js | 2 +- .../todomvc/{stores => reducers}/index.js | 0 .../todomvc/{stores => reducers}/todos.js | 0 examples/todomvc/webpack.config.js | 3 + src/Redux.js | 53 ----- src/Store.js | 50 +++++ src/components/createConnector.js | 12 +- src/components/createProvideDecorator.js | 4 +- src/components/createProvider.js | 28 ++- src/createDispatcher.js | 26 --- src/createRedux.js | 13 -- src/createStore.js | 45 ++++ src/index.js | 10 +- src/middleware/thunk.js | 14 +- src/utils/composeReducers.js | 12 + src/utils/composeStores.js | 11 - ...reateReduxShape.js => createStoreShape.js} | 2 +- test/Store.spec.js | 209 ++++++++++++++++++ test/_helpers.js | 32 --- test/components/Connector.spec.js | 128 +++++++---- test/components/Provider.spec.js | 35 +-- test/components/connect.spec.js | 64 ++++-- test/components/provide.spec.js | 33 +-- test/composeMiddleware.spec.js | 2 +- test/composeReducers.spec.js | 33 +++ test/composeStores.spec.js | 31 --- test/createDispatcher.spec.js | 45 ---- test/createRedux.spec.js | 72 ------ test/createStore.spec.js | 148 +++++++++++++ test/getDisplayName.spec.js | 10 +- test/helpers/actionCreators.js | 20 ++ test/helpers/actionTypes.js | 1 + test/helpers/reducers.js | 25 +++ test/utils/bindActionCreators.spec.js | 44 ++-- test/utils/identity.spec.js | 4 +- test/utils/mapValues.spec.js | 10 +- test/utils/pick.spec.js | 11 +- test/utils/shallowEquality.spec.js | 25 +-- 44 files changed, 816 insertions(+), 475 deletions(-) rename examples/counter/{stores => reducers}/counter.js (100%) rename examples/counter/{stores => reducers}/index.js (100%) rename examples/todomvc/{stores => reducers}/index.js (100%) rename examples/todomvc/{stores => reducers}/todos.js (100%) delete mode 100644 src/Redux.js create mode 100644 src/Store.js delete mode 100644 src/createDispatcher.js delete mode 100644 src/createRedux.js create mode 100644 src/createStore.js create mode 100644 src/utils/composeReducers.js delete mode 100644 src/utils/composeStores.js rename src/utils/{createReduxShape.js => createStoreShape.js} (74%) create mode 100644 test/Store.spec.js delete mode 100644 test/_helpers.js create mode 100644 test/composeReducers.spec.js delete mode 100644 test/composeStores.spec.js delete mode 100644 test/createDispatcher.spec.js delete mode 100644 test/createRedux.spec.js create mode 100644 test/createStore.spec.js create mode 100644 test/helpers/actionCreators.js create mode 100644 test/helpers/actionTypes.js create mode 100644 test/helpers/reducers.js diff --git a/examples/counter/containers/App.js b/examples/counter/containers/App.js index bf9195304b..3acf7b9a1f 100644 --- a/examples/counter/containers/App.js +++ b/examples/counter/containers/App.js @@ -1,15 +1,15 @@ import React from 'react'; import CounterApp from './CounterApp'; -import { createRedux } from 'redux'; +import { createStore } from 'redux/index'; import { Provider } from 'redux/react'; -import * as stores from '../stores'; +import * as reducers from '../reducers'; -const redux = createRedux(stores); +const store = createStore(reducers); export default class App { render() { return ( - + {() => } ); diff --git a/examples/counter/containers/CounterApp.js b/examples/counter/containers/CounterApp.js index f60d74c746..e6c90482e7 100644 --- a/examples/counter/containers/CounterApp.js +++ b/examples/counter/containers/CounterApp.js @@ -1,5 +1,5 @@ import React from 'react'; -import { bindActionCreators } from 'redux'; +import { bindActionCreators } from 'redux/index'; import { connect } from 'redux/react'; import Counter from '../components/Counter'; import * as CounterActions from '../actions/CounterActions'; diff --git a/examples/counter/stores/counter.js b/examples/counter/reducers/counter.js similarity index 100% rename from examples/counter/stores/counter.js rename to examples/counter/reducers/counter.js diff --git a/examples/counter/stores/index.js b/examples/counter/reducers/index.js similarity index 100% rename from examples/counter/stores/index.js rename to examples/counter/reducers/index.js diff --git a/examples/counter/webpack.config.js b/examples/counter/webpack.config.js index 032d620fd2..33fb9a3291 100644 --- a/examples/counter/webpack.config.js +++ b/examples/counter/webpack.config.js @@ -18,6 +18,9 @@ module.exports = { new webpack.NoErrorsPlugin() ], resolve: { + alias: { + 'redux': path.join(__dirname, '../../src') + }, extensions: ['', '.js'] }, module: { diff --git a/examples/todomvc/containers/App.js b/examples/todomvc/containers/App.js index 253f3260e1..2563bf2669 100644 --- a/examples/todomvc/containers/App.js +++ b/examples/todomvc/containers/App.js @@ -1,16 +1,17 @@ import React from 'react'; import TodoApp from './TodoApp'; -import { createRedux } from 'redux'; +import { createStore, composeReducers } from 'redux/index'; import { Provider } from 'redux/react'; -import * as stores from '../stores'; +import * as reducers from '../reducers'; -const redux = createRedux(stores); +const reducer = composeReducers(reducers); +const store = createStore(reducer); export default class App { render() { return ( - - {() => } + + {() => } ); } diff --git a/examples/todomvc/containers/TodoApp.js b/examples/todomvc/containers/TodoApp.js index 03df600025..348d51441a 100644 --- a/examples/todomvc/containers/TodoApp.js +++ b/examples/todomvc/containers/TodoApp.js @@ -1,5 +1,5 @@ import React from 'react'; -import { bindActionCreators } from 'redux'; +import { bindActionCreators } from 'redux/index'; import { Connector } from 'redux/react'; import Header from '../components/Header'; import MainSection from '../components/MainSection'; diff --git a/examples/todomvc/stores/index.js b/examples/todomvc/reducers/index.js similarity index 100% rename from examples/todomvc/stores/index.js rename to examples/todomvc/reducers/index.js diff --git a/examples/todomvc/stores/todos.js b/examples/todomvc/reducers/todos.js similarity index 100% rename from examples/todomvc/stores/todos.js rename to examples/todomvc/reducers/todos.js diff --git a/examples/todomvc/webpack.config.js b/examples/todomvc/webpack.config.js index 679803efdf..e1e16c0468 100644 --- a/examples/todomvc/webpack.config.js +++ b/examples/todomvc/webpack.config.js @@ -18,6 +18,9 @@ module.exports = { new webpack.NoErrorsPlugin() ], resolve: { + alias: { + 'redux': path.join(__dirname, '../../src') + }, extensions: ['', '.js'] }, module: { diff --git a/src/Redux.js b/src/Redux.js deleted file mode 100644 index 860da8746b..0000000000 --- a/src/Redux.js +++ /dev/null @@ -1,53 +0,0 @@ -import createDispatcher from './createDispatcher'; -import composeStores from './utils/composeStores'; -import thunkMiddleware from './middleware/thunk'; - -export default class Redux { - constructor(dispatcherOrStores, initialState) { - let finalDispatcher = dispatcherOrStores; - if (typeof dispatcherOrStores === 'object') { - // A shortcut notation to use the default dispatcher - finalDispatcher = createDispatcher( - composeStores(dispatcherOrStores), - (getState) => [thunkMiddleware(getState)] - ); - } - - this.state = initialState; - this.listeners = []; - this.replaceDispatcher(finalDispatcher); - } - - getDispatcher() { - return this.dispatcher; - } - - replaceDispatcher(nextDispatcher) { - this.dispatcher = nextDispatcher; - this.dispatchFn = nextDispatcher(this.state, ::this.setState); - } - - dispatch(action) { - return this.dispatchFn(action); - } - - getState() { - return this.state; - } - - setState(nextState) { - this.state = nextState; - this.listeners.forEach(listener => listener()); - return nextState; - } - - subscribe(listener) { - const { listeners } = this; - listeners.push(listener); - - return function unsubscribe() { - const index = listeners.indexOf(listener); - listeners.splice(index, 1); - }; - } -} diff --git a/src/Store.js b/src/Store.js new file mode 100644 index 0000000000..ed85b987fc --- /dev/null +++ b/src/Store.js @@ -0,0 +1,50 @@ +import invariant from 'invariant'; +import isPlainObject from './utils/isPlainObject'; + +export default class Store { + constructor(reducer, initialState) { + invariant( + typeof reducer === 'function', + 'Expected the reducer to be a function.' + ); + + this.state = initialState; + this.listeners = []; + this.replaceReducer(reducer); + } + + getReducer() { + return this.reducer; + } + + replaceReducer(nextReducer) { + this.reducer = nextReducer; + this.dispatch({ type: '@@INIT' }); + } + + dispatch(action) { + invariant( + isPlainObject(action), + 'Actions must be plain objects. Use custom middleware for async actions.' + ); + + const { reducer } = this; + this.state = reducer(this.state, action); + this.listeners.forEach(listener => listener()); + return action; + } + + getState() { + return this.state; + } + + subscribe(listener) { + const { listeners } = this; + listeners.push(listener); + + return function unsubscribe() { + const index = listeners.indexOf(listener); + listeners.splice(index, 1); + }; + } +} diff --git a/src/components/createConnector.js b/src/components/createConnector.js index 58d097ee24..153130000d 100644 --- a/src/components/createConnector.js +++ b/src/components/createConnector.js @@ -1,4 +1,4 @@ -import createReduxShape from '../utils/createReduxShape'; +import createStoreShape from '../utils/createStoreShape'; import identity from '../utils/identity'; import shallowEqual from '../utils/shallowEqual'; import isPlainObject from '../utils/isPlainObject'; @@ -6,10 +6,11 @@ import invariant from 'invariant'; export default function createConnector(React) { const { Component, PropTypes } = React; + const storeShape = createStoreShape(PropTypes); return class Connector extends Component { static contextTypes = { - redux: createReduxShape(PropTypes).isRequired + store: storeShape.isRequired }; static propTypes = { @@ -38,12 +39,11 @@ export default function createConnector(React) { constructor(props, context) { super(props, context); - this.state = this.selectState(props, context); } componentDidMount() { - this.unsubscribe = this.context.redux.subscribe(::this.handleChange); + this.unsubscribe = this.context.store.subscribe(::this.handleChange); } componentWillReceiveProps(nextProps) { @@ -63,7 +63,7 @@ export default function createConnector(React) { } selectState(props, context) { - const state = context.redux.getState(); + const state = context.store.getState(); const slice = props.select(state); invariant( @@ -78,7 +78,7 @@ export default function createConnector(React) { render() { const { children } = this.props; const { slice } = this.state; - const { redux: { dispatch } } = this.context; + const { store: { dispatch } } = this.context; return children({ dispatch, ...slice }); } diff --git a/src/components/createProvideDecorator.js b/src/components/createProvideDecorator.js index 5c1784513c..d181865a40 100644 --- a/src/components/createProvideDecorator.js +++ b/src/components/createProvideDecorator.js @@ -3,14 +3,14 @@ import getDisplayName from '../utils/getDisplayName'; export default function createProvideDecorator(React, Provider) { const { Component } = React; - return function provide(redux) { + return function provide(store) { return DecoratedComponent => class ProviderDecorator extends Component { static displayName = `Provider(${getDisplayName(DecoratedComponent)})`; static DecoratedComponent = DecoratedComponent; render() { return ( - + {() => } ); diff --git a/src/components/createProvider.js b/src/components/createProvider.js index cf988df40a..030c8e2ac7 100644 --- a/src/components/createProvider.js +++ b/src/components/createProvider.js @@ -1,36 +1,34 @@ -import createReduxShape from '../utils/createReduxShape'; +import createStoreShape from '../utils/createStoreShape'; export default function createProvider(React) { const { Component, PropTypes } = React; - - const reduxShapeIsRequired = createReduxShape(PropTypes).isRequired; + const storeShape = createStoreShape(PropTypes); return class Provider extends Component { - static propTypes = { - redux: reduxShapeIsRequired, - children: PropTypes.func.isRequired + static childContextTypes = { + store: storeShape.isRequired }; - static childContextTypes = { - redux: reduxShapeIsRequired + static propTypes = { + children: PropTypes.func.isRequired }; getChildContext() { - return { redux: this.state.redux }; + return { store: this.state.store }; } constructor(props, context) { super(props, context); - this.state = { redux: props.redux }; + this.state = { store: props.store }; } componentWillReceiveProps(nextProps) { - const { redux } = this.state; - const { redux: nextRedux } = nextProps; + const { store } = this.state; + const { store: nextStore } = nextProps; - if (redux !== nextRedux) { - const nextDispatcher = nextRedux.getDispatcher(); - redux.replaceDispatcher(nextDispatcher); + if (store !== nextStore) { + const nextReducer = nextStore.getReducer(); + store.replaceReducer(nextReducer); } } diff --git a/src/createDispatcher.js b/src/createDispatcher.js deleted file mode 100644 index 029b698697..0000000000 --- a/src/createDispatcher.js +++ /dev/null @@ -1,26 +0,0 @@ -import composeMiddleware from './utils/composeMiddleware'; - -const INIT_ACTION = { - type: '@@INIT' -}; - -export default function createDispatcher(store, middlewares = []) { - return function dispatcher(initialState, setState) { - let state = setState(store(initialState, INIT_ACTION)); - - function dispatch(action) { - state = setState(store(state, action)); - return action; - } - - function getState() { - return state; - } - - const finalMiddlewares = typeof middlewares === 'function' ? - middlewares(getState) : - middlewares; - - return composeMiddleware(...finalMiddlewares, dispatch); - }; -} diff --git a/src/createRedux.js b/src/createRedux.js deleted file mode 100644 index f3ee7c1347..0000000000 --- a/src/createRedux.js +++ /dev/null @@ -1,13 +0,0 @@ -import Redux from './Redux'; - -export default function createRedux(...args) { - const redux = new Redux(...args); - - return { - subscribe: ::redux.subscribe, - dispatch: ::redux.dispatch, - getState: ::redux.getState, - getDispatcher: ::redux.getDispatcher, - replaceDispatcher: ::redux.replaceDispatcher - }; -} diff --git a/src/createStore.js b/src/createStore.js new file mode 100644 index 0000000000..a06c60cf26 --- /dev/null +++ b/src/createStore.js @@ -0,0 +1,45 @@ +import Store from './Store'; +import composeReducers from './utils/composeReducers'; +import composeMiddleware from './utils/composeMiddleware'; +import thunkMiddleware from './middleware/thunk'; + +const defaultMiddlewares = ({ dispatch, getState }) => [ + thunkMiddleware({ dispatch, getState }) +]; + +export default function createStore( + reducer, + initialState, + middlewares = defaultMiddlewares +) { + const finalReducer = typeof reducer === 'function' ? + reducer : + composeReducers(reducer); + + const store = new Store(finalReducer, initialState); + const getState = ::store.getState; + + const rawDispatch = ::store.dispatch; + let cookedDispatch = null; + + function dispatch(action) { + return cookedDispatch(action); + } + + const finalMiddlewares = typeof middlewares === 'function' ? + middlewares({ dispatch, getState }) : + middlewares; + + cookedDispatch = composeMiddleware( + ...finalMiddlewares, + rawDispatch + ); + + return { + dispatch: cookedDispatch, + subscribe: ::store.subscribe, + getState: ::store.getState, + getReducer: ::store.getReducer, + replaceReducer: ::store.replaceReducer + }; +} diff --git a/src/index.js b/src/index.js index 607b83f317..6ba367c91c 100644 --- a/src/index.js +++ b/src/index.js @@ -1,16 +1,14 @@ // Core -import createRedux from './createRedux'; -import createDispatcher from './createDispatcher'; +import createStore from './createStore'; // Utilities import composeMiddleware from './utils/composeMiddleware'; -import composeStores from './utils/composeStores'; +import composeReducers from './utils/composeReducers'; import bindActionCreators from './utils/bindActionCreators'; export { - createRedux, - createDispatcher, + createStore, composeMiddleware, - composeStores, + composeReducers, bindActionCreators }; diff --git a/src/middleware/thunk.js b/src/middleware/thunk.js index a80a78159c..14e92df5c2 100644 --- a/src/middleware/thunk.js +++ b/src/middleware/thunk.js @@ -1,10 +1,6 @@ -export default function thunkMiddleware(getState) { - return (next) => { - const recurse = (action) => - typeof action === 'function' ? - action(recurse, getState) : - next(action); - - return recurse; - }; +export default function thunkMiddleware({ dispatch, getState }) { + return (next) => (action) => + typeof action === 'function' ? + action(dispatch, getState) : + next(action); } diff --git a/src/utils/composeReducers.js b/src/utils/composeReducers.js new file mode 100644 index 0000000000..e93887fabf --- /dev/null +++ b/src/utils/composeReducers.js @@ -0,0 +1,12 @@ +import mapValues from '../utils/mapValues'; +import pick from '../utils/pick'; + +export default function composeReducers(reducers) { + const finalReducers = pick(reducers, (val) => typeof val === 'function'); + + return function Composition(atom = {}, action) { + return mapValues(finalReducers, (store, key) => + store(atom[key], action) + ); + }; +} diff --git a/src/utils/composeStores.js b/src/utils/composeStores.js deleted file mode 100644 index d8c4420546..0000000000 --- a/src/utils/composeStores.js +++ /dev/null @@ -1,11 +0,0 @@ -import mapValues from '../utils/mapValues'; -import pick from '../utils/pick'; - -export default function composeStores(stores) { - const finalStores = pick(stores, (val) => typeof val === 'function'); - return function Composition(atom = {}, action) { - return mapValues(finalStores, (store, key) => - store(atom[key], action) - ); - }; -} diff --git a/src/utils/createReduxShape.js b/src/utils/createStoreShape.js similarity index 74% rename from src/utils/createReduxShape.js rename to src/utils/createStoreShape.js index e3795bedd2..851e7ce898 100644 --- a/src/utils/createReduxShape.js +++ b/src/utils/createStoreShape.js @@ -1,4 +1,4 @@ -export default function createReduxShape(PropTypes) { +export default function createStoreShape(PropTypes) { return PropTypes.shape({ subscribe: PropTypes.func.isRequired, dispatch: PropTypes.func.isRequired, diff --git a/test/Store.spec.js b/test/Store.spec.js new file mode 100644 index 0000000000..e1834db84e --- /dev/null +++ b/test/Store.spec.js @@ -0,0 +1,209 @@ +import expect from 'expect'; +import Store from '../src/Store'; +import { todos, todosReverse } from './helpers/reducers'; +import { addTodo } from './helpers/actionCreators'; + +describe('Store', () => { + it('should require a reducer function', () => { + expect(() => + new Store() + ).toThrow(); + + expect(() => + new Store('test') + ).toThrow(); + + expect(() => + new Store({}) + ).toThrow(); + + expect(() => + new Store(() => {}) + ).toNotThrow(); + }); + + it('should apply the reducer to the previous state', () => { + const store = new Store(todos); + expect(store.getState()).toEqual([]); + + store.dispatch({}); + expect(store.getState()).toEqual([]); + + store.dispatch(addTodo('Hello')); + expect(store.getState()).toEqual([{ + id: 1, + text: 'Hello' + }]); + + store.dispatch(addTodo('World')); + expect(store.getState()).toEqual([{ + id: 1, + text: 'Hello' + }, { + id: 2, + text: 'World' + }]); + }); + + it('should apply the reducer to the initial state', () => { + const store = new Store(todos, [{ + id: 1, + text: 'Hello' + }]); + expect(store.getState()).toEqual([{ + id: 1, + text: 'Hello' + }]); + + store.dispatch({}); + expect(store.getState()).toEqual([{ + id: 1, + text: 'Hello' + }]); + + store.dispatch(addTodo('World')); + expect(store.getState()).toEqual([{ + id: 1, + text: 'Hello' + }, { + id: 2, + text: 'World' + }]); + }); + + it('should preserve the state when replacing a reducer', () => { + const store = new Store(todos); + store.dispatch(addTodo('Hello')); + store.dispatch(addTodo('World')); + expect(store.getState()).toEqual([{ + id: 1, + text: 'Hello' + }, { + id: 2, + text: 'World' + }]); + + let nextStore = new Store(todosReverse); + store.replaceReducer(nextStore.getReducer()); + expect(store.getState()).toEqual([{ + id: 1, + text: 'Hello' + }, { + id: 2, + text: 'World' + }]); + + store.dispatch(addTodo('Perhaps')); + expect(store.getState()).toEqual([{ + id: 2, + text: 'Perhaps' + }, { + id: 1, + text: 'Hello' + }, { + id: 2, + text: 'World' + }]); + + nextStore = new Store(todos); + store.replaceReducer(nextStore.getReducer()); + expect(store.getState()).toEqual([{ + id: 2, + text: 'Perhaps' + }, { + id: 1, + text: 'Hello' + }, { + id: 2, + text: 'World' + }]); + + store.dispatch(addTodo('Surely')); + expect(store.getState()).toEqual([{ + id: 2, + text: 'Perhaps' + }, { + id: 1, + text: 'Hello' + }, { + id: 2, + text: 'World' + }, { + id: 3, + text: 'Surely' + }]); + }); + + it('should support multiple subscriptions', () => { + const store = new Store(todos); + const listenerA = expect.createSpy(() => {}); + const listenerB = expect.createSpy(() => {}); + + let unsubscribeA = store.subscribe(listenerA); + store.dispatch({}); + expect(listenerA.calls.length).toBe(1); + expect(listenerB.calls.length).toBe(0); + + store.dispatch({}); + expect(listenerA.calls.length).toBe(2); + expect(listenerB.calls.length).toBe(0); + + const unsubscribeB = store.subscribe(listenerB); + expect(listenerA.calls.length).toBe(2); + expect(listenerB.calls.length).toBe(0); + + store.dispatch({}); + expect(listenerA.calls.length).toBe(3); + expect(listenerB.calls.length).toBe(1); + + unsubscribeA(); + expect(listenerA.calls.length).toBe(3); + expect(listenerB.calls.length).toBe(1); + + store.dispatch({}); + expect(listenerA.calls.length).toBe(3); + expect(listenerB.calls.length).toBe(2); + + unsubscribeB(); + expect(listenerA.calls.length).toBe(3); + expect(listenerB.calls.length).toBe(2); + + store.dispatch({}); + expect(listenerA.calls.length).toBe(3); + expect(listenerB.calls.length).toBe(2); + + unsubscribeA = store.subscribe(listenerA); + expect(listenerA.calls.length).toBe(3); + expect(listenerB.calls.length).toBe(2); + + store.dispatch({}); + expect(listenerA.calls.length).toBe(4); + expect(listenerB.calls.length).toBe(2); + }); + + it('should provide an up-to-date state when a subscriber is notified', done => { + const store = new Store(todos); + store.subscribe(() => { + expect(store.getState()).toEqual([{ + id: 1, + text: 'Hello' + }]); + done(); + }); + store.dispatch(addTodo('Hello')); + }); + + it('should only accept plain object actions', () => { + const store = new Store(todos); + expect(() => + store.dispatch({}) + ).toNotThrow(); + + function AwesomeMap() { } + [null, undefined, 42, 'hey', new AwesomeMap()].forEach(nonObject => + expect(() => + store.dispatch(nonObject) + ).toThrow(/plain/) + ); + }); +}); diff --git a/test/_helpers.js b/test/_helpers.js deleted file mode 100644 index f525518ffe..0000000000 --- a/test/_helpers.js +++ /dev/null @@ -1,32 +0,0 @@ -const ADD_TODO = 'ADD_TODO'; -const ADD_TODO_ASYNC = 'ADD_TODO_ASYNC'; - -export const initialState = []; -export const defaultText = 'Hello World!'; -export const constants = { ADD_TODO, ADD_TODO_ASYNC }; - -export function todoStore(state = initialState, action) { - const { type } = action; - if (type === ADD_TODO || type === ADD_TODO_ASYNC) { - return [{ - id: state[0] ? state[0].id + 1 : 1, - text: action.text - }, ...state]; - } - return state; -} - -export const todoActions = { - addTodo(text) { - return { type: ADD_TODO, text }; - }, - - addTodoAsync(text, cb/* for testing only */) { - return dispatch => { - setImmediate(() => { - dispatch({ type: ADD_TODO_ASYNC, text }); - cb(); - }); - }; - } -}; diff --git a/test/components/Connector.spec.js b/test/components/Connector.spec.js index d9202a8c5c..27d968ea1a 100644 --- a/test/components/Connector.spec.js +++ b/test/components/Connector.spec.js @@ -1,7 +1,7 @@ import expect from 'expect'; import jsdomReact from './jsdomReact'; import React, { PropTypes, Component } from 'react/addons'; -import { createRedux } from '../../src'; +import { createStore } from '../../src'; import { Connector } from '../../src/react'; const { TestUtils } = React.addons; @@ -13,11 +13,11 @@ describe('React', () => { // Mock minimal Provider interface class Provider extends Component { static childContextTypes = { - redux: PropTypes.object.isRequired + store: PropTypes.object.isRequired } getChildContext() { - return { redux: this.props.redux }; + return { store: this.props.store }; } render() { @@ -25,17 +25,17 @@ describe('React', () => { } } - const stringBuilder = (prev = '', action) => { + function stringBuilder(prev = '', action) { return action.type === 'APPEND' ? prev + action.body : prev; - }; + } - it('gets Redux from context', () => { - const redux = createRedux({ test: () => 'test' }); + it('should receive the store in the context', () => { + const store = createStore({}); const tree = TestUtils.renderIntoDocument( - + {() => ( {() =>
} @@ -45,16 +45,16 @@ describe('React', () => { ); const connector = TestUtils.findRenderedComponentWithType(tree, Connector); - expect(connector.context.redux).toBe(redux); + expect(connector.context.store).toBe(store); }); - it('subscribes to Redux changes', () => { - const redux = createRedux({ string: stringBuilder }); + it('should subscribe to the store changes', () => { + const store = createStore(stringBuilder); const tree = TestUtils.renderIntoDocument( - + {() => ( - ({ string: state.string })}> + ({ string })}> {({ string }) =>
} )} @@ -63,19 +63,19 @@ describe('React', () => { const div = TestUtils.findRenderedDOMComponentWithTag(tree, 'div'); expect(div.props.string).toBe(''); - redux.dispatch({ type: 'APPEND', body: 'a'}); + store.dispatch({ type: 'APPEND', body: 'a'}); expect(div.props.string).toBe('a'); - redux.dispatch({ type: 'APPEND', body: 'b'}); + store.dispatch({ type: 'APPEND', body: 'b'}); expect(div.props.string).toBe('ab'); }); - it('unsubscribes before unmounting', () => { - const redux = createRedux({ test: () => 'test' }); - const subscribe = redux.subscribe; + it('should unsubscribe before unmounting', () => { + const store = createStore(stringBuilder); + const subscribe = store.subscribe; - // Keep track of unsubscribe by wrapping `subscribe()` + // Keep track of unsubscribe by wrapping subscribe() const spy = expect.createSpy(() => {}); - redux.subscribe = (listener) => { + store.subscribe = (listener) => { const unsubscribe = subscribe(listener); return () => { spy(); @@ -84,9 +84,9 @@ describe('React', () => { }; const tree = TestUtils.renderIntoDocument( - + {() => ( - ({ string: state.string })}> + ({ string })}> {({ string }) =>
} )} @@ -99,8 +99,8 @@ describe('React', () => { expect(spy.calls.length).toBe(1); }); - it('shallow compares selected state to prevent unnecessary updates', () => { - const redux = createRedux({ string: stringBuilder }); + it('should shallowly compare the selected state to prevent unnecessary updates', () => { + const store = createStore(stringBuilder); const spy = expect.createSpy(() => {}); function render({ string }) { spy(); @@ -108,9 +108,9 @@ describe('React', () => { } const tree = TestUtils.renderIntoDocument( - + {() => ( - ({ string: state.string })}> + ({ string })}> {render} )} @@ -120,16 +120,19 @@ describe('React', () => { const div = TestUtils.findRenderedDOMComponentWithTag(tree, 'div'); expect(spy.calls.length).toBe(1); expect(div.props.string).toBe(''); - redux.dispatch({ type: 'APPEND', body: 'a'}); + store.dispatch({ type: 'APPEND', body: 'a'}); expect(spy.calls.length).toBe(2); - redux.dispatch({ type: 'APPEND', body: 'b'}); + store.dispatch({ type: 'APPEND', body: 'b'}); expect(spy.calls.length).toBe(3); - redux.dispatch({ type: 'APPEND', body: ''}); + store.dispatch({ type: 'APPEND', body: ''}); expect(spy.calls.length).toBe(3); }); - it('recomputes the state slice when `select` prop changes', () => { - const redux = createRedux({ a: () => 42, b: () => 72 }); + it('should recompute the state slice when the select prop changes', () => { + const store = createStore({ + a: () => 42, + b: () => 72 + }); function selectA(state) { return { result: state.a }; @@ -151,7 +154,7 @@ describe('React', () => { render() { return ( - + {() => {render} @@ -170,11 +173,11 @@ describe('React', () => { expect(div.props.children).toBe(72); }); - it('passes `dispatch()` to child function', () => { - const redux = createRedux({ test: () => 'test' }); + it('should pass dispatch() to the child function', () => { + const store = createStore({}); const tree = TestUtils.renderIntoDocument( - + {() => ( {({ dispatch }) =>
} @@ -184,17 +187,43 @@ describe('React', () => { ); const div = TestUtils.findRenderedDOMComponentWithTag(tree, 'div'); - expect(div.props.dispatch).toBe(redux.dispatch); + expect(div.props.dispatch).toBe(store.dispatch); }); - it('should throw an error if `state` returns anything but a plain object', () => { - const redux = createRedux(() => {}); + it('should throw an error if select returns anything but a plain object', () => { + const store = createStore({}); + + expect(() => { + TestUtils.renderIntoDocument( + + {() => ( + 1}> + {() =>
} + + )} + + ); + }).toThrow(/select/); expect(() => { TestUtils.renderIntoDocument( - + {() => ( - 1}> + 'hey'}> + {() =>
} + + )} + + ); + }).toThrow(/select/); + + function AwesomeMap() { } + + expect(() => { + TestUtils.renderIntoDocument( + + {() => ( + new AwesomeMap()}> {() =>
} )} @@ -203,29 +232,34 @@ describe('React', () => { }).toThrow(/select/); }); - it('does not throw error when `renderToString` is called on server', () => { + it('should not setState when renderToString is called on the server', () => { const { renderToString } = React; - const redux = createRedux({ string: stringBuilder }); + const store = createStore(stringBuilder); + class TestComp extends Component { componentWillMount() { - // simulate response action on data returning - redux.dispatch({ type: 'APPEND', body: 'a'}); + store.dispatch({ + type: 'APPEND', + body: 'a' + }); } + render() { return (
{this.props.string}
); } } + const el = ( - + {() => ( - ({ string: state.string })}> + ({ string })}> {({ string }) => } )} ); - expect(() => renderToString(el)).toNotThrow(); + expect(() => renderToString(el)).toNotThrow(); }); }); }); diff --git a/test/components/Provider.spec.js b/test/components/Provider.spec.js index a94a9abfe5..3e3b3c3308 100644 --- a/test/components/Provider.spec.js +++ b/test/components/Provider.spec.js @@ -1,7 +1,7 @@ import expect from 'expect'; import jsdomReact from './jsdomReact'; import React, { PropTypes, Component } from 'react/addons'; -import { createRedux } from '../../src'; +import { createStore } from '../../src'; import { Provider } from '../../src/react'; const { TestUtils } = React.addons; @@ -12,7 +12,7 @@ describe('React', () => { class Child extends Component { static contextTypes = { - redux: PropTypes.object.isRequired + store: PropTypes.object.isRequired } render() { @@ -20,30 +20,30 @@ describe('React', () => { } } - it('adds Redux to child context', () => { - const redux = createRedux({ test: () => 'test' }); + it('should add the store to the child context', () => { + const store = createStore({}); const tree = TestUtils.renderIntoDocument( - + {() => } ); const child = TestUtils.findRenderedComponentWithType(tree, Child); - expect(child.context.redux).toBe(redux); + expect(child.context.store).toBe(store); }); - it('does not lose subscribers when receiving new props', () => { - const redux1 = createRedux({ test: () => 'test' }); - const redux2 = createRedux({ test: () => 'test' }); + it('should replace just the reducer when receiving a new store in props', () => { + const store1 = createStore((state = 10) => state + 1); + const store2 = createStore((state = 10) => state * 2); const spy = expect.createSpy(() => {}); class ProviderContainer extends Component { - state = { redux: redux1 }; + state = { store: store1 }; render() { return ( - + {() => } ); @@ -52,15 +52,20 @@ describe('React', () => { const container = TestUtils.renderIntoDocument(); const child = TestUtils.findRenderedComponentWithType(container, Child); + expect(child.context.store.getState()).toEqual(11); - child.context.redux.subscribe(spy); - child.context.redux.dispatch({}); + child.context.store.subscribe(spy); + child.context.store.dispatch({}); expect(spy.calls.length).toEqual(1); + expect(child.context.store.getState()).toEqual(12); - container.setState({ redux: redux2 }); + container.setState({ store: store2 }); expect(spy.calls.length).toEqual(2); - child.context.redux.dispatch({}); + expect(child.context.store.getState()).toEqual(24); + + child.context.store.dispatch({}); expect(spy.calls.length).toEqual(3); + expect(child.context.store.getState()).toEqual(48); }); }); }); diff --git a/test/components/connect.spec.js b/test/components/connect.spec.js index 0fce239d36..6693ef1095 100644 --- a/test/components/connect.spec.js +++ b/test/components/connect.spec.js @@ -1,7 +1,7 @@ import expect from 'expect'; import jsdomReact from './jsdomReact'; import React, { PropTypes, Component } from 'react/addons'; -import { createRedux } from '../../src'; +import { createStore } from '../../src'; import { connect, Connector } from '../../src/react'; const { TestUtils } = React.addons; @@ -13,11 +13,11 @@ describe('React', () => { // Mock minimal Provider interface class Provider extends Component { static childContextTypes = { - redux: PropTypes.object.isRequired + store: PropTypes.object.isRequired } getChildContext() { - return { redux: this.props.redux }; + return { store: this.props.store }; } render() { @@ -25,8 +25,10 @@ describe('React', () => { } } - it('wraps component with Provider', () => { - const redux = createRedux({ test: () => 'test' }); + it('should wrap the component into Provider', () => { + const store = createStore(() => ({ + foo: 'bar' + })); @connect(state => state) class Container extends Component { @@ -36,18 +38,50 @@ describe('React', () => { } const container = TestUtils.renderIntoDocument( - - {() => } + + {() => } ); const div = TestUtils.findRenderedDOMComponentWithTag(container, 'div'); expect(div.props.pass).toEqual('through'); - expect(div.props.test).toEqual('test'); - expect(() => TestUtils.findRenderedComponentWithType(container, Connector)) - .toNotThrow(); + expect(div.props.foo).toEqual('bar'); + expect(() => + TestUtils.findRenderedComponentWithType(container, Connector) + ).toNotThrow(); }); - it('sets displayName correctly', () => { + it('should pass the only argument as the select prop down', () => { + const store = createStore(() => ({ + foo: 'baz', + bar: 'baz' + })); + + function select({ foo }) { + return { foo }; + } + + @connect(select) + class Container extends Component { + render() { + return
; + } + } + + const container = TestUtils.renderIntoDocument( + + {() => } + + ); + const connector = TestUtils.findRenderedComponentWithType(container, Connector); + expect(connector.props.select({ + foo: 5, + bar: 7 + })).toEqual({ + foo: 5 + }); + }); + + it('should set the displayName correctly', () => { @connect(state => state) class Container extends Component { render() { @@ -58,17 +92,17 @@ describe('React', () => { expect(Container.displayName).toBe('Connector(Container)'); }); - it('sets DecoratedComponent to wrapped component', () => { + it('should expose the wrapped component as DecoratedComponent', () => { class Container extends Component { render() { return
; } } - let decorator = connect(state => state); - let ConnectorDecorator = decorator(Container); + const decorator = connect(state => state); + const decorated = decorator(Container); - expect(ConnectorDecorator.DecoratedComponent).toBe(Container); + expect(decorated.DecoratedComponent).toBe(Container); }); }); }); diff --git a/test/components/provide.spec.js b/test/components/provide.spec.js index 53ac234960..babef47758 100644 --- a/test/components/provide.spec.js +++ b/test/components/provide.spec.js @@ -1,7 +1,7 @@ import expect from 'expect'; import jsdomReact from './jsdomReact'; import React, { PropTypes, Component } from 'react/addons'; -import { createRedux } from '../../src'; +import { createStore } from '../../src'; import { provide, Provider } from '../../src/react'; const { TestUtils } = React.addons; @@ -12,7 +12,7 @@ describe('React', () => { class Child extends Component { static contextTypes = { - redux: PropTypes.object.isRequired + store: PropTypes.object.isRequired } render() { @@ -20,26 +20,29 @@ describe('React', () => { } } - it('wraps component with Provider', () => { - const redux = createRedux({ test: () => 'test' }); + it('should wrap the component into Provider', () => { + const store = createStore({}); - @provide(redux) + @provide(store) class Container extends Component { render() { return ; } } - const container = TestUtils.renderIntoDocument(); + const container = TestUtils.renderIntoDocument( + + ); const child = TestUtils.findRenderedComponentWithType(container, Child); expect(child.props.pass).toEqual('through'); - expect(() => TestUtils.findRenderedComponentWithType(container, Provider)) - .toNotThrow(); - expect(child.context.redux).toBe(redux); + expect(() => + TestUtils.findRenderedComponentWithType(container, Provider) + ).toNotThrow(); + expect(child.context.store).toBe(store); }); - it('sets displayName correctly', () => { - @provide(createRedux({ test: () => 'test' })) + it('sets the displayName correctly', () => { + @provide(createStore({})) class Container extends Component { render() { return
; @@ -49,17 +52,17 @@ describe('React', () => { expect(Container.displayName).toBe('Provider(Container)'); }); - it('sets DecoratedComponent to wrapped component', () => { + it('should expose the wrapped component as DecoratedComponent', () => { class Container extends Component { render() { return
; } } - let decorator = provide(state => state); - let ProviderDecorator = decorator(Container); + const decorator = provide(state => state); + const decorated = decorator(Container); - expect(ProviderDecorator.DecoratedComponent).toBe(Container); + expect(decorated.DecoratedComponent).toBe(Container); }); }); }); diff --git a/test/composeMiddleware.spec.js b/test/composeMiddleware.spec.js index 95d916afe6..5e39b08eab 100644 --- a/test/composeMiddleware.spec.js +++ b/test/composeMiddleware.spec.js @@ -3,7 +3,7 @@ import { composeMiddleware } from '../src'; describe('Utils', () => { describe('composeMiddleware', () => { - it('should return combined middleware that executes from left to right', () => { + it('should return the combined middleware that executes from left to right', () => { const a = next => action => next(action + 'a'); const b = next => action => next(action + 'b'); const c = next => action => next(action + 'c'); diff --git a/test/composeReducers.spec.js b/test/composeReducers.spec.js new file mode 100644 index 0000000000..c0ce3c919e --- /dev/null +++ b/test/composeReducers.spec.js @@ -0,0 +1,33 @@ +import expect from 'expect'; +import { composeReducers } from '../src'; + +describe('Utils', () => { + describe('composeReducers', () => { + it('should return a composite reducer that maps the state keys to given reducers', () => { + const reducer = composeReducers({ + counter: (state = 0, action) => + action.type === 'increment' ? state + 1 : state, + stack: (state = [], action) => + action.type === 'push' ? [...state, action.value] : state + }); + + const s1 = reducer({}, { type: 'increment' }); + expect(s1).toEqual({ counter: 1, stack: [] }); + const s2 = reducer(s1, { type: 'push', value: 'a' }); + expect(s2).toEqual({ counter: 1, stack: ['a'] }); + }); + + it('ignores all props which are not a function', () => { + const reducer = composeReducers({ + fake: true, + broken: 'string', + another: { nested: 'object' }, + stack: (state = []) => state + }); + + expect( + Object.keys(reducer({}, { type: 'push' })) + ).toEqual(['stack']); + }); + }); +}); diff --git a/test/composeStores.spec.js b/test/composeStores.spec.js deleted file mode 100644 index 551b275e59..0000000000 --- a/test/composeStores.spec.js +++ /dev/null @@ -1,31 +0,0 @@ -import expect from 'expect'; -import { composeStores } from '../src'; - -describe('Utils', () => { - describe('composeStores', () => { - it('should return a store that maps state keys to reducer functions', () => { - const store = composeStores({ - counter: (state = 0, action) => - action.type === 'increment' ? state + 1 : state, - stack: (state = [], action) => - action.type === 'push' ? [...state, action.value] : state - }); - - const s1 = store({}, { type: 'increment' }); - expect(s1).toEqual({ counter: 1, stack: [] }); - const s2 = store(s1, { type: 'push', value: 'a' }); - expect(s2).toEqual({ counter: 1, stack: ['a'] }); - }); - - it('should ignore all props which are not a function', () => { - const store = composeStores({ - fake: true, - broken: 'string', - another: {nested: 'object'}, - stack: (state = []) => state - }); - - expect(Object.keys(store({}, {type: 'push'}))).toEqual(['stack']); - }); - }); -}); diff --git a/test/createDispatcher.spec.js b/test/createDispatcher.spec.js deleted file mode 100644 index 40991bfcec..0000000000 --- a/test/createDispatcher.spec.js +++ /dev/null @@ -1,45 +0,0 @@ -import expect from 'expect'; -import { createDispatcher, composeStores } from '../src'; -import thunkMiddleware from '../src/middleware/thunk'; -import * as helpers from './_helpers'; - -const { constants, defaultText, todoActions, todoStore } = helpers; -const { addTodo, addTodoAsync } = todoActions; -const { ADD_TODO } = constants; - -describe('createDispatcher', () => { - - it('should handle sync and async dispatches', done => { - const spy = expect.createSpy( - nextState => nextState - ).andCallThrough(); - - const dispatcher = createDispatcher( - composeStores({ todoStore }), - // we need this middleware to handle async actions - getState => [thunkMiddleware(getState)] - ); - - expect(dispatcher).toBeA('function'); - - const dispatchFn = dispatcher(undefined, spy); - expect(spy.calls.length).toBe(1); - expect(spy).toHaveBeenCalledWith({ todoStore: [] }); - - const addTodoAction = dispatchFn(addTodo(defaultText)); - expect(addTodoAction).toEqual({ type: ADD_TODO, text: defaultText }); - expect(spy.calls.length).toBe(2); - expect(spy).toHaveBeenCalledWith({ todoStore: [ - { id: 1, text: defaultText } - ] }); - - dispatchFn(addTodoAsync(('Say hi!'), () => { - expect(spy.calls.length).toBe(3); - expect(spy).toHaveBeenCalledWith({ todoStore: [ - { id: 2, text: 'Say hi!' }, - { id: 1, text: defaultText } - ] }); - done(); - })); - }); -}); diff --git a/test/createRedux.spec.js b/test/createRedux.spec.js deleted file mode 100644 index 1900de351c..0000000000 --- a/test/createRedux.spec.js +++ /dev/null @@ -1,72 +0,0 @@ -import expect from 'expect'; -import { createRedux } from '../src'; -import * as helpers from './_helpers'; - -const { defaultText, todoActions, todoStore } = helpers; -const { addTodo } = todoActions; - -describe('createRedux', () => { - - let redux; - - beforeEach(() => { - redux = createRedux({ todoStore }); - }); - - it('should expose Redux public API', () => { - const methods = Object.keys(redux); - - expect(methods.length).toBe(5); - expect(methods).toContain('subscribe'); - expect(methods).toContain('dispatch'); - expect(methods).toContain('getState'); - expect(methods).toContain('getDispatcher'); - expect(methods).toContain('replaceDispatcher'); - }); - - it('should subscribe to changes', done => { - let state = redux.getState(); - expect(state.todoStore).toEqual({}); - redux.subscribe(() => { - state = redux.getState(); - expect(state.todoStore).toEqual([{ id: 1, text: 'Hello World!' }]); - done(); - }); - redux.dispatch(addTodo(defaultText)); - }); - - it('should unsubscribe a listener', () => { - const changeListenerSpy = expect.createSpy(() => {}); - const unsubscribe = redux.subscribe(changeListenerSpy); - - expect(changeListenerSpy.calls.length).toBe(0); - - redux.dispatch(addTodo('Hello')); - expect(redux.getState().todoStore).toEqual([{ id: 1, text: 'Hello'}]); - expect(changeListenerSpy.calls.length).toBe(1); - - unsubscribe(); - redux.dispatch(addTodo('World')); - expect(redux.getState().todoStore).toEqual([ - { id: 2, text: 'World'}, - { id: 1, text: 'Hello'} - ]); - expect(changeListenerSpy.calls.length).toBe(1); - }); - - it('should use existing state when replacing the dispatcher', () => { - redux.dispatch(addTodo('Hello')); - - let nextRedux = createRedux({ todoStore }); - redux.replaceDispatcher(nextRedux.getDispatcher()); - - let state; - let action = (_, getState) => { - state = getState().todoStore; - }; - - redux.dispatch(action); - - expect(state).toEqual(redux.getState().todoStore); - }); -}); diff --git a/test/createStore.spec.js b/test/createStore.spec.js new file mode 100644 index 0000000000..6d2d72d7c8 --- /dev/null +++ b/test/createStore.spec.js @@ -0,0 +1,148 @@ +import expect from 'expect'; +import { createStore } from '../src/index'; +import * as reducers from './helpers/reducers'; +import { addTodo, addTodoIfEmpty, addTodoAsync } from './helpers/actionCreators'; + +describe('createStore', () => { + it('should expose the public API', () => { + const store = createStore(reducers); + const methods = Object.keys(store); + + expect(methods.length).toBe(5); + expect(methods).toContain('subscribe'); + expect(methods).toContain('dispatch'); + expect(methods).toContain('getState'); + expect(methods).toContain('getReducer'); + expect(methods).toContain('replaceReducer'); + }); + + it('should compose the reducers when passed an object', () => { + const store = createStore(reducers); + expect(store.getState()).toEqual({ + todos: [], + todosReverse: [] + }); + }); + + it('should pass the initial action and the initial state', () => { + const store = createStore(reducers.todos, [{ + id: 1, + text: 'Hello' + }]); + expect(store.getState()).toEqual([{ + id: 1, + text: 'Hello' + }]); + }); + + it('should provide the thunk middleware by default', done => { + const store = createStore(reducers.todos); + store.dispatch(addTodoIfEmpty('Hello')); + expect(store.getState()).toEqual([{ + id: 1, + text: 'Hello' + }]); + + store.dispatch(addTodoIfEmpty('Hello')); + expect(store.getState()).toEqual([{ + id: 1, + text: 'Hello' + }]); + + store.dispatch(addTodo('World')); + expect(store.getState()).toEqual([{ + id: 1, + text: 'Hello' + }, { + id: 2, + text: 'World' + }]); + + store.dispatch(addTodoAsync('Maybe')).then(() => { + expect(store.getState()).toEqual([{ + id: 1, + text: 'Hello' + }, { + id: 2, + text: 'World' + }, { + id: 3, + text: 'Maybe' + }]); + done(); + }); + }); + + it('should dispatch the raw action without the middleware', () => { + const store = createStore(reducers.todos, undefined, []); + store.dispatch(addTodo('Hello')); + expect(store.getState()).toEqual([{ + id: 1, + text: 'Hello' + }]); + + expect(() => + store.dispatch(addTodoAsync('World')) + ).toThrow(/plain/); + }); + + it('should handle nested dispatches gracefully', () => { + function foo(state = 0, action) { + return action.type === 'foo' ? 1 : state; + } + + function bar(state = 0, action) { + return action.type === 'bar' ? 2 : state; + } + + const store = createStore({ foo, bar }); + + store.subscribe(function kindaComponentDidUpdate() { + const state = store.getState(); + if (state.bar === 0) { + store.dispatch({ type: 'bar' }); + } + }); + + store.dispatch({ type: 'foo' }); + expect(store.getState()).toEqual({ + foo: 1, + bar: 2 + }); + }); + + it('should support custom dumb middleware', done => { + const doneMiddleware = next => action => { + next(action); + done(); + }; + + const store = createStore( + reducers.todos, + undefined, + [doneMiddleware] + ); + store.dispatch(addTodo('Hello')); + }); + + it('should support custom smart middleware', done => { + function doneMiddleware({ getState, dispatch }) { + return next => action => { + next(action); + + if (getState().length < 10) { + dispatch(action); + } else { + done(); + } + }; + } + + const store = createStore( + reducers.todos, + undefined, + ({ getState, dispatch }) => [doneMiddleware({ getState, dispatch })] + ); + store.dispatch(addTodo('Hello')); + }); +}); diff --git a/test/getDisplayName.spec.js b/test/getDisplayName.spec.js index 8b18e0dad2..a3a712d345 100644 --- a/test/getDisplayName.spec.js +++ b/test/getDisplayName.spec.js @@ -1,14 +1,14 @@ import expect from 'expect'; +import { createClass, Component } from 'react'; import getDisplayName from '../src/utils/getDisplayName'; describe('Utils', () => { describe('getDisplayName', () => { - - it('should ensure a name for the given component', () => { + it('should extract the component class name', () => { const names = [ - { displayName: 'Foo'}, - { name: 'Bar' }, - {} + createClass({ displayName: 'Foo', render() {} }), + class Bar extends Component {}, + createClass({ render() {} }) ].map(getDisplayName); expect(names).toEqual(['Foo', 'Bar', 'Component']); diff --git a/test/helpers/actionCreators.js b/test/helpers/actionCreators.js new file mode 100644 index 0000000000..e39dee423b --- /dev/null +++ b/test/helpers/actionCreators.js @@ -0,0 +1,20 @@ +import { ADD_TODO } from './actionTypes'; + +export function addTodo(text) { + return { type: ADD_TODO, text }; +} + +export function addTodoAsync(text) { + return dispatch => new Promise(resolve => setImmediate(() => { + dispatch(addTodo(text)); + resolve(); + })); +} + +export function addTodoIfEmpty(text) { + return (dispatch, getState) => { + if (!getState().length) { + dispatch(addTodo(text)); + } + }; +} diff --git a/test/helpers/actionTypes.js b/test/helpers/actionTypes.js new file mode 100644 index 0000000000..1d5bfa67c9 --- /dev/null +++ b/test/helpers/actionTypes.js @@ -0,0 +1 @@ +export const ADD_TODO = 'ADD_TODO'; diff --git a/test/helpers/reducers.js b/test/helpers/reducers.js new file mode 100644 index 0000000000..08eab9893b --- /dev/null +++ b/test/helpers/reducers.js @@ -0,0 +1,25 @@ +import { ADD_TODO } from './actionTypes'; + +export function todos(state = [], action) { + switch (action.type) { + case ADD_TODO: + return [...state, { + id: state.length ? state[state.length - 1].id + 1 : 1, + text: action.text + }]; + default: + return state; + } +} + +export function todosReverse(state = [], action) { + switch (action.type) { + case ADD_TODO: + return [{ + id: state.length ? state[0].id + 1 : 1, + text: action.text + }, ...state]; + default: + return state; + } +} diff --git a/test/utils/bindActionCreators.spec.js b/test/utils/bindActionCreators.spec.js index a4acd6840d..cfe023dd76 100644 --- a/test/utils/bindActionCreators.spec.js +++ b/test/utils/bindActionCreators.spec.js @@ -1,37 +1,31 @@ import expect from 'expect'; -import { bindActionCreators, createRedux } from '../../src'; -import * as helpers from '../_helpers'; - -const { todoActions, todoStore } = helpers; +import { bindActionCreators, createStore } from '../../src'; +import { todos } from '../helpers/reducers'; +import * as actionCreators from '../helpers/actionCreators'; describe('Utils', () => { describe('bindActionCreators', () => { - - let redux; + let store; beforeEach(() => { - redux = createRedux({ todoStore }); + store = createStore(todos); }); - it('should bind given actions to the dispatcher', done => { - let expectedCallCount = 2; - // just for monitoring the dispatched actions - redux.subscribe(() => { - expectedCallCount--; - if (expectedCallCount === 0) { - const state = redux.getState(); - expect(state.todoStore).toEqual([ - { id: 2, text: 'World' }, - { id: 1, text: 'Hello' } - ]); - done(); - } - }); - const actions = bindActionCreators(todoActions, redux.dispatch); - expect(Object.keys(actions)).toEqual(Object.keys(todoActions)); + it('should wrap the action creators with the dispatch function', () => { + const boundActionCreators = bindActionCreators(actionCreators, store.dispatch); + expect( + Object.keys(boundActionCreators) + ).toEqual( + Object.keys(actionCreators) + ); - actions.addTodo('Hello'); - actions.addTodoAsync('World'); + const action = boundActionCreators.addTodo('Hello'); + expect(action).toEqual( + actionCreators.addTodo('Hello') + ); + expect(store.getState()).toEqual([ + { id: 1, text: 'Hello' } + ]); }); }); }); diff --git a/test/utils/identity.spec.js b/test/utils/identity.spec.js index 87783b8117..ef48e5593c 100644 --- a/test/utils/identity.spec.js +++ b/test/utils/identity.spec.js @@ -3,8 +3,8 @@ import identity from '../../src/utils/identity'; describe('Utils', () => { describe('identity', () => { - it('should return first argument passed to it', () => { - const test = { 'a': 1 }; + it('should return the first argument passed to it', () => { + const test = { a: 1 }; expect(identity(test, 'test')).toBe(test); }); }); diff --git a/test/utils/mapValues.spec.js b/test/utils/mapValues.spec.js index 36c3c6b2c9..6f7945aa11 100644 --- a/test/utils/mapValues.spec.js +++ b/test/utils/mapValues.spec.js @@ -4,8 +4,14 @@ import mapValues from '../../src/utils/mapValues'; describe('Utils', () => { describe('mapValues', () => { it('should return object with mapped values', () => { - const test = { 'a': 'c', 'b': 'd' }; - expect(mapValues(test, (val, key) => val + key)).toEqual({ 'a': 'ca', 'b': 'db' }); + const test = { + a: 'c', + b: 'd' + }; + expect(mapValues(test, (val, key) => val + key)).toEqual({ + a: 'ca', + b: 'db' + }); }); }); }); diff --git a/test/utils/pick.spec.js b/test/utils/pick.spec.js index 6cd95f7395..c7b5a71e84 100644 --- a/test/utils/pick.spec.js +++ b/test/utils/pick.spec.js @@ -4,8 +4,15 @@ import pick from '../../src/utils/pick'; describe('Utils', () => { describe('pick', () => { it('should return object with picked values', () => { - const test = { 'name': 'lily', 'age': 20 }; - expect(pick(test, x => typeof x === 'string')).toEqual({ 'name': 'lily' }); + const test = { + name: 'lily', + age: 20 + }; + expect( + pick(test, x => typeof x === 'string') + ).toEqual({ + name: 'lily' + }); }); }); }); diff --git a/test/utils/shallowEquality.spec.js b/test/utils/shallowEquality.spec.js index 8ba48e6bdd..7146185bfb 100644 --- a/test/utils/shallowEquality.spec.js +++ b/test/utils/shallowEquality.spec.js @@ -5,17 +5,17 @@ import shallowEqual from '../../src/utils/shallowEqual'; describe('Utils', () => { // More info: https://github.com/gaearon/redux/pull/75#issuecomment-111635748 describe('shallowEqualScalar', () => { - it('returns true if both arguments are the same object', () => { + it('should return true if both arguments are the same object', () => { const o = { a: 1, b: 2 }; expect(shallowEqualScalar(o, o)).toBe(true); }); - it('returns false if either argument is null', () => { + it('should return false if either argument is null', () => { expect(shallowEqualScalar(null, {})).toBe(false); expect(shallowEqualScalar({}, null)).toBe(false); }); - it('returns true if arguments fields are equal', () => { + it('should return true if arguments fields are equal', () => { expect( shallowEqualScalar( { a: 1, b: 2, c: undefined }, @@ -31,7 +31,7 @@ describe('Utils', () => { ).toBe(true); }); - it('returns false if first argument has too many keys', () => { + it('should return false if first argument has too many keys', () => { expect( shallowEqualScalar( { a: 1, b: 2, c: 3 }, @@ -40,7 +40,7 @@ describe('Utils', () => { ).toBe(false); }); - it('returns false if second argument has too many keys', () => { + it('should return false if second argument has too many keys', () => { expect( shallowEqualScalar( { a: 1, b: 2 }, @@ -49,7 +49,7 @@ describe('Utils', () => { ).toBe(false); }); - it('returns false if arguments have keys dont have same value', () => { + it('should return false if arguments have keys dont have same value', () => { expect( shallowEqualScalar( { a: 1, b: 2 }, @@ -58,7 +58,7 @@ describe('Utils', () => { ).toBe(false); }); - it('returns false if arguments have field that are objects', () => { + it('should return false if arguments have field that are objects', () => { const o = {}; expect( shallowEqualScalar( @@ -68,7 +68,7 @@ describe('Utils', () => { ).toBe(false); }); - it('returns false if arguments have different keys', () => { + it('should return false if arguments have different keys', () => { expect( shallowEqualScalar( { a: 1, b: 2, c: undefined }, @@ -79,7 +79,7 @@ describe('Utils', () => { }); describe('shallowEqual', () => { - it('returns true if arguments fields are equal', () => { + it('should return true if arguments fields are equal', () => { expect( shallowEqual( { a: 1, b: 2, c: undefined }, @@ -103,7 +103,7 @@ describe('Utils', () => { ).toBe(true); }); - it('returns false if first argument has too many keys', () => { + it('should return false if first argument has too many keys', () => { expect( shallowEqual( { a: 1, b: 2, c: 3 }, @@ -112,7 +112,7 @@ describe('Utils', () => { ).toBe(false); }); - it('returns false if second argument has too many keys', () => { + it('should return false if second argument has too many keys', () => { expect( shallowEqual( { a: 1, b: 2 }, @@ -121,7 +121,7 @@ describe('Utils', () => { ).toBe(false); }); - it('returns false if arguments have different keys', () => { + it('should return false if arguments have different keys', () => { expect( shallowEqual( { a: 1, b: 2, c: undefined }, @@ -130,5 +130,4 @@ describe('Utils', () => { ).toBe(false); }); }); - }); From 34d90b9bbd070455f0ca4b46016559b48b495e0c Mon Sep 17 00:00:00 2001 From: Dan Abramov Date: Wed, 1 Jul 2015 00:07:34 +0200 Subject: [PATCH 02/84] Remove unnecessary changes --- examples/counter/containers/App.js | 2 +- examples/counter/containers/CounterApp.js | 2 +- examples/todomvc/containers/App.js | 5 ++--- examples/todomvc/containers/TodoApp.js | 2 +- 4 files changed, 5 insertions(+), 6 deletions(-) diff --git a/examples/counter/containers/App.js b/examples/counter/containers/App.js index 3acf7b9a1f..0cbedccf4a 100644 --- a/examples/counter/containers/App.js +++ b/examples/counter/containers/App.js @@ -1,6 +1,6 @@ import React from 'react'; import CounterApp from './CounterApp'; -import { createStore } from 'redux/index'; +import { createStore } from 'redux'; import { Provider } from 'redux/react'; import * as reducers from '../reducers'; diff --git a/examples/counter/containers/CounterApp.js b/examples/counter/containers/CounterApp.js index e6c90482e7..f60d74c746 100644 --- a/examples/counter/containers/CounterApp.js +++ b/examples/counter/containers/CounterApp.js @@ -1,5 +1,5 @@ import React from 'react'; -import { bindActionCreators } from 'redux/index'; +import { bindActionCreators } from 'redux'; import { connect } from 'redux/react'; import Counter from '../components/Counter'; import * as CounterActions from '../actions/CounterActions'; diff --git a/examples/todomvc/containers/App.js b/examples/todomvc/containers/App.js index 2563bf2669..1609f0ad33 100644 --- a/examples/todomvc/containers/App.js +++ b/examples/todomvc/containers/App.js @@ -1,11 +1,10 @@ import React from 'react'; import TodoApp from './TodoApp'; -import { createStore, composeReducers } from 'redux/index'; +import { createStore } from 'redux'; import { Provider } from 'redux/react'; import * as reducers from '../reducers'; -const reducer = composeReducers(reducers); -const store = createStore(reducer); +const store = createStore(reducers); export default class App { render() { diff --git a/examples/todomvc/containers/TodoApp.js b/examples/todomvc/containers/TodoApp.js index 348d51441a..03df600025 100644 --- a/examples/todomvc/containers/TodoApp.js +++ b/examples/todomvc/containers/TodoApp.js @@ -1,5 +1,5 @@ import React from 'react'; -import { bindActionCreators } from 'redux/index'; +import { bindActionCreators } from 'redux'; import { Connector } from 'redux/react'; import Header from '../components/Header'; import MainSection from '../components/MainSection'; From 8c8e4291689e754d0e2093af0fab1d7fafd46269 Mon Sep 17 00:00:00 2001 From: Dustan Kasten Date: Wed, 1 Jul 2015 10:02:13 -0400 Subject: [PATCH 03/84] s/atom/state and s/Composition/composition --- src/utils/composeReducers.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/utils/composeReducers.js b/src/utils/composeReducers.js index e93887fabf..4d654ecfbe 100644 --- a/src/utils/composeReducers.js +++ b/src/utils/composeReducers.js @@ -4,9 +4,9 @@ import pick from '../utils/pick'; export default function composeReducers(reducers) { const finalReducers = pick(reducers, (val) => typeof val === 'function'); - return function Composition(atom = {}, action) { + return function composition(state = {}, action) { return mapValues(finalReducers, (store, key) => - store(atom[key], action) + store(state[key], action) ); }; } From 667d8f5064dadde90cc223ef48ff9fcfe8db511e Mon Sep 17 00:00:00 2001 From: Dustan Kasten Date: Wed, 1 Jul 2015 09:53:45 -0400 Subject: [PATCH 04/84] state preservation when replacing a reducer: continue incrementing the id --- test/Store.spec.js | 8 ++++---- test/helpers/reducers.js | 11 +++++++++-- 2 files changed, 13 insertions(+), 6 deletions(-) diff --git a/test/Store.spec.js b/test/Store.spec.js index e1834db84e..dfe21c5676 100644 --- a/test/Store.spec.js +++ b/test/Store.spec.js @@ -95,7 +95,7 @@ describe('Store', () => { store.dispatch(addTodo('Perhaps')); expect(store.getState()).toEqual([{ - id: 2, + id: 3, text: 'Perhaps' }, { id: 1, @@ -108,7 +108,7 @@ describe('Store', () => { nextStore = new Store(todos); store.replaceReducer(nextStore.getReducer()); expect(store.getState()).toEqual([{ - id: 2, + id: 3, text: 'Perhaps' }, { id: 1, @@ -120,7 +120,7 @@ describe('Store', () => { store.dispatch(addTodo('Surely')); expect(store.getState()).toEqual([{ - id: 2, + id: 3, text: 'Perhaps' }, { id: 1, @@ -129,7 +129,7 @@ describe('Store', () => { id: 2, text: 'World' }, { - id: 3, + id: 4, text: 'Surely' }]); }); diff --git a/test/helpers/reducers.js b/test/helpers/reducers.js index 08eab9893b..70fdd6f7b2 100644 --- a/test/helpers/reducers.js +++ b/test/helpers/reducers.js @@ -1,10 +1,17 @@ import { ADD_TODO } from './actionTypes'; + +function id(state = []) { + return state.reduce((result, item) => ( + item.id > result ? item.id : result + ), 0) + 1; +} + export function todos(state = [], action) { switch (action.type) { case ADD_TODO: return [...state, { - id: state.length ? state[state.length - 1].id + 1 : 1, + id: id(state), text: action.text }]; default: @@ -16,7 +23,7 @@ export function todosReverse(state = [], action) { switch (action.type) { case ADD_TODO: return [{ - id: state.length ? state[0].id + 1 : 1, + id: id(state), text: action.text }, ...state]; default: From 6a2730deaf91cc53f358a56e981b31f740e58806 Mon Sep 17 00:00:00 2001 From: Andrew Clark Date: Sat, 4 Jul 2015 10:13:40 -0700 Subject: [PATCH 05/84] Implement applyMiddleware higher-order store --- src/createStore.js | 28 ++------------- src/index.js | 8 +++-- src/utils/applyMiddleware.js | 34 ++++++++++++++++++ src/utils/compose.js | 8 +++++ src/utils/composeMiddleware.js | 3 -- test/applyMiddleware.spec.js | 65 ++++++++++++++++++++++++++++++++++ test/compose.spec.js | 17 +++++++++ test/composeMiddleware.spec.js | 17 --------- test/createStore.spec.js | 6 ++-- 9 files changed, 134 insertions(+), 52 deletions(-) create mode 100644 src/utils/applyMiddleware.js create mode 100644 src/utils/compose.js delete mode 100644 src/utils/composeMiddleware.js create mode 100644 test/applyMiddleware.spec.js create mode 100644 test/compose.spec.js delete mode 100644 test/composeMiddleware.spec.js diff --git a/src/createStore.js b/src/createStore.js index a06c60cf26..c774b3329c 100644 --- a/src/createStore.js +++ b/src/createStore.js @@ -1,42 +1,18 @@ import Store from './Store'; import composeReducers from './utils/composeReducers'; -import composeMiddleware from './utils/composeMiddleware'; -import thunkMiddleware from './middleware/thunk'; - -const defaultMiddlewares = ({ dispatch, getState }) => [ - thunkMiddleware({ dispatch, getState }) -]; export default function createStore( reducer, - initialState, - middlewares = defaultMiddlewares + initialState ) { const finalReducer = typeof reducer === 'function' ? reducer : composeReducers(reducer); const store = new Store(finalReducer, initialState); - const getState = ::store.getState; - - const rawDispatch = ::store.dispatch; - let cookedDispatch = null; - - function dispatch(action) { - return cookedDispatch(action); - } - - const finalMiddlewares = typeof middlewares === 'function' ? - middlewares({ dispatch, getState }) : - middlewares; - - cookedDispatch = composeMiddleware( - ...finalMiddlewares, - rawDispatch - ); return { - dispatch: cookedDispatch, + dispatch: ::store.dispatch, subscribe: ::store.subscribe, getState: ::store.getState, getReducer: ::store.getReducer, diff --git a/src/index.js b/src/index.js index 6ba367c91c..b214ecda5d 100644 --- a/src/index.js +++ b/src/index.js @@ -2,13 +2,15 @@ import createStore from './createStore'; // Utilities -import composeMiddleware from './utils/composeMiddleware'; +import compose from './utils/compose'; import composeReducers from './utils/composeReducers'; import bindActionCreators from './utils/bindActionCreators'; +import applyMiddleware from './utils/applyMiddleware'; export { createStore, - composeMiddleware, + compose, composeReducers, - bindActionCreators + bindActionCreators, + applyMiddleware }; diff --git a/src/utils/applyMiddleware.js b/src/utils/applyMiddleware.js new file mode 100644 index 0000000000..254053a269 --- /dev/null +++ b/src/utils/applyMiddleware.js @@ -0,0 +1,34 @@ +import compose from './compose'; +import thunk from '../middleware/thunk'; + +function composeMiddleware(...middlewares) { + return methods => compose(...middlewares.map(m => m(methods))); +} + +/** + * Creates a higher-order store that applies middleware to a store's dispatch. + * Because middleware is potentially asynchronous, this should be the first + * higher-order store in the composition chain. + * @param {...Function} ...middlewares + * @return {Function} A higher-order store + */ +export default function applyMiddleware(...middlewares) { + const finalMiddlewares = middlewares.length ? + middlewares : + [thunk]; + + return next => (...args) => { + const store = next(...args); + const methods = { + dispatch: store.dispatch, + getState: store.getState + }; + return { + ...store, + dispatch: compose( + composeMiddleware(...finalMiddlewares)(methods), + store.dispatch + ) + }; + }; +} diff --git a/src/utils/compose.js b/src/utils/compose.js new file mode 100644 index 0000000000..4db0884d3b --- /dev/null +++ b/src/utils/compose.js @@ -0,0 +1,8 @@ +/** + * Composes functions from left to right + * @param {...Function} funcs - Functions to compose + * @return {Function} + */ +export default function compose(...funcs) { + return funcs.reduceRight((composed, f) => f(composed)); +} diff --git a/src/utils/composeMiddleware.js b/src/utils/composeMiddleware.js deleted file mode 100644 index 596403919d..0000000000 --- a/src/utils/composeMiddleware.js +++ /dev/null @@ -1,3 +0,0 @@ -export default function composeMiddleware(...middlewares) { - return middlewares.reduceRight((composed, m) => m(composed)); -} diff --git a/test/applyMiddleware.spec.js b/test/applyMiddleware.spec.js new file mode 100644 index 0000000000..73229837f8 --- /dev/null +++ b/test/applyMiddleware.spec.js @@ -0,0 +1,65 @@ +import expect from 'expect'; +import { createStore, applyMiddleware } from '../src/index'; +import * as reducers from './helpers/reducers'; +import { addTodo, addTodoAsync, addTodoIfEmpty } from './helpers/actionCreators'; +import thunk from '../src/middleware/thunk'; + +describe('applyMiddleware', () => { + it('wraps dispatch method with middleware', () => { + function test(spy) { + return methods => next => action => { + spy(methods); + return next(action); + }; + } + + const spy = expect.createSpy(() => {}); + const store = applyMiddleware(test(spy), thunk)(createStore)(reducers.todos); + store.dispatch(addTodo('Use Redux')); + + expect(Object.keys(spy.calls[0].arguments[0])).toEqual([ + 'dispatch', + 'getState' + ]); + expect(store.getState()).toEqual([ { id: 1, text: 'Use Redux' } ]); + }); + + it('uses thunk middleware by default', done => { + const store = applyMiddleware()(createStore)(reducers.todos); + + store.dispatch(addTodoIfEmpty('Hello')); + expect(store.getState()).toEqual([{ + id: 1, + text: 'Hello' + }]); + + store.dispatch(addTodoIfEmpty('Hello')); + expect(store.getState()).toEqual([{ + id: 1, + text: 'Hello' + }]); + + store.dispatch(addTodo('World')); + expect(store.getState()).toEqual([{ + id: 1, + text: 'Hello' + }, { + id: 2, + text: 'World' + }]); + + store.dispatch(addTodoAsync('Maybe')).then(() => { + expect(store.getState()).toEqual([{ + id: 1, + text: 'Hello' + }, { + id: 2, + text: 'World' + }, { + id: 3, + text: 'Maybe' + }]); + done(); + }); + }); +}); diff --git a/test/compose.spec.js b/test/compose.spec.js new file mode 100644 index 0000000000..92a574d371 --- /dev/null +++ b/test/compose.spec.js @@ -0,0 +1,17 @@ +import expect from 'expect'; +import { compose } from '../src'; + +describe('Utils', () => { + describe('compose', () => { + it('composes functions from left to right', () => { + const a = next => x => next(x + 'a'); + const b = next => x => next(x + 'b'); + const c = next => x => next(x + 'c'); + const final = x => x; + + expect(compose(a, b, c, final)('')).toBe('abc'); + expect(compose(b, c, a, final)('')).toBe('bca'); + expect(compose(c, a, b, final)('')).toBe('cab'); + }); + }); +}); diff --git a/test/composeMiddleware.spec.js b/test/composeMiddleware.spec.js deleted file mode 100644 index 5e39b08eab..0000000000 --- a/test/composeMiddleware.spec.js +++ /dev/null @@ -1,17 +0,0 @@ -import expect from 'expect'; -import { composeMiddleware } from '../src'; - -describe('Utils', () => { - describe('composeMiddleware', () => { - it('should return the combined middleware that executes from left to right', () => { - const a = next => action => next(action + 'a'); - const b = next => action => next(action + 'b'); - const c = next => action => next(action + 'c'); - const dispatch = action => action; - - expect(composeMiddleware(a, b, c, dispatch)('')).toBe('abc'); - expect(composeMiddleware(b, c, a, dispatch)('')).toBe('bca'); - expect(composeMiddleware(c, a, b, dispatch)('')).toBe('cab'); - }); - }); -}); diff --git a/test/createStore.spec.js b/test/createStore.spec.js index 6d2d72d7c8..dec7fc6efe 100644 --- a/test/createStore.spec.js +++ b/test/createStore.spec.js @@ -35,7 +35,7 @@ describe('createStore', () => { }]); }); - it('should provide the thunk middleware by default', done => { + it.skip('should provide the thunk middleware by default', done => { const store = createStore(reducers.todos); store.dispatch(addTodoIfEmpty('Hello')); expect(store.getState()).toEqual([{ @@ -111,7 +111,7 @@ describe('createStore', () => { }); }); - it('should support custom dumb middleware', done => { + it.skip('should support custom dumb middleware', done => { const doneMiddleware = next => action => { next(action); done(); @@ -125,7 +125,7 @@ describe('createStore', () => { store.dispatch(addTodo('Hello')); }); - it('should support custom smart middleware', done => { + it.skip('should support custom smart middleware', done => { function doneMiddleware({ getState, dispatch }) { return next => action => { next(action); From 9a1a0cd1349563c8e4c4fa6adbc38247146098fb Mon Sep 17 00:00:00 2001 From: Andrew Clark Date: Sat, 4 Jul 2015 11:34:56 -0700 Subject: [PATCH 06/84] Fix composeMiddleware and move back into separate module Discovered a subtle and confounding bug with composeMiddleware where it only works if dispatch is the last argument. It did not combine multiple middlewares into a single middlewares as advertised; it only combined multiple middlewares with dispatch to create a new dispatch. This didn't come up earlier because the codebase never happened to use it in the former way. This commit fixes the issue and adds a test for it. --- src/index.js | 4 +- src/middleware/thunk.js | 2 +- src/utils/applyMiddleware.js | 5 +-- src/utils/composeMiddleware.js | 10 +++++ test/composeMiddleware.spec.js | 17 ++++++++ test/createStore.spec.js | 73 ---------------------------------- 6 files changed, 32 insertions(+), 79 deletions(-) create mode 100644 src/utils/composeMiddleware.js create mode 100644 test/composeMiddleware.spec.js diff --git a/src/index.js b/src/index.js index b214ecda5d..a8569ec2f2 100644 --- a/src/index.js +++ b/src/index.js @@ -6,11 +6,13 @@ import compose from './utils/compose'; import composeReducers from './utils/composeReducers'; import bindActionCreators from './utils/bindActionCreators'; import applyMiddleware from './utils/applyMiddleware'; +import composeMiddleware from './utils/composeMiddleware'; export { createStore, compose, composeReducers, bindActionCreators, - applyMiddleware + applyMiddleware, + composeMiddleware }; diff --git a/src/middleware/thunk.js b/src/middleware/thunk.js index 14e92df5c2..e6638cdb06 100644 --- a/src/middleware/thunk.js +++ b/src/middleware/thunk.js @@ -1,5 +1,5 @@ export default function thunkMiddleware({ dispatch, getState }) { - return (next) => (action) => + return next => action => typeof action === 'function' ? action(dispatch, getState) : next(action); diff --git a/src/utils/applyMiddleware.js b/src/utils/applyMiddleware.js index 254053a269..e6c63ad0bb 100644 --- a/src/utils/applyMiddleware.js +++ b/src/utils/applyMiddleware.js @@ -1,10 +1,7 @@ import compose from './compose'; +import composeMiddleware from './composeMiddleware'; import thunk from '../middleware/thunk'; -function composeMiddleware(...middlewares) { - return methods => compose(...middlewares.map(m => m(methods))); -} - /** * Creates a higher-order store that applies middleware to a store's dispatch. * Because middleware is potentially asynchronous, this should be the first diff --git a/src/utils/composeMiddleware.js b/src/utils/composeMiddleware.js new file mode 100644 index 0000000000..bf702aae30 --- /dev/null +++ b/src/utils/composeMiddleware.js @@ -0,0 +1,10 @@ +import compose from './compose'; + +/** + * Compose middleware from left to right + * @param {...Function} middlewares + * @return {Function} + */ +export default function composeMiddleware(...middlewares) { + return methods => next => compose(...middlewares.map(m => m(methods)), next); +} diff --git a/test/composeMiddleware.spec.js b/test/composeMiddleware.spec.js new file mode 100644 index 0000000000..4d291f4816 --- /dev/null +++ b/test/composeMiddleware.spec.js @@ -0,0 +1,17 @@ +import expect from 'expect'; +import { composeMiddleware } from '../src'; + +describe('Utils', () => { + describe('composeMiddleware', () => { + it('should return combined middleware that executes from left to right', () => { + const a = () => next => action => next(action + 'a'); + const b = () => next => action => next(action + 'b'); + const c = () => next => action => next(action + 'c'); + const dispatch = action => action; + + expect(composeMiddleware(a, b, c)()(dispatch)('')).toBe('abc'); + expect(composeMiddleware(b, c, a)()(dispatch)('')).toBe('bca'); + expect(composeMiddleware(c, a, b)()(dispatch)('')).toBe('cab'); + }); + }); +}); diff --git a/test/createStore.spec.js b/test/createStore.spec.js index dec7fc6efe..efab9b6b98 100644 --- a/test/createStore.spec.js +++ b/test/createStore.spec.js @@ -35,44 +35,6 @@ describe('createStore', () => { }]); }); - it.skip('should provide the thunk middleware by default', done => { - const store = createStore(reducers.todos); - store.dispatch(addTodoIfEmpty('Hello')); - expect(store.getState()).toEqual([{ - id: 1, - text: 'Hello' - }]); - - store.dispatch(addTodoIfEmpty('Hello')); - expect(store.getState()).toEqual([{ - id: 1, - text: 'Hello' - }]); - - store.dispatch(addTodo('World')); - expect(store.getState()).toEqual([{ - id: 1, - text: 'Hello' - }, { - id: 2, - text: 'World' - }]); - - store.dispatch(addTodoAsync('Maybe')).then(() => { - expect(store.getState()).toEqual([{ - id: 1, - text: 'Hello' - }, { - id: 2, - text: 'World' - }, { - id: 3, - text: 'Maybe' - }]); - done(); - }); - }); - it('should dispatch the raw action without the middleware', () => { const store = createStore(reducers.todos, undefined, []); store.dispatch(addTodo('Hello')); @@ -110,39 +72,4 @@ describe('createStore', () => { bar: 2 }); }); - - it.skip('should support custom dumb middleware', done => { - const doneMiddleware = next => action => { - next(action); - done(); - }; - - const store = createStore( - reducers.todos, - undefined, - [doneMiddleware] - ); - store.dispatch(addTodo('Hello')); - }); - - it.skip('should support custom smart middleware', done => { - function doneMiddleware({ getState, dispatch }) { - return next => action => { - next(action); - - if (getState().length < 10) { - dispatch(action); - } else { - done(); - } - }; - } - - const store = createStore( - reducers.todos, - undefined, - ({ getState, dispatch }) => [doneMiddleware({ getState, dispatch })] - ); - store.dispatch(addTodo('Hello')); - }); }); From b72f90960314d4e95f13f2329429edfdfe0f1849 Mon Sep 17 00:00:00 2001 From: Andrew Clark Date: Sat, 4 Jul 2015 11:40:36 -0700 Subject: [PATCH 07/84] Fix linting --- test/createStore.spec.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test/createStore.spec.js b/test/createStore.spec.js index efab9b6b98..20f22e0566 100644 --- a/test/createStore.spec.js +++ b/test/createStore.spec.js @@ -1,7 +1,7 @@ import expect from 'expect'; import { createStore } from '../src/index'; import * as reducers from './helpers/reducers'; -import { addTodo, addTodoIfEmpty, addTodoAsync } from './helpers/actionCreators'; +import { addTodo, addTodoAsync } from './helpers/actionCreators'; describe('createStore', () => { it('should expose the public API', () => { From b8e7e1ee2e8dc1431cd17a6763ccf8dc330f914b Mon Sep 17 00:00:00 2001 From: taylorhakes Date: Tue, 30 Jun 2015 21:26:59 -0400 Subject: [PATCH 08/84] Throw error on undefined value from reducer function --- src/utils/composeReducers.js | 12 +++++++++--- test/composeReducers.spec.js | 25 +++++++++++++++++++++++++ 2 files changed, 34 insertions(+), 3 deletions(-) diff --git a/src/utils/composeReducers.js b/src/utils/composeReducers.js index 4d654ecfbe..e1f4249933 100644 --- a/src/utils/composeReducers.js +++ b/src/utils/composeReducers.js @@ -1,12 +1,18 @@ import mapValues from '../utils/mapValues'; import pick from '../utils/pick'; +import invariant from 'invariant'; export default function composeReducers(reducers) { const finalReducers = pick(reducers, (val) => typeof val === 'function'); return function composition(state = {}, action) { - return mapValues(finalReducers, (store, key) => - store(state[key], action) - ); + return mapValues(finalReducers, (reducer, key) => { + const newState = reducer(state[key], action); + invariant( + typeof newState !== 'undefined', + `Reducer ${key} returns undefined. By default reducer should return original state.` + ); + return newState; + }); }; } diff --git a/test/composeReducers.spec.js b/test/composeReducers.spec.js index c0ce3c919e..0e578df68e 100644 --- a/test/composeReducers.spec.js +++ b/test/composeReducers.spec.js @@ -29,5 +29,30 @@ describe('Utils', () => { Object.keys(reducer({}, { type: 'push' })) ).toEqual(['stack']); }); + + it('should throw an error if undefined return from reducer', () => { + const reducer = composeReducers({ + stack: (state = []) => state, + bad: (state = [], action) => { + if (action.type === 'something') { + return state; + } + } + }); + expect(() => reducer({}, {type: '@@testType'})).toThrow(); + }); + + it('should throw an error if undefined return not by default', () => { + const reducer = composeReducers({ + stack: (state = []) => state, + bad: (state = 1, action) => { + if (action.type !== 'something') { + return state; + } + } + }); + expect(reducer({}, {type: '@@testType'})).toEqual({stack: [], bad: 1}); + expect(() => reducer({}, {type: 'something'})).toThrow(); + }); }); }); From de685de23289575f344638370912fea4989c0350 Mon Sep 17 00:00:00 2001 From: Dan Abramov Date: Wed, 8 Jul 2015 13:32:48 +0300 Subject: [PATCH 09/84] Add more helpful error messages --- src/utils/composeReducers.js | 23 +++++++++++++-- test/composeReducers.spec.js | 54 ++++++++++++++++++++++++++++-------- 2 files changed, 63 insertions(+), 14 deletions(-) diff --git a/src/utils/composeReducers.js b/src/utils/composeReducers.js index e1f4249933..ab5ab7ce24 100644 --- a/src/utils/composeReducers.js +++ b/src/utils/composeReducers.js @@ -2,6 +2,25 @@ import mapValues from '../utils/mapValues'; import pick from '../utils/pick'; import invariant from 'invariant'; +function getErrorMessage(key, action) { + const actionType = action && action.type; + const actionName = actionType && `"${actionType}"` || 'an action'; + const reducerName = `Reducer "${key}"`; + + if (actionType === '@@INIT') { + return ( + `${reducerName} returned undefined during initialization. ` + + `If the state passed to the reducer is undefined, ` + + `you must explicitly return the initial state.` + ); + } + + return ( + `Reducer "${key}" returned undefined handling ${actionName}. ` + + `To ignore an action, you must explicitly return the previous state.` + ); +} + export default function composeReducers(reducers) { const finalReducers = pick(reducers, (val) => typeof val === 'function'); @@ -9,8 +28,8 @@ export default function composeReducers(reducers) { return mapValues(finalReducers, (reducer, key) => { const newState = reducer(state[key], action); invariant( - typeof newState !== 'undefined', - `Reducer ${key} returns undefined. By default reducer should return original state.` + typeof newState !== 'undefined', + getErrorMessage(key, action) ); return newState; }); diff --git a/test/composeReducers.spec.js b/test/composeReducers.spec.js index 0e578df68e..c2e429571f 100644 --- a/test/composeReducers.spec.js +++ b/test/composeReducers.spec.js @@ -30,29 +30,59 @@ describe('Utils', () => { ).toEqual(['stack']); }); - it('should throw an error if undefined return from reducer', () => { + it('should throw an error if a reducer returns undefined', () => { const reducer = composeReducers({ - stack: (state = []) => state, - bad: (state = [], action) => { - if (action.type === 'something') { + undefinedByDefault(state = 0, action) { + switch (action && action.type) { + case 'increment': + return state + 1; + case 'decrement': + return state - 1; + case '@@INIT': return state; + default: + return undefined; } } }); - expect(() => reducer({}, {type: '@@testType'})).toThrow(); + + const initialState = reducer(undefined, { type: '@@INIT' }); + expect( + () => reducer(initialState, { type: 'whatever' }) + ).toThrow( + /"undefinedByDefault".*"whatever"/ + ); + expect( + () => reducer(initialState, null) + ).toThrow( + /"undefinedByDefault".*an action/ + ); + expect( + () => reducer(initialState, {}) + ).toThrow( + /"undefinedByDefault".*an action/ + ); }); - - it('should throw an error if undefined return not by default', () => { + + it('should throw an error if a reducer returns undefined initializing', () => { const reducer = composeReducers({ - stack: (state = []) => state, - bad: (state = 1, action) => { - if (action.type !== 'something') { + undefinedInitially(state, action) { + switch (action.type) { + case 'increment': + return state + 1; + case 'decrement': + return state - 1; + default: return state; } } }); - expect(reducer({}, {type: '@@testType'})).toEqual({stack: [], bad: 1}); - expect(() => reducer({}, {type: 'something'})).toThrow(); + + expect( + () => reducer(undefined, { type: '@@INIT' }) + ).toThrow( + /"undefinedInitially".*initialization/ + ); }); }); }); From 039c30fd65af291483ca9c226a66b0285e57745f Mon Sep 17 00:00:00 2001 From: Dan Abramov Date: Wed, 8 Jul 2015 14:21:14 +0300 Subject: [PATCH 10/84] composeReducers -> combineReducers --- src/createStore.js | 4 ++-- src/index.js | 4 ++-- src/utils/{composeReducers.js => combineReducers.js} | 2 +- ...mposeReducers.spec.js => combineReducers.spec.js} | 12 ++++++------ 4 files changed, 11 insertions(+), 11 deletions(-) rename src/utils/{composeReducers.js => combineReducers.js} (95%) rename test/{composeReducers.spec.js => combineReducers.spec.js} (90%) diff --git a/src/createStore.js b/src/createStore.js index c774b3329c..6a79406c98 100644 --- a/src/createStore.js +++ b/src/createStore.js @@ -1,5 +1,5 @@ import Store from './Store'; -import composeReducers from './utils/composeReducers'; +import combineReducers from './utils/combineReducers'; export default function createStore( reducer, @@ -7,7 +7,7 @@ export default function createStore( ) { const finalReducer = typeof reducer === 'function' ? reducer : - composeReducers(reducer); + combineReducers(reducer); const store = new Store(finalReducer, initialState); diff --git a/src/index.js b/src/index.js index a8569ec2f2..1cfbbf6489 100644 --- a/src/index.js +++ b/src/index.js @@ -3,7 +3,7 @@ import createStore from './createStore'; // Utilities import compose from './utils/compose'; -import composeReducers from './utils/composeReducers'; +import combineReducers from './utils/combineReducers'; import bindActionCreators from './utils/bindActionCreators'; import applyMiddleware from './utils/applyMiddleware'; import composeMiddleware from './utils/composeMiddleware'; @@ -11,7 +11,7 @@ import composeMiddleware from './utils/composeMiddleware'; export { createStore, compose, - composeReducers, + combineReducers, bindActionCreators, applyMiddleware, composeMiddleware diff --git a/src/utils/composeReducers.js b/src/utils/combineReducers.js similarity index 95% rename from src/utils/composeReducers.js rename to src/utils/combineReducers.js index ab5ab7ce24..fe65c056cc 100644 --- a/src/utils/composeReducers.js +++ b/src/utils/combineReducers.js @@ -21,7 +21,7 @@ function getErrorMessage(key, action) { ); } -export default function composeReducers(reducers) { +export default function combineReducers(reducers) { const finalReducers = pick(reducers, (val) => typeof val === 'function'); return function composition(state = {}, action) { diff --git a/test/composeReducers.spec.js b/test/combineReducers.spec.js similarity index 90% rename from test/composeReducers.spec.js rename to test/combineReducers.spec.js index c2e429571f..49483702ef 100644 --- a/test/composeReducers.spec.js +++ b/test/combineReducers.spec.js @@ -1,10 +1,10 @@ import expect from 'expect'; -import { composeReducers } from '../src'; +import { combineReducers } from '../src'; describe('Utils', () => { - describe('composeReducers', () => { + describe('combineReducers', () => { it('should return a composite reducer that maps the state keys to given reducers', () => { - const reducer = composeReducers({ + const reducer = combineReducers({ counter: (state = 0, action) => action.type === 'increment' ? state + 1 : state, stack: (state = [], action) => @@ -18,7 +18,7 @@ describe('Utils', () => { }); it('ignores all props which are not a function', () => { - const reducer = composeReducers({ + const reducer = combineReducers({ fake: true, broken: 'string', another: { nested: 'object' }, @@ -31,7 +31,7 @@ describe('Utils', () => { }); it('should throw an error if a reducer returns undefined', () => { - const reducer = composeReducers({ + const reducer = combineReducers({ undefinedByDefault(state = 0, action) { switch (action && action.type) { case 'increment': @@ -65,7 +65,7 @@ describe('Utils', () => { }); it('should throw an error if a reducer returns undefined initializing', () => { - const reducer = composeReducers({ + const reducer = combineReducers({ undefinedInitially(state, action) { switch (action.type) { case 'increment': From 7245b589262c56c6d3b4540c3cd3f17edda65467 Mon Sep 17 00:00:00 2001 From: Dan Abramov Date: Wed, 8 Jul 2015 15:13:30 +0300 Subject: [PATCH 11/84] Fix duplicate React when running examples --- examples/counter/webpack.config.js | 3 ++- examples/todomvc/webpack.config.js | 3 ++- 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/examples/counter/webpack.config.js b/examples/counter/webpack.config.js index 33fb9a3291..2e86f7e7ea 100644 --- a/examples/counter/webpack.config.js +++ b/examples/counter/webpack.config.js @@ -19,7 +19,8 @@ module.exports = { ], resolve: { alias: { - 'redux': path.join(__dirname, '../../src') + 'redux': path.join(__dirname, '..', '..', 'src'), + 'react': path.join(__dirname, '..', '..', 'node_modules', 'react') }, extensions: ['', '.js'] }, diff --git a/examples/todomvc/webpack.config.js b/examples/todomvc/webpack.config.js index e1e16c0468..9a949855bd 100644 --- a/examples/todomvc/webpack.config.js +++ b/examples/todomvc/webpack.config.js @@ -19,7 +19,8 @@ module.exports = { ], resolve: { alias: { - 'redux': path.join(__dirname, '../../src') + 'redux': path.join(__dirname, '..', '..', 'src'), + 'react': path.join(__dirname, '..', '..', 'node_modules', 'react') }, extensions: ['', '.js'] }, From e621c79639d4ca661196024c04ed63aaf0c0fd0a Mon Sep 17 00:00:00 2001 From: Dan Abramov Date: Wed, 8 Jul 2015 15:15:51 +0300 Subject: [PATCH 12/84] Fix counter example --- examples/counter/containers/App.js | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/examples/counter/containers/App.js b/examples/counter/containers/App.js index 0cbedccf4a..28ed8049b6 100644 --- a/examples/counter/containers/App.js +++ b/examples/counter/containers/App.js @@ -1,10 +1,11 @@ import React from 'react'; import CounterApp from './CounterApp'; -import { createStore } from 'redux'; +import { createStore, applyMiddleware } from 'redux'; import { Provider } from 'redux/react'; import * as reducers from '../reducers'; -const store = createStore(reducers); +const createStoreWithMiddleware = applyMiddleware()(createStore); +const store = createStoreWithMiddleware(reducers); export default class App { render() { From 152c886127fe9848dd2cf562fcf7c6495c7c447a Mon Sep 17 00:00:00 2001 From: Dan Abramov Date: Thu, 9 Jul 2015 04:21:19 +0300 Subject: [PATCH 13/84] Fix lint --- test/applyMiddleware.spec.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/test/applyMiddleware.spec.js b/test/applyMiddleware.spec.js index 73229837f8..ca7e72bff8 100644 --- a/test/applyMiddleware.spec.js +++ b/test/applyMiddleware.spec.js @@ -6,9 +6,9 @@ import thunk from '../src/middleware/thunk'; describe('applyMiddleware', () => { it('wraps dispatch method with middleware', () => { - function test(spy) { + function test(spyOnMethods) { return methods => next => action => { - spy(methods); + spyOnMethods(methods); return next(action); }; } From 406ca6225b8154afee7d6ea4a389572263bee921 Mon Sep 17 00:00:00 2001 From: Dan Abramov Date: Thu, 9 Jul 2015 04:31:16 +0300 Subject: [PATCH 14/84] Handle changes from dispatch inside componentDidMount (fixes #208) --- src/components/createConnector.js | 5 ++++- test/components/Connector.spec.js | 32 ++++++++++++++++++++++++++++++- 2 files changed, 35 insertions(+), 2 deletions(-) diff --git a/src/components/createConnector.js b/src/components/createConnector.js index 153130000d..8bfddd4da6 100644 --- a/src/components/createConnector.js +++ b/src/components/createConnector.js @@ -44,6 +44,7 @@ export default function createConnector(React) { componentDidMount() { this.unsubscribe = this.context.store.subscribe(::this.handleChange); + this.handleChange(); } componentWillReceiveProps(nextProps) { @@ -59,7 +60,9 @@ export default function createConnector(React) { handleChange(props = this.props) { const nextState = this.selectState(props, this.context); - this.setState(nextState); + if (!this.isSliceEqual(this.state.slice, nextState.slice)) { + this.setState(nextState); + } } selectState(props, context) { diff --git a/test/components/Connector.spec.js b/test/components/Connector.spec.js index 27d968ea1a..bdc13b1c54 100644 --- a/test/components/Connector.spec.js +++ b/test/components/Connector.spec.js @@ -245,7 +245,7 @@ describe('React', () => { } render() { - return (
{this.props.string}
); + return
{this.props.string}
; } } @@ -261,5 +261,35 @@ describe('React', () => { expect(() => renderToString(el)).toNotThrow(); }); + + it('should handle dispatch inside componentDidMount', () => { + const store = createStore(stringBuilder); + + class TestComp extends Component { + componentDidMount() { + store.dispatch({ + type: 'APPEND', + body: 'a' + }); + } + + render() { + return
{this.props.string}
; + } + } + + const tree = TestUtils.renderIntoDocument( + + {() => ( + ({ string })}> + {({ string }) => } + + )} + + ); + + const testComp = TestUtils.findRenderedComponentWithType(tree, TestComp); + expect(testComp.props.string).toBe('a'); + }); }); }); From 038cd25b31b26c4fd4ad0524a601a83fe23bcdd1 Mon Sep 17 00:00:00 2001 From: taylorhakes Date: Thu, 9 Jul 2015 01:33:27 -0400 Subject: [PATCH 15/84] Added test for shallowEqualScalar in connect decorator --- test/components/connect.spec.js | 48 ++++++++++++++++++++++++++++++++- 1 file changed, 47 insertions(+), 1 deletion(-) diff --git a/test/components/connect.spec.js b/test/components/connect.spec.js index 6693ef1095..5b3fee90c2 100644 --- a/test/components/connect.spec.js +++ b/test/components/connect.spec.js @@ -7,7 +7,7 @@ import { connect, Connector } from '../../src/react'; const { TestUtils } = React.addons; describe('React', () => { - describe('provide', () => { + describe('connect', () => { jsdomReact(); // Mock minimal Provider interface @@ -50,6 +50,52 @@ describe('React', () => { ).toNotThrow(); }); + it('should handle additional prop changes in addition to slice', () => { + const store = createStore(() => ({ + foo: 'bar' + })); + + @connect(state => state) + class ConnectContainer extends Component { + render() { + return ( +
+ ); + } + } + + class Container extends Component { + constructor() { + super(); + this.state = { + bar: { + baz: '' + } + }; + } + componentDidMount() { + + // Simulate deep object mutation + this.state.bar.baz = 'through'; + this.setState({ + bar: this.state.bar + }); + } + render() { + return ( + + {() => } + + ); + } + } + + const container = TestUtils.renderIntoDocument(); + const div = TestUtils.findRenderedDOMComponentWithTag(container, 'div'); + expect(div.props.foo).toEqual('bar'); + expect(div.props.pass).toEqual('through'); + }); + it('should pass the only argument as the select prop down', () => { const store = createStore(() => ({ foo: 'baz', From 4c68696134f48a1ad71f62ce9aae3031c69ac0c5 Mon Sep 17 00:00:00 2001 From: Andrew Clark Date: Thu, 9 Jul 2015 23:41:47 -0700 Subject: [PATCH 16/84] Add Flow type annotations --- .eslintrc | 4 ++++ .flowconfig | 9 +++++++++ src/Store.js | 26 +++++++++++++++++--------- src/createStore.js | 26 +++++++++++++++----------- src/middleware/thunk.js | 11 +++++++++-- src/types.js | 10 ++++++++++ src/utils/applyMiddleware.js | 16 +++++++++++----- src/utils/bindActionCreators.js | 8 +++++++- src/utils/combineReducers.js | 8 +++++--- src/utils/compose.js | 4 +++- src/utils/createStoreShape.js | 4 +++- src/utils/identity.js | 4 +++- src/utils/isPlainObject.js | 4 +++- src/utils/mapValues.js | 4 +++- src/utils/pick.js | 4 +++- src/utils/shallowEqual.js | 12 +++++++----- src/utils/shallowEqualScalar.js | 16 +++++++++------- 17 files changed, 121 insertions(+), 49 deletions(-) create mode 100644 .flowconfig create mode 100644 src/types.js diff --git a/.eslintrc b/.eslintrc index 5cf245ac59..90e3903312 100644 --- a/.eslintrc +++ b/.eslintrc @@ -10,6 +10,10 @@ "react/jsx-uses-vars": 2, "react/react-in-jsx-scope": 2, + // Disable until Flow supports let and const + "no-var": 0, + "vars-on-top": 0, + //Temporarirly disabled due to a possible bug in babel-eslint (todomvc example) "block-scoped-var": 0, // Temporarily disabled for test/* until babel/babel-eslint#33 is resolved diff --git a/.flowconfig b/.flowconfig new file mode 100644 index 0000000000..369768b7ad --- /dev/null +++ b/.flowconfig @@ -0,0 +1,9 @@ +[ignore] +.*/lib +.*/test + +[include] + +[libs] + +[options] diff --git a/src/Store.js b/src/Store.js index ed85b987fc..5ee17c431d 100644 --- a/src/Store.js +++ b/src/Store.js @@ -1,8 +1,16 @@ +/* @flow */ + import invariant from 'invariant'; import isPlainObject from './utils/isPlainObject'; +import type { State, Action, Reducer } from './types'; + export default class Store { - constructor(reducer, initialState) { + state: State; + reducer: Reducer; + listeners: Array; + + constructor(reducer: Reducer, initialState: State): void { invariant( typeof reducer === 'function', 'Expected the reducer to be a function.' @@ -13,37 +21,37 @@ export default class Store { this.replaceReducer(reducer); } - getReducer() { + getReducer(): Reducer { return this.reducer; } - replaceReducer(nextReducer) { + replaceReducer(nextReducer: Reducer): void { this.reducer = nextReducer; this.dispatch({ type: '@@INIT' }); } - dispatch(action) { + dispatch(action: Action): Action { invariant( isPlainObject(action), 'Actions must be plain objects. Use custom middleware for async actions.' ); - const { reducer } = this; + var { reducer } = this; this.state = reducer(this.state, action); this.listeners.forEach(listener => listener()); return action; } - getState() { + getState(): State { return this.state; } - subscribe(listener) { - const { listeners } = this; + subscribe(listener: Function): Function { + var { listeners } = this; listeners.push(listener); return function unsubscribe() { - const index = listeners.indexOf(listener); + var index = listeners.indexOf(listener); listeners.splice(index, 1); }; } diff --git a/src/createStore.js b/src/createStore.js index 6a79406c98..1bfca57b2f 100644 --- a/src/createStore.js +++ b/src/createStore.js @@ -1,21 +1,25 @@ -import Store from './Store'; +/* @flow */ + +import StoreClass from './Store'; import combineReducers from './utils/combineReducers'; +import type { State, Action, Reducer, Dispatch, Store } from './types'; + export default function createStore( - reducer, - initialState -) { - const finalReducer = typeof reducer === 'function' ? + reducer: Reducer, + initialState: State +): Store { + var finalReducer = typeof reducer === 'function' ? reducer : combineReducers(reducer); - const store = new Store(finalReducer, initialState); + var store = new StoreClass(finalReducer, initialState); return { - dispatch: ::store.dispatch, - subscribe: ::store.subscribe, - getState: ::store.getState, - getReducer: ::store.getReducer, - replaceReducer: ::store.replaceReducer + dispatch: store.dispatch.bind(store), + subscribe: store.subscribe.bind(store), + getState: store.getState.bind(store), + getReducer: store.getReducer.bind(store), + replaceReducer: store.replaceReducer.bind(store) }; } diff --git a/src/middleware/thunk.js b/src/middleware/thunk.js index e6638cdb06..121aaa9bc9 100644 --- a/src/middleware/thunk.js +++ b/src/middleware/thunk.js @@ -1,5 +1,12 @@ -export default function thunkMiddleware({ dispatch, getState }) { - return next => action => +/* @flow */ + +import type { Dispatch, State, Action } from '../types'; + +type StoreMethods = { dispatch: Dispatch, getState: () => State }; + +export default function thunkMiddleware(storeMethods: StoreMethods): Dispatch { + var { dispatch, getState } = storeMethods; + return (next: Dispatch) => (action: Action) => typeof action === 'function' ? action(dispatch, getState) : next(action); diff --git a/src/types.js b/src/types.js new file mode 100644 index 0000000000..e1b79270d1 --- /dev/null +++ b/src/types.js @@ -0,0 +1,10 @@ +export type State = any; +export type Action = Object; +export type IntermediateAction = any; +export type Dispatch = (a: Action | IntermediateAction) => any; +export type Reducer = (state: S, action: A) => S; +export type ActionCreator = (...args: any) => Action | IntermediateAction +export type Middleware = (methods: { dispatch: Dispatch, getState: () => State }) => (next: Dispatch) => Dispatch; +export type Store = { dispatch: Dispatch, getState: State, subscribe: Function, getReducer: Reducer, replaceReducer: void }; +export type CreateStore = (reducer: Function, initialState: any) => Store; +export type HigherOrderStore = (next: CreateStore) => CreateStore; diff --git a/src/utils/applyMiddleware.js b/src/utils/applyMiddleware.js index e6c63ad0bb..a310afd63b 100644 --- a/src/utils/applyMiddleware.js +++ b/src/utils/applyMiddleware.js @@ -1,7 +1,11 @@ +/* @flow */ + import compose from './compose'; import composeMiddleware from './composeMiddleware'; import thunk from '../middleware/thunk'; +import type { Middleware, Dispatch, CreateStore } from '../types'; + /** * Creates a higher-order store that applies middleware to a store's dispatch. * Because middleware is potentially asynchronous, this should be the first @@ -9,14 +13,16 @@ import thunk from '../middleware/thunk'; * @param {...Function} ...middlewares * @return {Function} A higher-order store */ -export default function applyMiddleware(...middlewares) { - const finalMiddlewares = middlewares.length ? +export default function applyMiddleware( + ...middlewares: Array +): Dispatch { + var finalMiddlewares = middlewares.length ? middlewares : [thunk]; - return next => (...args) => { - const store = next(...args); - const methods = { + return (next: CreateStore) => (...args) => { + var store = next(...args); + var methods = { dispatch: store.dispatch, getState: store.getState }; diff --git a/src/utils/bindActionCreators.js b/src/utils/bindActionCreators.js index f9a81a5a80..05374c812c 100644 --- a/src/utils/bindActionCreators.js +++ b/src/utils/bindActionCreators.js @@ -1,6 +1,12 @@ +/* @flow */ + import mapValues from '../utils/mapValues'; -export default function bindActionCreators(actionCreators, dispatch) { +import type { Dispatch } from '../types'; + +export default function bindActionCreators( + actionCreators: Object, dispatch: Dispatch +): Object { return mapValues(actionCreators, actionCreator => (...args) => dispatch(actionCreator(...args)) ); diff --git a/src/utils/combineReducers.js b/src/utils/combineReducers.js index fe65c056cc..600e074e08 100644 --- a/src/utils/combineReducers.js +++ b/src/utils/combineReducers.js @@ -2,7 +2,9 @@ import mapValues from '../utils/mapValues'; import pick from '../utils/pick'; import invariant from 'invariant'; -function getErrorMessage(key, action) { +import type { Action, State, Reducer } from '../types'; + +function getErrorMessage(key: String, action: Action): String { const actionType = action && action.type; const actionName = actionType && `"${actionType}"` || 'an action'; const reducerName = `Reducer "${key}"`; @@ -21,10 +23,10 @@ function getErrorMessage(key, action) { ); } -export default function combineReducers(reducers) { +export default function combineReducers(reducers: Object): Reducer { const finalReducers = pick(reducers, (val) => typeof val === 'function'); - return function composition(state = {}, action) { + return function composition(state: State = {}, action: Action): State { return mapValues(finalReducers, (reducer, key) => { const newState = reducer(state[key], action); invariant( diff --git a/src/utils/compose.js b/src/utils/compose.js index 4db0884d3b..b4b291e015 100644 --- a/src/utils/compose.js +++ b/src/utils/compose.js @@ -1,8 +1,10 @@ +/* @flow */ + /** * Composes functions from left to right * @param {...Function} funcs - Functions to compose * @return {Function} */ -export default function compose(...funcs) { +export default function compose(...funcs: Array): Function { return funcs.reduceRight((composed, f) => f(composed)); } diff --git a/src/utils/createStoreShape.js b/src/utils/createStoreShape.js index 851e7ce898..f4e22159ed 100644 --- a/src/utils/createStoreShape.js +++ b/src/utils/createStoreShape.js @@ -1,4 +1,6 @@ -export default function createStoreShape(PropTypes) { +/* @flow */ + +export default function createStoreShape(PropTypes: Object): Object { return PropTypes.shape({ subscribe: PropTypes.func.isRequired, dispatch: PropTypes.func.isRequired, diff --git a/src/utils/identity.js b/src/utils/identity.js index 8c690a8859..ccab53fd9a 100644 --- a/src/utils/identity.js +++ b/src/utils/identity.js @@ -1,3 +1,5 @@ -export default function identity(value) { +/* @flow */ + +export default function identity(value: T): T { return value; } diff --git a/src/utils/isPlainObject.js b/src/utils/isPlainObject.js index a5845486cf..b31d8f37a3 100644 --- a/src/utils/isPlainObject.js +++ b/src/utils/isPlainObject.js @@ -1,3 +1,5 @@ -export default function isPlainObject(obj) { +/* @flow */ + +export default function isPlainObject(obj: Object): boolean { return obj ? typeof obj === 'object' && Object.getPrototypeOf(obj) === Object.prototype : false; } diff --git a/src/utils/mapValues.js b/src/utils/mapValues.js index 29d203cf61..9e09aeaab8 100644 --- a/src/utils/mapValues.js +++ b/src/utils/mapValues.js @@ -1,4 +1,6 @@ -export default function mapValues(obj, fn) { +/* @flow */ + +export default function mapValues(obj: Object, fn: Function): Object { return Object.keys(obj).reduce((result, key) => { result[key] = fn(obj[key], key); return result; diff --git a/src/utils/pick.js b/src/utils/pick.js index 2c9719c1c0..7025cb35e3 100644 --- a/src/utils/pick.js +++ b/src/utils/pick.js @@ -1,4 +1,6 @@ -export default function pick(obj, fn) { +/* @flow */ + +export default function pick(obj: Object, fn: Function): Object { return Object.keys(obj).reduce((result, key) => { if (fn(obj[key])) { result[key] = obj[key]; diff --git a/src/utils/shallowEqual.js b/src/utils/shallowEqual.js index f82be71949..8414ec3be1 100644 --- a/src/utils/shallowEqual.js +++ b/src/utils/shallowEqual.js @@ -1,18 +1,20 @@ -export default function shallowEqual(objA, objB) { +/* @flow */ + +export default function shallowEqual(objA: Object, objB: Object): boolean { if (objA === objB) { return true; } - const keysA = Object.keys(objA); - const keysB = Object.keys(objB); + var keysA = Object.keys(objA); + var keysB = Object.keys(objB); if (keysA.length !== keysB.length) { return false; } // Test for A's keys different from B. - const hasOwn = Object.prototype.hasOwnProperty; - for (let i = 0; i < keysA.length; i++) { + var hasOwn = Object.prototype.hasOwnProperty; + for (var i = 0; i < keysA.length; i++) { if (!hasOwn.call(objB, keysA[i]) || objA[keysA[i]] !== objB[keysA[i]]) { return false; diff --git a/src/utils/shallowEqualScalar.js b/src/utils/shallowEqualScalar.js index 2adb8ea85b..8f4f779a03 100644 --- a/src/utils/shallowEqualScalar.js +++ b/src/utils/shallowEqualScalar.js @@ -1,4 +1,6 @@ -export default function shallowEqualScalar(objA, objB) { +/* @flow */ + +export default function shallowEqualScalar(objA: Object, objB: Object): boolean { if (objA === objB) { return true; } @@ -8,22 +10,22 @@ export default function shallowEqualScalar(objA, objB) { return false; } - const keysA = Object.keys(objA); - const keysB = Object.keys(objB); + var keysA = Object.keys(objA); + var keysB = Object.keys(objB); if (keysA.length !== keysB.length) { return false; } // Test for A's keys different from B. - const hasOwn = Object.prototype.hasOwnProperty; - for (let i = 0; i < keysA.length; i++) { + var hasOwn = Object.prototype.hasOwnProperty; + for (var i = 0; i < keysA.length; i++) { if (!hasOwn.call(objB, keysA[i])) { return false; } - const valA = objA[keysA[i]]; - const valB = objB[keysA[i]]; + var valA = objA[keysA[i]]; + var valB = objB[keysA[i]]; if (valA !== valB || typeof valA === 'object' || typeof valB === 'object') { return false; From e85461771a28a47638e57a171b2f43c8dbef4eb9 Mon Sep 17 00:00:00 2001 From: Andrew Clark Date: Thu, 9 Jul 2015 23:44:57 -0700 Subject: [PATCH 17/84] Fix linting --- src/createStore.js | 2 +- src/utils/applyMiddleware.js | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/src/createStore.js b/src/createStore.js index 1bfca57b2f..5268cf5b27 100644 --- a/src/createStore.js +++ b/src/createStore.js @@ -3,7 +3,7 @@ import StoreClass from './Store'; import combineReducers from './utils/combineReducers'; -import type { State, Action, Reducer, Dispatch, Store } from './types'; +import type { State, Reducer, Store } from './types'; export default function createStore( reducer: Reducer, diff --git a/src/utils/applyMiddleware.js b/src/utils/applyMiddleware.js index a310afd63b..a147d863f3 100644 --- a/src/utils/applyMiddleware.js +++ b/src/utils/applyMiddleware.js @@ -4,7 +4,7 @@ import compose from './compose'; import composeMiddleware from './composeMiddleware'; import thunk from '../middleware/thunk'; -import type { Middleware, Dispatch, CreateStore } from '../types'; +import type { Dispatch, CreateStore } from '../types'; /** * Creates a higher-order store that applies middleware to a store's dispatch. From c46494b024ca7fcba12686ff8737a03953101ccb Mon Sep 17 00:00:00 2001 From: Dan Abramov Date: Sat, 11 Jul 2015 23:36:11 +0300 Subject: [PATCH 18/84] Remove React-specific code in favor of gaearon/redux-react --- .gitignore | 2 - examples/counter/containers/App.js | 2 +- examples/counter/containers/CounterApp.js | 2 +- examples/counter/package.json | 3 +- examples/todomvc/containers/App.js | 2 +- examples/todomvc/containers/TodoApp.js | 2 +- examples/todomvc/package.json | 3 +- package.json | 2 - scripts/browser | 7 +- scripts/build | 3 - src/components/createAll.js | 17 -- src/components/createConnectDecorator.js | 25 -- src/components/createConnector.js | 89 ------- src/components/createProvideDecorator.js | 20 -- src/components/createProvider.js | 40 --- src/entry-react-native.js | 1 - src/entry-react.js | 1 - src/react-native.js | 4 - src/react.js | 4 - src/umd-react.js | 2 - src/umd.js | 1 - src/utils/createStoreShape.js | 7 - src/utils/getDisplayName.js | 3 - src/utils/identity.js | 3 - src/utils/shallowEqual.js | 23 -- src/utils/shallowEqualScalar.js | 34 --- test/components/Connector.spec.js | 295 ---------------------- test/components/Provider.spec.js | 71 ------ test/components/connect.spec.js | 154 ----------- test/components/jsdomReact.js | 7 - test/components/provide.spec.js | 68 ----- test/getDisplayName.spec.js | 17 -- test/utils/identity.spec.js | 11 - test/utils/shallowEquality.spec.js | 133 ---------- webpack.config.js | 11 - 35 files changed, 10 insertions(+), 1059 deletions(-) delete mode 100644 src/components/createAll.js delete mode 100644 src/components/createConnectDecorator.js delete mode 100644 src/components/createConnector.js delete mode 100644 src/components/createProvideDecorator.js delete mode 100644 src/components/createProvider.js delete mode 100644 src/entry-react-native.js delete mode 100644 src/entry-react.js delete mode 100644 src/react-native.js delete mode 100644 src/react.js delete mode 100644 src/umd-react.js delete mode 100644 src/umd.js delete mode 100644 src/utils/createStoreShape.js delete mode 100644 src/utils/getDisplayName.js delete mode 100644 src/utils/identity.js delete mode 100644 src/utils/shallowEqual.js delete mode 100644 src/utils/shallowEqualScalar.js delete mode 100644 test/components/Connector.spec.js delete mode 100644 test/components/Provider.spec.js delete mode 100644 test/components/connect.spec.js delete mode 100644 test/components/jsdomReact.js delete mode 100644 test/components/provide.spec.js delete mode 100644 test/getDisplayName.spec.js delete mode 100644 test/utils/identity.spec.js delete mode 100644 test/utils/shallowEquality.spec.js diff --git a/.gitignore b/.gitignore index 37d08f874a..dbb9d4c83b 100644 --- a/.gitignore +++ b/.gitignore @@ -4,5 +4,3 @@ npm-debug.log dist lib coverage -react.js -react-native.js diff --git a/examples/counter/containers/App.js b/examples/counter/containers/App.js index 28ed8049b6..72912dcf40 100644 --- a/examples/counter/containers/App.js +++ b/examples/counter/containers/App.js @@ -1,7 +1,7 @@ import React from 'react'; import CounterApp from './CounterApp'; import { createStore, applyMiddleware } from 'redux'; -import { Provider } from 'redux/react'; +import { Provider } from 'redux-react'; import * as reducers from '../reducers'; const createStoreWithMiddleware = applyMiddleware()(createStore); diff --git a/examples/counter/containers/CounterApp.js b/examples/counter/containers/CounterApp.js index f60d74c746..93adc324e3 100644 --- a/examples/counter/containers/CounterApp.js +++ b/examples/counter/containers/CounterApp.js @@ -1,6 +1,6 @@ import React from 'react'; import { bindActionCreators } from 'redux'; -import { connect } from 'redux/react'; +import { connect } from 'redux-react'; import Counter from '../components/Counter'; import * as CounterActions from '../actions/CounterActions'; diff --git a/examples/counter/package.json b/examples/counter/package.json index 4e5c64f72c..c60d894704 100644 --- a/examples/counter/package.json +++ b/examples/counter/package.json @@ -28,7 +28,8 @@ "homepage": "https://github.com/gaearon/redux#readme", "dependencies": { "react": "^0.13.3", - "redux": "^0.12.0" + "redux": "^0.12.0", + "redux-react": "^1.0.0" }, "devDependencies": { "babel-core": "^5.5.8", diff --git a/examples/todomvc/containers/App.js b/examples/todomvc/containers/App.js index 1609f0ad33..ccbabbb819 100644 --- a/examples/todomvc/containers/App.js +++ b/examples/todomvc/containers/App.js @@ -1,7 +1,7 @@ import React from 'react'; import TodoApp from './TodoApp'; import { createStore } from 'redux'; -import { Provider } from 'redux/react'; +import { Provider } from 'redux-react'; import * as reducers from '../reducers'; const store = createStore(reducers); diff --git a/examples/todomvc/containers/TodoApp.js b/examples/todomvc/containers/TodoApp.js index 03df600025..8edc850a6e 100644 --- a/examples/todomvc/containers/TodoApp.js +++ b/examples/todomvc/containers/TodoApp.js @@ -1,6 +1,6 @@ import React from 'react'; import { bindActionCreators } from 'redux'; -import { Connector } from 'redux/react'; +import { Connector } from 'redux-react'; import Header from '../components/Header'; import MainSection from '../components/MainSection'; import * as TodoActions from '../actions/TodoActions'; diff --git a/examples/todomvc/package.json b/examples/todomvc/package.json index 58e7ea36d0..8fc0b1d513 100644 --- a/examples/todomvc/package.json +++ b/examples/todomvc/package.json @@ -30,7 +30,8 @@ "dependencies": { "classnames": "^2.1.2", "react": "^0.13.3", - "redux": "^0.12.0" + "redux": "^0.12.0", + "redux-react": "^1.0.0" }, "devDependencies": { "babel-core": "^5.5.8", diff --git a/package.json b/package.json index 1646effc1a..2183da21fe 100644 --- a/package.json +++ b/package.json @@ -44,9 +44,7 @@ "eslint-plugin-react": "^2.3.0", "expect": "^1.6.0", "istanbul": "^0.3.15", - "jsdom": "~5.4.3", "mocha": "^2.2.5", - "mocha-jsdom": "~0.4.0", "react": "^0.13.0", "react-hot-loader": "^1.2.7", "rimraf": "^2.3.4", diff --git a/scripts/browser b/scripts/browser index a714807862..22bad9a9fe 100755 --- a/scripts/browser +++ b/scripts/browser @@ -4,8 +4,5 @@ WEBPACK_CMD=node_modules/.bin/webpack mkdir -p dist -$WEBPACK_CMD src/umd.js dist/redux.js -NODE_ENV=production $WEBPACK_CMD src/umd.js dist/redux.min.js - -$WEBPACK_CMD src/umd-react.js dist/redux-react.js -NODE_ENV=production $WEBPACK_CMD src/umd-react.js dist/redux-react.min.js +$WEBPACK_CMD src/index.js dist/redux.js +NODE_ENV=production $WEBPACK_CMD src/index.js dist/redux.min.js diff --git a/scripts/build b/scripts/build index 8ae8463bad..50a3a47dcd 100755 --- a/scripts/build +++ b/scripts/build @@ -2,6 +2,3 @@ rm -rf lib `npm bin`/babel src --out-dir lib - -mv lib/entry-react.js ./react.js -mv lib/entry-react-native.js ./react-native.js diff --git a/src/components/createAll.js b/src/components/createAll.js deleted file mode 100644 index bd6383cccf..0000000000 --- a/src/components/createAll.js +++ /dev/null @@ -1,17 +0,0 @@ -import createProvider from './createProvider'; -import createProvideDecorator from './createProvideDecorator'; - -import createConnector from './createConnector'; -import createConnectDecorator from './createConnectDecorator'; - -export default function createAll(React) { - // Wrapper components - const Provider = createProvider(React); - const Connector = createConnector(React); - - // Higher-order components (decorators) - const provide = createProvideDecorator(React, Provider); - const connect = createConnectDecorator(React, Connector); - - return { Provider, Connector, provide, connect }; -} diff --git a/src/components/createConnectDecorator.js b/src/components/createConnectDecorator.js deleted file mode 100644 index a3196e1c16..0000000000 --- a/src/components/createConnectDecorator.js +++ /dev/null @@ -1,25 +0,0 @@ -import getDisplayName from '../utils/getDisplayName'; -import shallowEqualScalar from '../utils/shallowEqualScalar'; - -export default function createConnectDecorator(React, Connector) { - const { Component } = React; - - return function connect(select) { - return DecoratedComponent => class ConnectorDecorator extends Component { - static displayName = `Connector(${getDisplayName(DecoratedComponent)})`; - static DecoratedComponent = DecoratedComponent; - - shouldComponentUpdate(nextProps) { - return !shallowEqualScalar(this.props, nextProps); - } - - render() { - return ( - select(state, this.props)}> - {stuff => } - - ); - } - }; - }; -} diff --git a/src/components/createConnector.js b/src/components/createConnector.js deleted file mode 100644 index 8bfddd4da6..0000000000 --- a/src/components/createConnector.js +++ /dev/null @@ -1,89 +0,0 @@ -import createStoreShape from '../utils/createStoreShape'; -import identity from '../utils/identity'; -import shallowEqual from '../utils/shallowEqual'; -import isPlainObject from '../utils/isPlainObject'; -import invariant from 'invariant'; - -export default function createConnector(React) { - const { Component, PropTypes } = React; - const storeShape = createStoreShape(PropTypes); - - return class Connector extends Component { - static contextTypes = { - store: storeShape.isRequired - }; - - static propTypes = { - children: PropTypes.func.isRequired, - select: PropTypes.func.isRequired - }; - - static defaultProps = { - select: identity - }; - - shouldComponentUpdate(nextProps, nextState) { - return !this.isSliceEqual(this.state.slice, nextState.slice) || - !shallowEqual(this.props, nextProps); - } - - isSliceEqual(slice, nextSlice) { - const isRefEqual = slice === nextSlice; - if (isRefEqual) { - return true; - } else if (typeof slice !== 'object' || typeof nextSlice !== 'object') { - return isRefEqual; - } - return shallowEqual(slice, nextSlice); - } - - constructor(props, context) { - super(props, context); - this.state = this.selectState(props, context); - } - - componentDidMount() { - this.unsubscribe = this.context.store.subscribe(::this.handleChange); - this.handleChange(); - } - - componentWillReceiveProps(nextProps) { - if (nextProps.select !== this.props.select) { - // Force the state slice recalculation - this.handleChange(nextProps); - } - } - - componentWillUnmount() { - this.unsubscribe(); - } - - handleChange(props = this.props) { - const nextState = this.selectState(props, this.context); - if (!this.isSliceEqual(this.state.slice, nextState.slice)) { - this.setState(nextState); - } - } - - selectState(props, context) { - const state = context.store.getState(); - const slice = props.select(state); - - invariant( - isPlainObject(slice), - 'The return value of `select` prop must be an object. Instead received %s.', - slice - ); - - return { slice }; - } - - render() { - const { children } = this.props; - const { slice } = this.state; - const { store: { dispatch } } = this.context; - - return children({ dispatch, ...slice }); - } - }; -} diff --git a/src/components/createProvideDecorator.js b/src/components/createProvideDecorator.js deleted file mode 100644 index d181865a40..0000000000 --- a/src/components/createProvideDecorator.js +++ /dev/null @@ -1,20 +0,0 @@ -import getDisplayName from '../utils/getDisplayName'; - -export default function createProvideDecorator(React, Provider) { - const { Component } = React; - - return function provide(store) { - return DecoratedComponent => class ProviderDecorator extends Component { - static displayName = `Provider(${getDisplayName(DecoratedComponent)})`; - static DecoratedComponent = DecoratedComponent; - - render() { - return ( - - {() => } - - ); - } - }; - }; -} diff --git a/src/components/createProvider.js b/src/components/createProvider.js deleted file mode 100644 index 030c8e2ac7..0000000000 --- a/src/components/createProvider.js +++ /dev/null @@ -1,40 +0,0 @@ -import createStoreShape from '../utils/createStoreShape'; - -export default function createProvider(React) { - const { Component, PropTypes } = React; - const storeShape = createStoreShape(PropTypes); - - return class Provider extends Component { - static childContextTypes = { - store: storeShape.isRequired - }; - - static propTypes = { - children: PropTypes.func.isRequired - }; - - getChildContext() { - return { store: this.state.store }; - } - - constructor(props, context) { - super(props, context); - this.state = { store: props.store }; - } - - componentWillReceiveProps(nextProps) { - const { store } = this.state; - const { store: nextStore } = nextProps; - - if (store !== nextStore) { - const nextReducer = nextStore.getReducer(); - store.replaceReducer(nextReducer); - } - } - - render() { - const { children } = this.props; - return children(); - } - }; -} diff --git a/src/entry-react-native.js b/src/entry-react-native.js deleted file mode 100644 index 981975b461..0000000000 --- a/src/entry-react-native.js +++ /dev/null @@ -1 +0,0 @@ -export * from './lib/react-native'; diff --git a/src/entry-react.js b/src/entry-react.js deleted file mode 100644 index 241d66113b..0000000000 --- a/src/entry-react.js +++ /dev/null @@ -1 +0,0 @@ -export * from './lib/react'; diff --git a/src/react-native.js b/src/react-native.js deleted file mode 100644 index c6fc5363e0..0000000000 --- a/src/react-native.js +++ /dev/null @@ -1,4 +0,0 @@ -import React from 'react-native'; -import createAll from './components/createAll'; - -export const { Provider, Connector, provide, connect } = createAll(React); diff --git a/src/react.js b/src/react.js deleted file mode 100644 index a2058d695a..0000000000 --- a/src/react.js +++ /dev/null @@ -1,4 +0,0 @@ -import React from 'react'; -import createAll from './components/createAll'; - -export const { Provider, Connector, provide, connect } = createAll(React); diff --git a/src/umd-react.js b/src/umd-react.js deleted file mode 100644 index 88a5f4b408..0000000000 --- a/src/umd-react.js +++ /dev/null @@ -1,2 +0,0 @@ -export * from './index'; -export * from './react'; diff --git a/src/umd.js b/src/umd.js deleted file mode 100644 index ea465c2a34..0000000000 --- a/src/umd.js +++ /dev/null @@ -1 +0,0 @@ -export * from './index'; diff --git a/src/utils/createStoreShape.js b/src/utils/createStoreShape.js deleted file mode 100644 index 851e7ce898..0000000000 --- a/src/utils/createStoreShape.js +++ /dev/null @@ -1,7 +0,0 @@ -export default function createStoreShape(PropTypes) { - return PropTypes.shape({ - subscribe: PropTypes.func.isRequired, - dispatch: PropTypes.func.isRequired, - getState: PropTypes.func.isRequired - }); -} diff --git a/src/utils/getDisplayName.js b/src/utils/getDisplayName.js deleted file mode 100644 index 512702c87a..0000000000 --- a/src/utils/getDisplayName.js +++ /dev/null @@ -1,3 +0,0 @@ -export default function getDisplayName(Component) { - return Component.displayName || Component.name || 'Component'; -} diff --git a/src/utils/identity.js b/src/utils/identity.js deleted file mode 100644 index 8c690a8859..0000000000 --- a/src/utils/identity.js +++ /dev/null @@ -1,3 +0,0 @@ -export default function identity(value) { - return value; -} diff --git a/src/utils/shallowEqual.js b/src/utils/shallowEqual.js deleted file mode 100644 index f82be71949..0000000000 --- a/src/utils/shallowEqual.js +++ /dev/null @@ -1,23 +0,0 @@ -export default function shallowEqual(objA, objB) { - if (objA === objB) { - return true; - } - - const keysA = Object.keys(objA); - const keysB = Object.keys(objB); - - if (keysA.length !== keysB.length) { - return false; - } - - // Test for A's keys different from B. - const hasOwn = Object.prototype.hasOwnProperty; - for (let i = 0; i < keysA.length; i++) { - if (!hasOwn.call(objB, keysA[i]) || - objA[keysA[i]] !== objB[keysA[i]]) { - return false; - } - } - - return true; -} diff --git a/src/utils/shallowEqualScalar.js b/src/utils/shallowEqualScalar.js deleted file mode 100644 index 2adb8ea85b..0000000000 --- a/src/utils/shallowEqualScalar.js +++ /dev/null @@ -1,34 +0,0 @@ -export default function shallowEqualScalar(objA, objB) { - if (objA === objB) { - return true; - } - - if (typeof objA !== 'object' || objA === null || - typeof objB !== 'object' || objB === null) { - return false; - } - - const keysA = Object.keys(objA); - const keysB = Object.keys(objB); - - if (keysA.length !== keysB.length) { - return false; - } - - // Test for A's keys different from B. - const hasOwn = Object.prototype.hasOwnProperty; - for (let i = 0; i < keysA.length; i++) { - if (!hasOwn.call(objB, keysA[i])) { - return false; - } - - const valA = objA[keysA[i]]; - const valB = objB[keysA[i]]; - - if (valA !== valB || typeof valA === 'object' || typeof valB === 'object') { - return false; - } - } - - return true; -} diff --git a/test/components/Connector.spec.js b/test/components/Connector.spec.js deleted file mode 100644 index bdc13b1c54..0000000000 --- a/test/components/Connector.spec.js +++ /dev/null @@ -1,295 +0,0 @@ -import expect from 'expect'; -import jsdomReact from './jsdomReact'; -import React, { PropTypes, Component } from 'react/addons'; -import { createStore } from '../../src'; -import { Connector } from '../../src/react'; - -const { TestUtils } = React.addons; - -describe('React', () => { - describe('Connector', () => { - jsdomReact(); - - // Mock minimal Provider interface - class Provider extends Component { - static childContextTypes = { - store: PropTypes.object.isRequired - } - - getChildContext() { - return { store: this.props.store }; - } - - render() { - return this.props.children(); - } - } - - function stringBuilder(prev = '', action) { - return action.type === 'APPEND' - ? prev + action.body - : prev; - } - - it('should receive the store in the context', () => { - const store = createStore({}); - - const tree = TestUtils.renderIntoDocument( - - {() => ( - - {() =>
} - - )} - - ); - - const connector = TestUtils.findRenderedComponentWithType(tree, Connector); - expect(connector.context.store).toBe(store); - }); - - it('should subscribe to the store changes', () => { - const store = createStore(stringBuilder); - - const tree = TestUtils.renderIntoDocument( - - {() => ( - ({ string })}> - {({ string }) =>
} - - )} - - ); - - const div = TestUtils.findRenderedDOMComponentWithTag(tree, 'div'); - expect(div.props.string).toBe(''); - store.dispatch({ type: 'APPEND', body: 'a'}); - expect(div.props.string).toBe('a'); - store.dispatch({ type: 'APPEND', body: 'b'}); - expect(div.props.string).toBe('ab'); - }); - - it('should unsubscribe before unmounting', () => { - const store = createStore(stringBuilder); - const subscribe = store.subscribe; - - // Keep track of unsubscribe by wrapping subscribe() - const spy = expect.createSpy(() => {}); - store.subscribe = (listener) => { - const unsubscribe = subscribe(listener); - return () => { - spy(); - return unsubscribe(); - }; - }; - - const tree = TestUtils.renderIntoDocument( - - {() => ( - ({ string })}> - {({ string }) =>
} - - )} - - ); - - const connector = TestUtils.findRenderedComponentWithType(tree, Connector); - expect(spy.calls.length).toBe(0); - connector.componentWillUnmount(); - expect(spy.calls.length).toBe(1); - }); - - it('should shallowly compare the selected state to prevent unnecessary updates', () => { - const store = createStore(stringBuilder); - const spy = expect.createSpy(() => {}); - function render({ string }) { - spy(); - return
; - } - - const tree = TestUtils.renderIntoDocument( - - {() => ( - ({ string })}> - {render} - - )} - - ); - - const div = TestUtils.findRenderedDOMComponentWithTag(tree, 'div'); - expect(spy.calls.length).toBe(1); - expect(div.props.string).toBe(''); - store.dispatch({ type: 'APPEND', body: 'a'}); - expect(spy.calls.length).toBe(2); - store.dispatch({ type: 'APPEND', body: 'b'}); - expect(spy.calls.length).toBe(3); - store.dispatch({ type: 'APPEND', body: ''}); - expect(spy.calls.length).toBe(3); - }); - - it('should recompute the state slice when the select prop changes', () => { - const store = createStore({ - a: () => 42, - b: () => 72 - }); - - function selectA(state) { - return { result: state.a }; - } - - function selectB(state) { - return { result: state.b }; - } - - function render({ result }) { - return
{result}
; - } - - class Container extends Component { - constructor() { - super(); - this.state = { select: selectA }; - } - - render() { - return ( - - {() => - - {render} - - } - - ); - } - } - - let tree = TestUtils.renderIntoDocument(); - let div = TestUtils.findRenderedDOMComponentWithTag(tree, 'div'); - expect(div.props.children).toBe(42); - - tree.setState({ select: selectB }); - expect(div.props.children).toBe(72); - }); - - it('should pass dispatch() to the child function', () => { - const store = createStore({}); - - const tree = TestUtils.renderIntoDocument( - - {() => ( - - {({ dispatch }) =>
} - - )} - - ); - - const div = TestUtils.findRenderedDOMComponentWithTag(tree, 'div'); - expect(div.props.dispatch).toBe(store.dispatch); - }); - - it('should throw an error if select returns anything but a plain object', () => { - const store = createStore({}); - - expect(() => { - TestUtils.renderIntoDocument( - - {() => ( - 1}> - {() =>
} - - )} - - ); - }).toThrow(/select/); - - expect(() => { - TestUtils.renderIntoDocument( - - {() => ( - 'hey'}> - {() =>
} - - )} - - ); - }).toThrow(/select/); - - function AwesomeMap() { } - - expect(() => { - TestUtils.renderIntoDocument( - - {() => ( - new AwesomeMap()}> - {() =>
} - - )} - - ); - }).toThrow(/select/); - }); - - it('should not setState when renderToString is called on the server', () => { - const { renderToString } = React; - const store = createStore(stringBuilder); - - class TestComp extends Component { - componentWillMount() { - store.dispatch({ - type: 'APPEND', - body: 'a' - }); - } - - render() { - return
{this.props.string}
; - } - } - - const el = ( - - {() => ( - ({ string })}> - {({ string }) => } - - )} - - ); - - expect(() => renderToString(el)).toNotThrow(); - }); - - it('should handle dispatch inside componentDidMount', () => { - const store = createStore(stringBuilder); - - class TestComp extends Component { - componentDidMount() { - store.dispatch({ - type: 'APPEND', - body: 'a' - }); - } - - render() { - return
{this.props.string}
; - } - } - - const tree = TestUtils.renderIntoDocument( - - {() => ( - ({ string })}> - {({ string }) => } - - )} - - ); - - const testComp = TestUtils.findRenderedComponentWithType(tree, TestComp); - expect(testComp.props.string).toBe('a'); - }); - }); -}); diff --git a/test/components/Provider.spec.js b/test/components/Provider.spec.js deleted file mode 100644 index 3e3b3c3308..0000000000 --- a/test/components/Provider.spec.js +++ /dev/null @@ -1,71 +0,0 @@ -import expect from 'expect'; -import jsdomReact from './jsdomReact'; -import React, { PropTypes, Component } from 'react/addons'; -import { createStore } from '../../src'; -import { Provider } from '../../src/react'; - -const { TestUtils } = React.addons; - -describe('React', () => { - describe('Provider', () => { - jsdomReact(); - - class Child extends Component { - static contextTypes = { - store: PropTypes.object.isRequired - } - - render() { - return
; - } - } - - it('should add the store to the child context', () => { - const store = createStore({}); - - const tree = TestUtils.renderIntoDocument( - - {() => } - - ); - - const child = TestUtils.findRenderedComponentWithType(tree, Child); - expect(child.context.store).toBe(store); - }); - - it('should replace just the reducer when receiving a new store in props', () => { - const store1 = createStore((state = 10) => state + 1); - const store2 = createStore((state = 10) => state * 2); - const spy = expect.createSpy(() => {}); - - class ProviderContainer extends Component { - state = { store: store1 }; - - render() { - return ( - - {() => } - - ); - } - } - - const container = TestUtils.renderIntoDocument(); - const child = TestUtils.findRenderedComponentWithType(container, Child); - expect(child.context.store.getState()).toEqual(11); - - child.context.store.subscribe(spy); - child.context.store.dispatch({}); - expect(spy.calls.length).toEqual(1); - expect(child.context.store.getState()).toEqual(12); - - container.setState({ store: store2 }); - expect(spy.calls.length).toEqual(2); - expect(child.context.store.getState()).toEqual(24); - - child.context.store.dispatch({}); - expect(spy.calls.length).toEqual(3); - expect(child.context.store.getState()).toEqual(48); - }); - }); -}); diff --git a/test/components/connect.spec.js b/test/components/connect.spec.js deleted file mode 100644 index 5b3fee90c2..0000000000 --- a/test/components/connect.spec.js +++ /dev/null @@ -1,154 +0,0 @@ -import expect from 'expect'; -import jsdomReact from './jsdomReact'; -import React, { PropTypes, Component } from 'react/addons'; -import { createStore } from '../../src'; -import { connect, Connector } from '../../src/react'; - -const { TestUtils } = React.addons; - -describe('React', () => { - describe('connect', () => { - jsdomReact(); - - // Mock minimal Provider interface - class Provider extends Component { - static childContextTypes = { - store: PropTypes.object.isRequired - } - - getChildContext() { - return { store: this.props.store }; - } - - render() { - return this.props.children(); - } - } - - it('should wrap the component into Provider', () => { - const store = createStore(() => ({ - foo: 'bar' - })); - - @connect(state => state) - class Container extends Component { - render() { - return
; - } - } - - const container = TestUtils.renderIntoDocument( - - {() => } - - ); - const div = TestUtils.findRenderedDOMComponentWithTag(container, 'div'); - expect(div.props.pass).toEqual('through'); - expect(div.props.foo).toEqual('bar'); - expect(() => - TestUtils.findRenderedComponentWithType(container, Connector) - ).toNotThrow(); - }); - - it('should handle additional prop changes in addition to slice', () => { - const store = createStore(() => ({ - foo: 'bar' - })); - - @connect(state => state) - class ConnectContainer extends Component { - render() { - return ( -
- ); - } - } - - class Container extends Component { - constructor() { - super(); - this.state = { - bar: { - baz: '' - } - }; - } - componentDidMount() { - - // Simulate deep object mutation - this.state.bar.baz = 'through'; - this.setState({ - bar: this.state.bar - }); - } - render() { - return ( - - {() => } - - ); - } - } - - const container = TestUtils.renderIntoDocument(); - const div = TestUtils.findRenderedDOMComponentWithTag(container, 'div'); - expect(div.props.foo).toEqual('bar'); - expect(div.props.pass).toEqual('through'); - }); - - it('should pass the only argument as the select prop down', () => { - const store = createStore(() => ({ - foo: 'baz', - bar: 'baz' - })); - - function select({ foo }) { - return { foo }; - } - - @connect(select) - class Container extends Component { - render() { - return
; - } - } - - const container = TestUtils.renderIntoDocument( - - {() => } - - ); - const connector = TestUtils.findRenderedComponentWithType(container, Connector); - expect(connector.props.select({ - foo: 5, - bar: 7 - })).toEqual({ - foo: 5 - }); - }); - - it('should set the displayName correctly', () => { - @connect(state => state) - class Container extends Component { - render() { - return
; - } - } - - expect(Container.displayName).toBe('Connector(Container)'); - }); - - it('should expose the wrapped component as DecoratedComponent', () => { - class Container extends Component { - render() { - return
; - } - } - - const decorator = connect(state => state); - const decorated = decorator(Container); - - expect(decorated.DecoratedComponent).toBe(Container); - }); - }); -}); diff --git a/test/components/jsdomReact.js b/test/components/jsdomReact.js deleted file mode 100644 index 0083824baf..0000000000 --- a/test/components/jsdomReact.js +++ /dev/null @@ -1,7 +0,0 @@ -import ExecutionEnvironment from 'react/lib/ExecutionEnvironment'; -import jsdom from 'mocha-jsdom'; - -export default function jsdomReact() { - jsdom(); - ExecutionEnvironment.canUseDOM = true; -} diff --git a/test/components/provide.spec.js b/test/components/provide.spec.js deleted file mode 100644 index babef47758..0000000000 --- a/test/components/provide.spec.js +++ /dev/null @@ -1,68 +0,0 @@ -import expect from 'expect'; -import jsdomReact from './jsdomReact'; -import React, { PropTypes, Component } from 'react/addons'; -import { createStore } from '../../src'; -import { provide, Provider } from '../../src/react'; - -const { TestUtils } = React.addons; - -describe('React', () => { - describe('provide', () => { - jsdomReact(); - - class Child extends Component { - static contextTypes = { - store: PropTypes.object.isRequired - } - - render() { - return
; - } - } - - it('should wrap the component into Provider', () => { - const store = createStore({}); - - @provide(store) - class Container extends Component { - render() { - return ; - } - } - - const container = TestUtils.renderIntoDocument( - - ); - const child = TestUtils.findRenderedComponentWithType(container, Child); - expect(child.props.pass).toEqual('through'); - expect(() => - TestUtils.findRenderedComponentWithType(container, Provider) - ).toNotThrow(); - expect(child.context.store).toBe(store); - }); - - it('sets the displayName correctly', () => { - @provide(createStore({})) - class Container extends Component { - render() { - return
; - } - } - - expect(Container.displayName).toBe('Provider(Container)'); - }); - - it('should expose the wrapped component as DecoratedComponent', () => { - class Container extends Component { - render() { - return
; - } - } - - const decorator = provide(state => state); - const decorated = decorator(Container); - - expect(decorated.DecoratedComponent).toBe(Container); - }); - }); -}); diff --git a/test/getDisplayName.spec.js b/test/getDisplayName.spec.js deleted file mode 100644 index a3a712d345..0000000000 --- a/test/getDisplayName.spec.js +++ /dev/null @@ -1,17 +0,0 @@ -import expect from 'expect'; -import { createClass, Component } from 'react'; -import getDisplayName from '../src/utils/getDisplayName'; - -describe('Utils', () => { - describe('getDisplayName', () => { - it('should extract the component class name', () => { - const names = [ - createClass({ displayName: 'Foo', render() {} }), - class Bar extends Component {}, - createClass({ render() {} }) - ].map(getDisplayName); - - expect(names).toEqual(['Foo', 'Bar', 'Component']); - }); - }); -}); diff --git a/test/utils/identity.spec.js b/test/utils/identity.spec.js deleted file mode 100644 index ef48e5593c..0000000000 --- a/test/utils/identity.spec.js +++ /dev/null @@ -1,11 +0,0 @@ -import expect from 'expect'; -import identity from '../../src/utils/identity'; - -describe('Utils', () => { - describe('identity', () => { - it('should return the first argument passed to it', () => { - const test = { a: 1 }; - expect(identity(test, 'test')).toBe(test); - }); - }); -}); diff --git a/test/utils/shallowEquality.spec.js b/test/utils/shallowEquality.spec.js deleted file mode 100644 index 7146185bfb..0000000000 --- a/test/utils/shallowEquality.spec.js +++ /dev/null @@ -1,133 +0,0 @@ -import expect from 'expect'; -import shallowEqualScalar from '../../src/utils/shallowEqualScalar'; -import shallowEqual from '../../src/utils/shallowEqual'; - -describe('Utils', () => { - // More info: https://github.com/gaearon/redux/pull/75#issuecomment-111635748 - describe('shallowEqualScalar', () => { - it('should return true if both arguments are the same object', () => { - const o = { a: 1, b: 2 }; - expect(shallowEqualScalar(o, o)).toBe(true); - }); - - it('should return false if either argument is null', () => { - expect(shallowEqualScalar(null, {})).toBe(false); - expect(shallowEqualScalar({}, null)).toBe(false); - }); - - it('should return true if arguments fields are equal', () => { - expect( - shallowEqualScalar( - { a: 1, b: 2, c: undefined }, - { a: 1, b: 2, c: undefined } - ) - ).toBe(true); - - expect( - shallowEqualScalar( - { a: 1, b: 2, c: 3 }, - { a: 1, b: 2, c: 3 } - ) - ).toBe(true); - }); - - it('should return false if first argument has too many keys', () => { - expect( - shallowEqualScalar( - { a: 1, b: 2, c: 3 }, - { a: 1, b: 2 } - ) - ).toBe(false); - }); - - it('should return false if second argument has too many keys', () => { - expect( - shallowEqualScalar( - { a: 1, b: 2 }, - { a: 1, b: 2, c: 3 } - ) - ).toBe(false); - }); - - it('should return false if arguments have keys dont have same value', () => { - expect( - shallowEqualScalar( - { a: 1, b: 2 }, - { a: 1, b: 3 } - ) - ).toBe(false); - }); - - it('should return false if arguments have field that are objects', () => { - const o = {}; - expect( - shallowEqualScalar( - { a: 1, b: 2, c: o }, - { a: 1, b: 2, c: o } - ) - ).toBe(false); - }); - - it('should return false if arguments have different keys', () => { - expect( - shallowEqualScalar( - { a: 1, b: 2, c: undefined }, - { a: 1, bb: 2, c: undefined } - ) - ).toBe(false); - }); - }); - - describe('shallowEqual', () => { - it('should return true if arguments fields are equal', () => { - expect( - shallowEqual( - { a: 1, b: 2, c: undefined }, - { a: 1, b: 2, c: undefined } - ) - ).toBe(true); - - expect( - shallowEqual( - { a: 1, b: 2, c: 3 }, - { a: 1, b: 2, c: 3 } - ) - ).toBe(true); - - const o = {}; - expect( - shallowEqual( - { a: 1, b: 2, c: o }, - { a: 1, b: 2, c: o } - ) - ).toBe(true); - }); - - it('should return false if first argument has too many keys', () => { - expect( - shallowEqual( - { a: 1, b: 2, c: 3 }, - { a: 1, b: 2 } - ) - ).toBe(false); - }); - - it('should return false if second argument has too many keys', () => { - expect( - shallowEqual( - { a: 1, b: 2 }, - { a: 1, b: 2, c: 3 } - ) - ).toBe(false); - }); - - it('should return false if arguments have different keys', () => { - expect( - shallowEqual( - { a: 1, b: 2, c: undefined }, - { a: 1, bb: 2, c: undefined } - ) - ).toBe(false); - }); - }); -}); diff --git a/webpack.config.js b/webpack.config.js index d7c5f5a61c..a1da852834 100644 --- a/webpack.config.js +++ b/webpack.config.js @@ -20,18 +20,7 @@ if (process.env.NODE_ENV === 'production') { ); } -var reactExternal = { - root: 'React', - commonjs2: 'react', - commonjs: 'react', - amd: 'react' -}; - module.exports = { - externals: { - 'react': reactExternal, - 'react-native': reactExternal - }, module: { loaders: [ { test: /\.js$/, loaders: ['babel-loader'], exclude: /node_modules/ } From ab347bfff95dd93e70561eb87a16fe34349a87b5 Mon Sep 17 00:00:00 2001 From: Dan Abramov Date: Sun, 12 Jul 2015 00:49:39 +0300 Subject: [PATCH 19/84] Bump the versions --- examples/counter/containers/App.js | 2 +- examples/counter/containers/CounterApp.js | 2 +- examples/counter/package.json | 4 ++-- examples/todomvc/containers/App.js | 2 +- examples/todomvc/containers/TodoApp.js | 2 +- examples/todomvc/package.json | 4 ++-- package.json | 2 +- 7 files changed, 9 insertions(+), 9 deletions(-) diff --git a/examples/counter/containers/App.js b/examples/counter/containers/App.js index 72912dcf40..77ca8c0508 100644 --- a/examples/counter/containers/App.js +++ b/examples/counter/containers/App.js @@ -1,7 +1,7 @@ import React from 'react'; import CounterApp from './CounterApp'; import { createStore, applyMiddleware } from 'redux'; -import { Provider } from 'redux-react'; +import { Provider } from 'react-redux'; import * as reducers from '../reducers'; const createStoreWithMiddleware = applyMiddleware()(createStore); diff --git a/examples/counter/containers/CounterApp.js b/examples/counter/containers/CounterApp.js index 93adc324e3..f2a9fa4219 100644 --- a/examples/counter/containers/CounterApp.js +++ b/examples/counter/containers/CounterApp.js @@ -1,6 +1,6 @@ import React from 'react'; import { bindActionCreators } from 'redux'; -import { connect } from 'redux-react'; +import { connect } from 'react-redux'; import Counter from '../components/Counter'; import * as CounterActions from '../actions/CounterActions'; diff --git a/examples/counter/package.json b/examples/counter/package.json index c60d894704..9206cb430a 100644 --- a/examples/counter/package.json +++ b/examples/counter/package.json @@ -28,8 +28,8 @@ "homepage": "https://github.com/gaearon/redux#readme", "dependencies": { "react": "^0.13.3", - "redux": "^0.12.0", - "redux-react": "^1.0.0" + "redux": "^1.0.0-alpha", + "react-redux": "^1.0.0-alpha" }, "devDependencies": { "babel-core": "^5.5.8", diff --git a/examples/todomvc/containers/App.js b/examples/todomvc/containers/App.js index ccbabbb819..127b37d23a 100644 --- a/examples/todomvc/containers/App.js +++ b/examples/todomvc/containers/App.js @@ -1,7 +1,7 @@ import React from 'react'; import TodoApp from './TodoApp'; import { createStore } from 'redux'; -import { Provider } from 'redux-react'; +import { Provider } from 'react-redux'; import * as reducers from '../reducers'; const store = createStore(reducers); diff --git a/examples/todomvc/containers/TodoApp.js b/examples/todomvc/containers/TodoApp.js index 8edc850a6e..f1c1aa1495 100644 --- a/examples/todomvc/containers/TodoApp.js +++ b/examples/todomvc/containers/TodoApp.js @@ -1,6 +1,6 @@ import React from 'react'; import { bindActionCreators } from 'redux'; -import { Connector } from 'redux-react'; +import { Connector } from 'react-redux'; import Header from '../components/Header'; import MainSection from '../components/MainSection'; import * as TodoActions from '../actions/TodoActions'; diff --git a/examples/todomvc/package.json b/examples/todomvc/package.json index 8fc0b1d513..4e50e51038 100644 --- a/examples/todomvc/package.json +++ b/examples/todomvc/package.json @@ -30,8 +30,8 @@ "dependencies": { "classnames": "^2.1.2", "react": "^0.13.3", - "redux": "^0.12.0", - "redux-react": "^1.0.0" + "redux": "^1.0.0-alpha", + "react-redux": "^1.0.0-alpha" }, "devDependencies": { "babel-core": "^5.5.8", diff --git a/package.json b/package.json index 2183da21fe..f68942eb55 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "redux", - "version": "0.12.0", + "version": "1.0.0-alpha", "description": "Atomic Flux with hot reloading", "main": "lib/index.js", "scripts": { From a38e4adbaa13d3f343238be42a7a1b2410f286b4 Mon Sep 17 00:00:00 2001 From: jquense Date: Sat, 11 Jul 2015 20:44:48 -0400 Subject: [PATCH 20/84] Dispatch sends action through entire middleware chain --- src/utils/applyMiddleware.js | 18 ++++++++++-------- test/applyMiddleware.spec.js | 19 +++++++++++++++++++ 2 files changed, 29 insertions(+), 8 deletions(-) diff --git a/src/utils/applyMiddleware.js b/src/utils/applyMiddleware.js index e6c63ad0bb..f34ad76128 100644 --- a/src/utils/applyMiddleware.js +++ b/src/utils/applyMiddleware.js @@ -16,16 +16,18 @@ export default function applyMiddleware(...middlewares) { return next => (...args) => { const store = next(...args); - const methods = { - dispatch: store.dispatch, - getState: store.getState - }; + const middleware = composeMiddleware(...finalMiddlewares); + return { ...store, - dispatch: compose( - composeMiddleware(...finalMiddlewares)(methods), - store.dispatch - ) + dispatch: function dispatch(action) { + const methods = { dispatch, getState: store.getState }; + + return compose( + middleware(methods), + store.dispatch + )(action); + } }; }; } diff --git a/test/applyMiddleware.spec.js b/test/applyMiddleware.spec.js index ca7e72bff8..cfea770d86 100644 --- a/test/applyMiddleware.spec.js +++ b/test/applyMiddleware.spec.js @@ -15,6 +15,7 @@ describe('applyMiddleware', () => { const spy = expect.createSpy(() => {}); const store = applyMiddleware(test(spy), thunk)(createStore)(reducers.todos); + store.dispatch(addTodo('Use Redux')); expect(Object.keys(spy.calls[0].arguments[0])).toEqual([ @@ -24,6 +25,24 @@ describe('applyMiddleware', () => { expect(store.getState()).toEqual([ { id: 1, text: 'Use Redux' } ]); }); + it('should pass recursive dispatches through the middleware chain', () => { + function test(spyOnMethods) { + return () => next => action => { + spyOnMethods(action); + return next(action); + }; + } + + const spy = expect.createSpy(() => {}); + + const store = applyMiddleware(test(spy), thunk)(createStore)(reducers.todos); + + return store.dispatch(addTodoAsync('Use Redux')).then(() => { + expect(spy.calls.length).toEqual(2); + }); + + }); + it('uses thunk middleware by default', done => { const store = applyMiddleware()(createStore)(reducers.todos); From 18aceeee36d30c4b6928612107408ecf5a908dd7 Mon Sep 17 00:00:00 2001 From: Dan Abramov Date: Sun, 12 Jul 2015 21:06:36 +0300 Subject: [PATCH 21/84] Remove nesting from tests --- test/utils/bindActionCreators.spec.js | 40 +++++++++++++-------------- test/utils/isPlainObject.spec.js | 24 ++++++++-------- test/utils/mapValues.spec.js | 20 ++++++-------- test/utils/pick.spec.js | 22 +++++++-------- 4 files changed, 49 insertions(+), 57 deletions(-) diff --git a/test/utils/bindActionCreators.spec.js b/test/utils/bindActionCreators.spec.js index cfe023dd76..3405f5c284 100644 --- a/test/utils/bindActionCreators.spec.js +++ b/test/utils/bindActionCreators.spec.js @@ -3,29 +3,27 @@ import { bindActionCreators, createStore } from '../../src'; import { todos } from '../helpers/reducers'; import * as actionCreators from '../helpers/actionCreators'; -describe('Utils', () => { - describe('bindActionCreators', () => { - let store; +describe('bindActionCreators', () => { + let store; - beforeEach(() => { - store = createStore(todos); - }); + beforeEach(() => { + store = createStore(todos); + }); - it('should wrap the action creators with the dispatch function', () => { - const boundActionCreators = bindActionCreators(actionCreators, store.dispatch); - expect( - Object.keys(boundActionCreators) - ).toEqual( - Object.keys(actionCreators) - ); + it('should wrap the action creators with the dispatch function', () => { + const boundActionCreators = bindActionCreators(actionCreators, store.dispatch); + expect( + Object.keys(boundActionCreators) + ).toEqual( + Object.keys(actionCreators) + ); - const action = boundActionCreators.addTodo('Hello'); - expect(action).toEqual( - actionCreators.addTodo('Hello') - ); - expect(store.getState()).toEqual([ - { id: 1, text: 'Hello' } - ]); - }); + const action = boundActionCreators.addTodo('Hello'); + expect(action).toEqual( + actionCreators.addTodo('Hello') + ); + expect(store.getState()).toEqual([ + { id: 1, text: 'Hello' } + ]); }); }); diff --git a/test/utils/isPlainObject.spec.js b/test/utils/isPlainObject.spec.js index 13cd49a1cf..24180258aa 100644 --- a/test/utils/isPlainObject.spec.js +++ b/test/utils/isPlainObject.spec.js @@ -1,19 +1,17 @@ import expect from 'expect'; import isPlainObject from '../../src/utils/isPlainObject'; -describe('Utils', () => { - describe('isPlainObject', () => { - it('should return true only if plain object', () => { - function Test() { - this.prop = 1; - } +describe('isPlainObject', () => { + it('should return true only if plain object', () => { + function Test() { + this.prop = 1; + } - expect(isPlainObject(new Test())).toBe(false); - expect(isPlainObject(new Date())).toBe(false); - expect(isPlainObject([1, 2, 3])).toBe(false); - expect(isPlainObject(null)).toBe(false); - expect(isPlainObject()).toBe(false); - expect(isPlainObject({ 'x': 1, 'y': 2 })).toBe(true); - }); + expect(isPlainObject(new Test())).toBe(false); + expect(isPlainObject(new Date())).toBe(false); + expect(isPlainObject([1, 2, 3])).toBe(false); + expect(isPlainObject(null)).toBe(false); + expect(isPlainObject()).toBe(false); + expect(isPlainObject({ 'x': 1, 'y': 2 })).toBe(true); }); }); diff --git a/test/utils/mapValues.spec.js b/test/utils/mapValues.spec.js index 6f7945aa11..007848af81 100644 --- a/test/utils/mapValues.spec.js +++ b/test/utils/mapValues.spec.js @@ -1,17 +1,15 @@ import expect from 'expect'; import mapValues from '../../src/utils/mapValues'; -describe('Utils', () => { - describe('mapValues', () => { - it('should return object with mapped values', () => { - const test = { - a: 'c', - b: 'd' - }; - expect(mapValues(test, (val, key) => val + key)).toEqual({ - a: 'ca', - b: 'db' - }); +describe('mapValues', () => { + it('should return object with mapped values', () => { + const test = { + a: 'c', + b: 'd' + }; + expect(mapValues(test, (val, key) => val + key)).toEqual({ + a: 'ca', + b: 'db' }); }); }); diff --git a/test/utils/pick.spec.js b/test/utils/pick.spec.js index c7b5a71e84..14e02d5db0 100644 --- a/test/utils/pick.spec.js +++ b/test/utils/pick.spec.js @@ -1,18 +1,16 @@ import expect from 'expect'; import pick from '../../src/utils/pick'; -describe('Utils', () => { - describe('pick', () => { - it('should return object with picked values', () => { - const test = { - name: 'lily', - age: 20 - }; - expect( - pick(test, x => typeof x === 'string') - ).toEqual({ - name: 'lily' - }); +describe('pick', () => { + it('should return object with picked values', () => { + const test = { + name: 'lily', + age: 20 + }; + expect( + pick(test, x => typeof x === 'string') + ).toEqual({ + name: 'lily' }); }); }); From 0b5207f4a423ab1d7cc83d0533de10904c100a8a Mon Sep 17 00:00:00 2001 From: Dan Abramov Date: Mon, 13 Jul 2015 02:07:40 +0300 Subject: [PATCH 22/84] Remove thunk middleware from the core --- examples/counter/containers/App.js | 10 ++++++- src/utils/applyMiddleware.js | 28 +++++++++---------- test/applyMiddleware.spec.js | 8 ++---- .../thunk.js => test/helpers/middleware.js | 2 +- 4 files changed, 27 insertions(+), 21 deletions(-) rename src/middleware/thunk.js => test/helpers/middleware.js (64%) diff --git a/examples/counter/containers/App.js b/examples/counter/containers/App.js index 77ca8c0508..5546c59948 100644 --- a/examples/counter/containers/App.js +++ b/examples/counter/containers/App.js @@ -4,7 +4,15 @@ import { createStore, applyMiddleware } from 'redux'; import { Provider } from 'react-redux'; import * as reducers from '../reducers'; -const createStoreWithMiddleware = applyMiddleware()(createStore); +// TODO: move into a separate project +function thunk({ dispatch, getState }) { + return next => action => + typeof action === 'function' ? + action(dispatch, getState) : + next(action); +} + +const createStoreWithMiddleware = applyMiddleware(thunk)(createStore); const store = createStoreWithMiddleware(reducers); export default class App { diff --git a/src/utils/applyMiddleware.js b/src/utils/applyMiddleware.js index f34ad76128..8f52d23a51 100644 --- a/src/utils/applyMiddleware.js +++ b/src/utils/applyMiddleware.js @@ -1,6 +1,5 @@ import compose from './compose'; import composeMiddleware from './composeMiddleware'; -import thunk from '../middleware/thunk'; /** * Creates a higher-order store that applies middleware to a store's dispatch. @@ -10,24 +9,25 @@ import thunk from '../middleware/thunk'; * @return {Function} A higher-order store */ export default function applyMiddleware(...middlewares) { - const finalMiddlewares = middlewares.length ? - middlewares : - [thunk]; - return next => (...args) => { const store = next(...args); - const middleware = composeMiddleware(...finalMiddlewares); + const middleware = composeMiddleware(...middlewares); + + function dispatch(action) { + const methods = { + dispatch, + getState: store.getState + }; + + return compose( + middleware(methods), + store.dispatch + )(action); + } return { ...store, - dispatch: function dispatch(action) { - const methods = { dispatch, getState: store.getState }; - - return compose( - middleware(methods), - store.dispatch - )(action); - } + dispatch }; }; } diff --git a/test/applyMiddleware.spec.js b/test/applyMiddleware.spec.js index cfea770d86..1e8905151c 100644 --- a/test/applyMiddleware.spec.js +++ b/test/applyMiddleware.spec.js @@ -2,7 +2,7 @@ import expect from 'expect'; import { createStore, applyMiddleware } from '../src/index'; import * as reducers from './helpers/reducers'; import { addTodo, addTodoAsync, addTodoIfEmpty } from './helpers/actionCreators'; -import thunk from '../src/middleware/thunk'; +import { thunk } from './helpers/middleware'; describe('applyMiddleware', () => { it('wraps dispatch method with middleware', () => { @@ -34,17 +34,15 @@ describe('applyMiddleware', () => { } const spy = expect.createSpy(() => {}); - const store = applyMiddleware(test(spy), thunk)(createStore)(reducers.todos); return store.dispatch(addTodoAsync('Use Redux')).then(() => { expect(spy.calls.length).toEqual(2); }); - }); - it('uses thunk middleware by default', done => { - const store = applyMiddleware()(createStore)(reducers.todos); + it('works with thunk middleware', done => { + const store = applyMiddleware(thunk)(createStore)(reducers.todos); store.dispatch(addTodoIfEmpty('Hello')); expect(store.getState()).toEqual([{ diff --git a/src/middleware/thunk.js b/test/helpers/middleware.js similarity index 64% rename from src/middleware/thunk.js rename to test/helpers/middleware.js index e6638cdb06..009a5dfd8c 100644 --- a/src/middleware/thunk.js +++ b/test/helpers/middleware.js @@ -1,4 +1,4 @@ -export default function thunkMiddleware({ dispatch, getState }) { +export function thunk({ dispatch, getState }) { return next => action => typeof action === 'function' ? action(dispatch, getState) : From 650f002dfc806c9518d2afd19c29dd99fa46a78a Mon Sep 17 00:00:00 2001 From: Dan Abramov Date: Mon, 13 Jul 2015 02:27:17 +0300 Subject: [PATCH 23/84] Remove combineReducers shortcut in favor of an explicit call --- examples/counter/containers/App.js | 5 +- examples/todomvc/containers/App.js | 5 +- src/createStore.js | 12 +- test/Store.spec.js | 209 ---------------------------- test/createStore.spec.js | 210 +++++++++++++++++++++++++++-- 5 files changed, 204 insertions(+), 237 deletions(-) delete mode 100644 test/Store.spec.js diff --git a/examples/counter/containers/App.js b/examples/counter/containers/App.js index 5546c59948..5d394dde35 100644 --- a/examples/counter/containers/App.js +++ b/examples/counter/containers/App.js @@ -1,6 +1,6 @@ import React from 'react'; import CounterApp from './CounterApp'; -import { createStore, applyMiddleware } from 'redux'; +import { createStore, applyMiddleware, combineReducers } from 'redux'; import { Provider } from 'react-redux'; import * as reducers from '../reducers'; @@ -13,7 +13,8 @@ function thunk({ dispatch, getState }) { } const createStoreWithMiddleware = applyMiddleware(thunk)(createStore); -const store = createStoreWithMiddleware(reducers); +const reducer = combineReducers(reducers); +const store = createStoreWithMiddleware(reducer); export default class App { render() { diff --git a/examples/todomvc/containers/App.js b/examples/todomvc/containers/App.js index 127b37d23a..b95a36e4db 100644 --- a/examples/todomvc/containers/App.js +++ b/examples/todomvc/containers/App.js @@ -1,10 +1,11 @@ import React from 'react'; import TodoApp from './TodoApp'; -import { createStore } from 'redux'; +import { createStore, combineReducers } from 'redux'; import { Provider } from 'react-redux'; import * as reducers from '../reducers'; -const store = createStore(reducers); +const reducer = combineReducers(reducers); +const store = createStore(reducer); export default class App { render() { diff --git a/src/createStore.js b/src/createStore.js index 6a79406c98..5344d42088 100644 --- a/src/createStore.js +++ b/src/createStore.js @@ -1,15 +1,7 @@ import Store from './Store'; -import combineReducers from './utils/combineReducers'; -export default function createStore( - reducer, - initialState -) { - const finalReducer = typeof reducer === 'function' ? - reducer : - combineReducers(reducer); - - const store = new Store(finalReducer, initialState); +export default function createStore(reducer, initialState) { + const store = new Store(reducer, initialState); return { dispatch: ::store.dispatch, diff --git a/test/Store.spec.js b/test/Store.spec.js deleted file mode 100644 index dfe21c5676..0000000000 --- a/test/Store.spec.js +++ /dev/null @@ -1,209 +0,0 @@ -import expect from 'expect'; -import Store from '../src/Store'; -import { todos, todosReverse } from './helpers/reducers'; -import { addTodo } from './helpers/actionCreators'; - -describe('Store', () => { - it('should require a reducer function', () => { - expect(() => - new Store() - ).toThrow(); - - expect(() => - new Store('test') - ).toThrow(); - - expect(() => - new Store({}) - ).toThrow(); - - expect(() => - new Store(() => {}) - ).toNotThrow(); - }); - - it('should apply the reducer to the previous state', () => { - const store = new Store(todos); - expect(store.getState()).toEqual([]); - - store.dispatch({}); - expect(store.getState()).toEqual([]); - - store.dispatch(addTodo('Hello')); - expect(store.getState()).toEqual([{ - id: 1, - text: 'Hello' - }]); - - store.dispatch(addTodo('World')); - expect(store.getState()).toEqual([{ - id: 1, - text: 'Hello' - }, { - id: 2, - text: 'World' - }]); - }); - - it('should apply the reducer to the initial state', () => { - const store = new Store(todos, [{ - id: 1, - text: 'Hello' - }]); - expect(store.getState()).toEqual([{ - id: 1, - text: 'Hello' - }]); - - store.dispatch({}); - expect(store.getState()).toEqual([{ - id: 1, - text: 'Hello' - }]); - - store.dispatch(addTodo('World')); - expect(store.getState()).toEqual([{ - id: 1, - text: 'Hello' - }, { - id: 2, - text: 'World' - }]); - }); - - it('should preserve the state when replacing a reducer', () => { - const store = new Store(todos); - store.dispatch(addTodo('Hello')); - store.dispatch(addTodo('World')); - expect(store.getState()).toEqual([{ - id: 1, - text: 'Hello' - }, { - id: 2, - text: 'World' - }]); - - let nextStore = new Store(todosReverse); - store.replaceReducer(nextStore.getReducer()); - expect(store.getState()).toEqual([{ - id: 1, - text: 'Hello' - }, { - id: 2, - text: 'World' - }]); - - store.dispatch(addTodo('Perhaps')); - expect(store.getState()).toEqual([{ - id: 3, - text: 'Perhaps' - }, { - id: 1, - text: 'Hello' - }, { - id: 2, - text: 'World' - }]); - - nextStore = new Store(todos); - store.replaceReducer(nextStore.getReducer()); - expect(store.getState()).toEqual([{ - id: 3, - text: 'Perhaps' - }, { - id: 1, - text: 'Hello' - }, { - id: 2, - text: 'World' - }]); - - store.dispatch(addTodo('Surely')); - expect(store.getState()).toEqual([{ - id: 3, - text: 'Perhaps' - }, { - id: 1, - text: 'Hello' - }, { - id: 2, - text: 'World' - }, { - id: 4, - text: 'Surely' - }]); - }); - - it('should support multiple subscriptions', () => { - const store = new Store(todos); - const listenerA = expect.createSpy(() => {}); - const listenerB = expect.createSpy(() => {}); - - let unsubscribeA = store.subscribe(listenerA); - store.dispatch({}); - expect(listenerA.calls.length).toBe(1); - expect(listenerB.calls.length).toBe(0); - - store.dispatch({}); - expect(listenerA.calls.length).toBe(2); - expect(listenerB.calls.length).toBe(0); - - const unsubscribeB = store.subscribe(listenerB); - expect(listenerA.calls.length).toBe(2); - expect(listenerB.calls.length).toBe(0); - - store.dispatch({}); - expect(listenerA.calls.length).toBe(3); - expect(listenerB.calls.length).toBe(1); - - unsubscribeA(); - expect(listenerA.calls.length).toBe(3); - expect(listenerB.calls.length).toBe(1); - - store.dispatch({}); - expect(listenerA.calls.length).toBe(3); - expect(listenerB.calls.length).toBe(2); - - unsubscribeB(); - expect(listenerA.calls.length).toBe(3); - expect(listenerB.calls.length).toBe(2); - - store.dispatch({}); - expect(listenerA.calls.length).toBe(3); - expect(listenerB.calls.length).toBe(2); - - unsubscribeA = store.subscribe(listenerA); - expect(listenerA.calls.length).toBe(3); - expect(listenerB.calls.length).toBe(2); - - store.dispatch({}); - expect(listenerA.calls.length).toBe(4); - expect(listenerB.calls.length).toBe(2); - }); - - it('should provide an up-to-date state when a subscriber is notified', done => { - const store = new Store(todos); - store.subscribe(() => { - expect(store.getState()).toEqual([{ - id: 1, - text: 'Hello' - }]); - done(); - }); - store.dispatch(addTodo('Hello')); - }); - - it('should only accept plain object actions', () => { - const store = new Store(todos); - expect(() => - store.dispatch({}) - ).toNotThrow(); - - function AwesomeMap() { } - [null, undefined, 42, 'hey', new AwesomeMap()].forEach(nonObject => - expect(() => - store.dispatch(nonObject) - ).toThrow(/plain/) - ); - }); -}); diff --git a/test/createStore.spec.js b/test/createStore.spec.js index 20f22e0566..b880752157 100644 --- a/test/createStore.spec.js +++ b/test/createStore.spec.js @@ -1,11 +1,11 @@ import expect from 'expect'; -import { createStore } from '../src/index'; +import { createStore, combineReducers } from '../src/index'; +import { addTodo } from './helpers/actionCreators'; import * as reducers from './helpers/reducers'; -import { addTodo, addTodoAsync } from './helpers/actionCreators'; describe('createStore', () => { it('should expose the public API', () => { - const store = createStore(reducers); + const store = createStore(combineReducers(reducers)); const methods = Object.keys(store); expect(methods.length).toBe(5); @@ -16,12 +16,22 @@ describe('createStore', () => { expect(methods).toContain('replaceReducer'); }); - it('should compose the reducers when passed an object', () => { - const store = createStore(reducers); - expect(store.getState()).toEqual({ - todos: [], - todosReverse: [] - }); + it('should require a reducer function', () => { + expect(() => + createStore() + ).toThrow(); + + expect(() => + createStore('test') + ).toThrow(); + + expect(() => + createStore({}) + ).toThrow(); + + expect(() => + createStore(() => {}) + ).toNotThrow(); }); it('should pass the initial action and the initial state', () => { @@ -35,17 +45,189 @@ describe('createStore', () => { }]); }); - it('should dispatch the raw action without the middleware', () => { - const store = createStore(reducers.todos, undefined, []); + it('should apply the reducer to the previous state', () => { + const store = createStore(reducers.todos); + expect(store.getState()).toEqual([]); + + store.dispatch({}); + expect(store.getState()).toEqual([]); + store.dispatch(addTodo('Hello')); expect(store.getState()).toEqual([{ id: 1, text: 'Hello' }]); + store.dispatch(addTodo('World')); + expect(store.getState()).toEqual([{ + id: 1, + text: 'Hello' + }, { + id: 2, + text: 'World' + }]); + }); + + it('should apply the reducer to the initial state', () => { + const store = createStore(reducers.todos, [{ + id: 1, + text: 'Hello' + }]); + expect(store.getState()).toEqual([{ + id: 1, + text: 'Hello' + }]); + + store.dispatch({}); + expect(store.getState()).toEqual([{ + id: 1, + text: 'Hello' + }]); + + store.dispatch(addTodo('World')); + expect(store.getState()).toEqual([{ + id: 1, + text: 'Hello' + }, { + id: 2, + text: 'World' + }]); + }); + + it('should preserve the state when replacing a reducer', () => { + const store = createStore(reducers.todos); + store.dispatch(addTodo('Hello')); + store.dispatch(addTodo('World')); + expect(store.getState()).toEqual([{ + id: 1, + text: 'Hello' + }, { + id: 2, + text: 'World' + }]); + + let nextStore = createStore(reducers.todosReverse); + store.replaceReducer(nextStore.getReducer()); + expect(store.getState()).toEqual([{ + id: 1, + text: 'Hello' + }, { + id: 2, + text: 'World' + }]); + + store.dispatch(addTodo('Perhaps')); + expect(store.getState()).toEqual([{ + id: 3, + text: 'Perhaps' + }, { + id: 1, + text: 'Hello' + }, { + id: 2, + text: 'World' + }]); + + nextStore = createStore(reducers.todos); + store.replaceReducer(nextStore.getReducer()); + expect(store.getState()).toEqual([{ + id: 3, + text: 'Perhaps' + }, { + id: 1, + text: 'Hello' + }, { + id: 2, + text: 'World' + }]); + + store.dispatch(addTodo('Surely')); + expect(store.getState()).toEqual([{ + id: 3, + text: 'Perhaps' + }, { + id: 1, + text: 'Hello' + }, { + id: 2, + text: 'World' + }, { + id: 4, + text: 'Surely' + }]); + }); + + it('should support multiple subscriptions', () => { + const store = createStore(reducers.todos); + const listenerA = expect.createSpy(() => {}); + const listenerB = expect.createSpy(() => {}); + + let unsubscribeA = store.subscribe(listenerA); + store.dispatch({}); + expect(listenerA.calls.length).toBe(1); + expect(listenerB.calls.length).toBe(0); + + store.dispatch({}); + expect(listenerA.calls.length).toBe(2); + expect(listenerB.calls.length).toBe(0); + + const unsubscribeB = store.subscribe(listenerB); + expect(listenerA.calls.length).toBe(2); + expect(listenerB.calls.length).toBe(0); + + store.dispatch({}); + expect(listenerA.calls.length).toBe(3); + expect(listenerB.calls.length).toBe(1); + + unsubscribeA(); + expect(listenerA.calls.length).toBe(3); + expect(listenerB.calls.length).toBe(1); + + store.dispatch({}); + expect(listenerA.calls.length).toBe(3); + expect(listenerB.calls.length).toBe(2); + + unsubscribeB(); + expect(listenerA.calls.length).toBe(3); + expect(listenerB.calls.length).toBe(2); + + store.dispatch({}); + expect(listenerA.calls.length).toBe(3); + expect(listenerB.calls.length).toBe(2); + + unsubscribeA = store.subscribe(listenerA); + expect(listenerA.calls.length).toBe(3); + expect(listenerB.calls.length).toBe(2); + + store.dispatch({}); + expect(listenerA.calls.length).toBe(4); + expect(listenerB.calls.length).toBe(2); + }); + + it('should provide an up-to-date state when a subscriber is notified', done => { + const store = createStore(reducers.todos); + store.subscribe(() => { + expect(store.getState()).toEqual([{ + id: 1, + text: 'Hello' + }]); + done(); + }); + store.dispatch(addTodo('Hello')); + }); + + it('should only accept plain object actions', () => { + const store = createStore(reducers.todos); expect(() => - store.dispatch(addTodoAsync('World')) - ).toThrow(/plain/); + store.dispatch({}) + ).toNotThrow(); + + function AwesomeMap() { } + [null, undefined, 42, 'hey', new AwesomeMap()].forEach(nonObject => + expect(() => + store.dispatch(nonObject) + ).toThrow(/plain/) + ); }); it('should handle nested dispatches gracefully', () => { @@ -57,7 +239,7 @@ describe('createStore', () => { return action.type === 'bar' ? 2 : state; } - const store = createStore({ foo, bar }); + const store = createStore(combineReducers({ foo, bar })); store.subscribe(function kindaComponentDidUpdate() { const state = store.getState(); From 749b4abaa39f536057a420324515ba4d3944600c Mon Sep 17 00:00:00 2001 From: Dan Abramov Date: Mon, 13 Jul 2015 03:45:03 +0300 Subject: [PATCH 24/84] Tweak dependencies --- examples/counter/package.json | 2 +- examples/counter/webpack.config.js | 10 +++++++--- examples/todomvc/package.json | 2 +- examples/todomvc/webpack.config.js | 13 +++++++++---- package.json | 4 +--- 5 files changed, 19 insertions(+), 12 deletions(-) diff --git a/examples/counter/package.json b/examples/counter/package.json index 9206cb430a..72c81ee4f7 100644 --- a/examples/counter/package.json +++ b/examples/counter/package.json @@ -32,7 +32,7 @@ "react-redux": "^1.0.0-alpha" }, "devDependencies": { - "babel-core": "^5.5.8", + "babel-core": "^5.6.18", "babel-loader": "^5.1.4", "node-libs-browser": "^0.5.2", "react-hot-loader": "^1.2.7", diff --git a/examples/counter/webpack.config.js b/examples/counter/webpack.config.js index 2e86f7e7ea..2061e19a48 100644 --- a/examples/counter/webpack.config.js +++ b/examples/counter/webpack.config.js @@ -19,8 +19,7 @@ module.exports = { ], resolve: { alias: { - 'redux': path.join(__dirname, '..', '..', 'src'), - 'react': path.join(__dirname, '..', '..', 'node_modules', 'react') + 'redux': path.join(__dirname, '..', '..', 'src') }, extensions: ['', '.js'] }, @@ -28,7 +27,12 @@ module.exports = { loaders: [{ test: /\.js$/, loaders: ['react-hot', 'babel'], - exclude: /node_modules/ + exclude: /node_modules/, + include: __dirname + }, { + test: /\.js$/, + loaders: ['babel'], + include: path.join(__dirname, '..', '..', 'src') }] } }; diff --git a/examples/todomvc/package.json b/examples/todomvc/package.json index 4e50e51038..0e0293f7ff 100644 --- a/examples/todomvc/package.json +++ b/examples/todomvc/package.json @@ -34,7 +34,7 @@ "react-redux": "^1.0.0-alpha" }, "devDependencies": { - "babel-core": "^5.5.8", + "babel-core": "^5.6.18", "babel-loader": "^5.1.4", "node-libs-browser": "^0.5.2", "raw-loader": "^0.5.1", diff --git a/examples/todomvc/webpack.config.js b/examples/todomvc/webpack.config.js index 9a949855bd..6b487b38c6 100644 --- a/examples/todomvc/webpack.config.js +++ b/examples/todomvc/webpack.config.js @@ -19,8 +19,7 @@ module.exports = { ], resolve: { alias: { - 'redux': path.join(__dirname, '..', '..', 'src'), - 'react': path.join(__dirname, '..', '..', 'node_modules', 'react') + 'redux': path.join(__dirname, '..', '..', 'src') }, extensions: ['', '.js'] }, @@ -28,10 +27,16 @@ module.exports = { loaders: [{ test: /\.js$/, loaders: ['react-hot', 'babel'], - exclude: /node_modules/ + exclude: /node_modules/, + include: __dirname + }, { + test: /\.js$/, + loaders: ['babel'], + include: path.join(__dirname, '..', '..', 'src') }, { test: /\.css?$/, - loaders: ['style', 'raw'] + loaders: ['style', 'raw'], + include: __dirname }] } }; diff --git a/package.json b/package.json index 4ba22084a1..86f808b435 100644 --- a/package.json +++ b/package.json @@ -36,7 +36,7 @@ "homepage": "https://github.com/gaearon/redux", "devDependencies": { "babel": "^5.5.8", - "babel-core": "5.6.15", + "babel-core": "^5.6.18", "babel-eslint": "^3.1.15", "babel-loader": "^5.1.4", "eslint": "^0.23", @@ -45,8 +45,6 @@ "expect": "^1.6.0", "isparta": "^3.0.3", "mocha": "^2.2.5", - "react": "^0.13.0", - "react-hot-loader": "^1.2.7", "rimraf": "^2.3.4", "webpack": "^1.9.6", "webpack-dev-server": "^1.8.2" From e3ec5019cfc8d1815290855d995381416dff0c20 Mon Sep 17 00:00:00 2001 From: Dan Abramov Date: Mon, 13 Jul 2015 16:39:41 +0300 Subject: [PATCH 25/84] Extract thunk --- examples/counter/containers/App.js | 9 +-------- examples/counter/package.json | 3 ++- examples/todomvc/package.json | 4 ++-- 3 files changed, 5 insertions(+), 11 deletions(-) diff --git a/examples/counter/containers/App.js b/examples/counter/containers/App.js index edca2dd1f2..970195d202 100644 --- a/examples/counter/containers/App.js +++ b/examples/counter/containers/App.js @@ -1,17 +1,10 @@ import React, { Component } from 'react'; import CounterApp from './CounterApp'; import { createStore, applyMiddleware, combineReducers } from 'redux'; +import thunk from 'redux-thunk'; import { Provider } from 'react-redux'; import * as reducers from '../reducers'; -// TODO: move into a separate project -function thunk({ dispatch, getState }) { - return next => action => - typeof action === 'function' ? - action(dispatch, getState) : - next(action); -} - const createStoreWithMiddleware = applyMiddleware(thunk)(createStore); const reducer = combineReducers(reducers); const store = createStoreWithMiddleware(reducer); diff --git a/examples/counter/package.json b/examples/counter/package.json index 72c81ee4f7..9a88226fd7 100644 --- a/examples/counter/package.json +++ b/examples/counter/package.json @@ -28,8 +28,9 @@ "homepage": "https://github.com/gaearon/redux#readme", "dependencies": { "react": "^0.13.3", + "react-redux": "^1.0.0-alpha", "redux": "^1.0.0-alpha", - "react-redux": "^1.0.0-alpha" + "redux-thunk": "^0.1.0" }, "devDependencies": { "babel-core": "^5.6.18", diff --git a/examples/todomvc/package.json b/examples/todomvc/package.json index 0e0293f7ff..bca9aa62c0 100644 --- a/examples/todomvc/package.json +++ b/examples/todomvc/package.json @@ -30,8 +30,8 @@ "dependencies": { "classnames": "^2.1.2", "react": "^0.13.3", - "redux": "^1.0.0-alpha", - "react-redux": "^1.0.0-alpha" + "react-redux": "^1.0.0-alpha", + "redux": "^1.0.0-alpha" }, "devDependencies": { "babel-core": "^5.6.18", From afd42605a9519c5a2bc0678bcb5b88c89da9b0f0 Mon Sep 17 00:00:00 2001 From: Dan Abramov Date: Mon, 13 Jul 2015 17:29:58 +0300 Subject: [PATCH 26/84] Move test files --- test/{ => utils}/applyMiddleware.spec.js | 8 ++++---- test/{ => utils}/combineReducers.spec.js | 2 +- test/{ => utils}/compose.spec.js | 2 +- test/{ => utils}/composeMiddleware.spec.js | 2 +- 4 files changed, 7 insertions(+), 7 deletions(-) rename test/{ => utils}/applyMiddleware.spec.js (88%) rename test/{ => utils}/combineReducers.spec.js (98%) rename test/{ => utils}/compose.spec.js (92%) rename test/{ => utils}/composeMiddleware.spec.js (93%) diff --git a/test/applyMiddleware.spec.js b/test/utils/applyMiddleware.spec.js similarity index 88% rename from test/applyMiddleware.spec.js rename to test/utils/applyMiddleware.spec.js index 1e8905151c..72b8a9b3bf 100644 --- a/test/applyMiddleware.spec.js +++ b/test/utils/applyMiddleware.spec.js @@ -1,8 +1,8 @@ import expect from 'expect'; -import { createStore, applyMiddleware } from '../src/index'; -import * as reducers from './helpers/reducers'; -import { addTodo, addTodoAsync, addTodoIfEmpty } from './helpers/actionCreators'; -import { thunk } from './helpers/middleware'; +import { createStore, applyMiddleware } from '../../src/index'; +import * as reducers from '../helpers/reducers'; +import { addTodo, addTodoAsync, addTodoIfEmpty } from '../helpers/actionCreators'; +import { thunk } from '../helpers/middleware'; describe('applyMiddleware', () => { it('wraps dispatch method with middleware', () => { diff --git a/test/combineReducers.spec.js b/test/utils/combineReducers.spec.js similarity index 98% rename from test/combineReducers.spec.js rename to test/utils/combineReducers.spec.js index 49483702ef..63dc58bbd8 100644 --- a/test/combineReducers.spec.js +++ b/test/utils/combineReducers.spec.js @@ -1,5 +1,5 @@ import expect from 'expect'; -import { combineReducers } from '../src'; +import { combineReducers } from '../../src'; describe('Utils', () => { describe('combineReducers', () => { diff --git a/test/compose.spec.js b/test/utils/compose.spec.js similarity index 92% rename from test/compose.spec.js rename to test/utils/compose.spec.js index 92a574d371..dd4dd5c9bb 100644 --- a/test/compose.spec.js +++ b/test/utils/compose.spec.js @@ -1,5 +1,5 @@ import expect from 'expect'; -import { compose } from '../src'; +import { compose } from '../../src'; describe('Utils', () => { describe('compose', () => { diff --git a/test/composeMiddleware.spec.js b/test/utils/composeMiddleware.spec.js similarity index 93% rename from test/composeMiddleware.spec.js rename to test/utils/composeMiddleware.spec.js index 4d291f4816..8b5192faea 100644 --- a/test/composeMiddleware.spec.js +++ b/test/utils/composeMiddleware.spec.js @@ -1,5 +1,5 @@ import expect from 'expect'; -import { composeMiddleware } from '../src'; +import { composeMiddleware } from '../../src'; describe('Utils', () => { describe('composeMiddleware', () => { From 9045a0c1732ab266a1ff09926420f266e1a74d65 Mon Sep 17 00:00:00 2001 From: Dan Abramov Date: Mon, 13 Jul 2015 19:10:03 +0300 Subject: [PATCH 27/84] Handling private actions is an anti-pattern. Enforce it. (Fixes #186) --- src/Store.js | 9 ++++- src/utils/combineReducers.js | 32 ++++++++++++----- test/utils/combineReducers.spec.js | 56 ++++++++++++++++++++---------- 3 files changed, 68 insertions(+), 29 deletions(-) diff --git a/src/Store.js b/src/Store.js index ed85b987fc..2459fe8d31 100644 --- a/src/Store.js +++ b/src/Store.js @@ -1,6 +1,13 @@ import invariant from 'invariant'; import isPlainObject from './utils/isPlainObject'; +// Don't ever try to handle these action types in your code. They are private. +// For any unknown actions, you must return the current state. +// If the current state is undefined, you must return the initial state. +export const ActionTypes = { + INIT: '@@redux/INIT' +}; + export default class Store { constructor(reducer, initialState) { invariant( @@ -19,7 +26,7 @@ export default class Store { replaceReducer(nextReducer) { this.reducer = nextReducer; - this.dispatch({ type: '@@INIT' }); + this.dispatch({ type: ActionTypes.INIT }); } dispatch(action) { diff --git a/src/utils/combineReducers.js b/src/utils/combineReducers.js index fe65c056cc..4008a5587e 100644 --- a/src/utils/combineReducers.js +++ b/src/utils/combineReducers.js @@ -1,19 +1,11 @@ import mapValues from '../utils/mapValues'; import pick from '../utils/pick'; import invariant from 'invariant'; +import { ActionTypes } from '../Store'; function getErrorMessage(key, action) { const actionType = action && action.type; const actionName = actionType && `"${actionType}"` || 'an action'; - const reducerName = `Reducer "${key}"`; - - if (actionType === '@@INIT') { - return ( - `${reducerName} returned undefined during initialization. ` + - `If the state passed to the reducer is undefined, ` + - `you must explicitly return the initial state.` - ); - } return ( `Reducer "${key}" returned undefined handling ${actionName}. ` + @@ -24,6 +16,28 @@ function getErrorMessage(key, action) { export default function combineReducers(reducers) { const finalReducers = pick(reducers, (val) => typeof val === 'function'); + Object.keys(finalReducers).forEach(key => { + const reducer = finalReducers[key]; + invariant( + typeof reducer(undefined, { type: ActionTypes.INIT }) !== 'undefined', + `Reducer "${key}" returned undefined during initialization. ` + + `If the state passed to the reducer is undefined, you must ` + + `explicitly return the initial state. The initial state may ` + + `not be undefined.` + ); + + const type = Math.random().toString(36).substring(7).split('').join('.'); + invariant( + typeof reducer(undefined, { type }) !== 'undefined', + `Reducer "${key}" returned undefined when probed with a random type. ` + + `Don't try to handle ${ActionTypes.INIT} or other actions in "redux/*" ` + + `namespace. They are considered private. Instead, you must return the ` + + `current state for any unknown actions, unless it is undefined, ` + + `in which case you must return the initial state, regardless of the ` + + `action type. The initial state may not be undefined.` + ); + }); + return function composition(state = {}, action) { return mapValues(finalReducers, (reducer, key) => { const newState = reducer(state[key], action); diff --git a/test/utils/combineReducers.spec.js b/test/utils/combineReducers.spec.js index 63dc58bbd8..b72429e38a 100644 --- a/test/utils/combineReducers.spec.js +++ b/test/utils/combineReducers.spec.js @@ -1,5 +1,6 @@ import expect from 'expect'; import { combineReducers } from '../../src'; +import { ActionTypes } from '../../src/Store'; describe('Utils', () => { describe('combineReducers', () => { @@ -30,43 +31,44 @@ describe('Utils', () => { ).toEqual(['stack']); }); - it('should throw an error if a reducer returns undefined', () => { + it('should throw an error if a reducer returns undefined handling an action', () => { const reducer = combineReducers({ - undefinedByDefault(state = 0, action) { + counter(state = 0, action) { switch (action && action.type) { case 'increment': return state + 1; case 'decrement': return state - 1; - case '@@INIT': - return state; - default: + case 'whatever': + case null: + case undefined: return undefined; + default: + return state; } } }); - const initialState = reducer(undefined, { type: '@@INIT' }); expect( - () => reducer(initialState, { type: 'whatever' }) + () => reducer({ counter: 0 }, { type: 'whatever' }) ).toThrow( - /"undefinedByDefault".*"whatever"/ + /"counter".*"whatever"/ ); expect( - () => reducer(initialState, null) + () => reducer({ counter: 0 }, null) ).toThrow( - /"undefinedByDefault".*an action/ + /"counter".*an action/ ); expect( - () => reducer(initialState, {}) + () => reducer({ counter: 0 }, {}) ).toThrow( - /"undefinedByDefault".*an action/ + /"counter".*an action/ ); }); it('should throw an error if a reducer returns undefined initializing', () => { - const reducer = combineReducers({ - undefinedInitially(state, action) { + expect(() => combineReducers({ + counter(state, action) { switch (action.type) { case 'increment': return state + 1; @@ -76,12 +78,28 @@ describe('Utils', () => { return state; } } - }); + })).toThrow( + /"counter".*initialization/ + ); + }); - expect( - () => reducer(undefined, { type: '@@INIT' }) - ).toThrow( - /"undefinedInitially".*initialization/ + it('should throw an error if a reducer attempts to handle a private action', () => { + expect(() => combineReducers({ + counter(state, action) { + switch (action.type) { + case 'increment': + return state + 1; + case 'decrement': + return state - 1; + // Never do this in your code: + case ActionTypes.INIT: + return 0; + default: + return undefined; + } + } + })).toThrow( + /"counter".*private/ ); }); }); From e39afbec270e9381df3d23dfa2f770c44f488380 Mon Sep 17 00:00:00 2001 From: Dan Abramov Date: Mon, 13 Jul 2015 19:42:57 +0300 Subject: [PATCH 28/84] 1.0.0-rc --- examples/counter/package.json | 2 +- examples/todomvc/package.json | 2 +- package.json | 8 +++++--- 3 files changed, 7 insertions(+), 5 deletions(-) diff --git a/examples/counter/package.json b/examples/counter/package.json index 9a88226fd7..bf39679bfa 100644 --- a/examples/counter/package.json +++ b/examples/counter/package.json @@ -29,7 +29,7 @@ "dependencies": { "react": "^0.13.3", "react-redux": "^1.0.0-alpha", - "redux": "^1.0.0-alpha", + "redux": "^1.0.0-rc", "redux-thunk": "^0.1.0" }, "devDependencies": { diff --git a/examples/todomvc/package.json b/examples/todomvc/package.json index bca9aa62c0..ddcf8c5fcb 100644 --- a/examples/todomvc/package.json +++ b/examples/todomvc/package.json @@ -31,7 +31,7 @@ "classnames": "^2.1.2", "react": "^0.13.3", "react-redux": "^1.0.0-alpha", - "redux": "^1.0.0-alpha" + "redux": "^1.0.0-rc" }, "devDependencies": { "babel-core": "^5.6.18", diff --git a/package.json b/package.json index 86f808b435..cc2955e334 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "redux", - "version": "1.0.0-alpha", + "version": "1.0.0-rc", "description": "Atomic Flux with hot reloading", "main": "lib/index.js", "scripts": { @@ -18,6 +18,9 @@ "url": "https://github.com/gaearon/redux.git" }, "keywords": [ + "flux", + "redux", + "reducer", "react", "reactjs", "hot", @@ -25,8 +28,7 @@ "hmr", "live", "edit", - "webpack", - "flux" + "webpack" ], "author": "Dan Abramov (http://github.com/gaearon)", "license": "MIT", From 2aa83ffe90922b3475f54b726f01f7fbc722e47d Mon Sep 17 00:00:00 2001 From: Dan Abramov Date: Mon, 13 Jul 2015 22:51:52 +0300 Subject: [PATCH 29/84] Bad idea to call that React Redux 1.0 alpha. Stick to 0.x for now --- examples/counter/package.json | 2 +- examples/todomvc/package.json | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/examples/counter/package.json b/examples/counter/package.json index bf39679bfa..6e7e689762 100644 --- a/examples/counter/package.json +++ b/examples/counter/package.json @@ -28,7 +28,7 @@ "homepage": "https://github.com/gaearon/redux#readme", "dependencies": { "react": "^0.13.3", - "react-redux": "^1.0.0-alpha", + "react-redux": "^0.2.1", "redux": "^1.0.0-rc", "redux-thunk": "^0.1.0" }, diff --git a/examples/todomvc/package.json b/examples/todomvc/package.json index ddcf8c5fcb..e95fa73784 100644 --- a/examples/todomvc/package.json +++ b/examples/todomvc/package.json @@ -30,7 +30,7 @@ "dependencies": { "classnames": "^2.1.2", "react": "^0.13.3", - "react-redux": "^1.0.0-alpha", + "react-redux": "^0.2.1", "redux": "^1.0.0-rc" }, "devDependencies": { From baede742c6e17cf00818bef144d4690af3fae063 Mon Sep 17 00:00:00 2001 From: Terry Appleby Date: Mon, 13 Jul 2015 23:07:28 -0400 Subject: [PATCH 30/84] update wrap dispatch test with the failure case. --- test/utils/applyMiddleware.spec.js | 12 ++++++++---- 1 file changed, 8 insertions(+), 4 deletions(-) diff --git a/test/utils/applyMiddleware.spec.js b/test/utils/applyMiddleware.spec.js index 72b8a9b3bf..d3f8f60ea5 100644 --- a/test/utils/applyMiddleware.spec.js +++ b/test/utils/applyMiddleware.spec.js @@ -5,11 +5,11 @@ import { addTodo, addTodoAsync, addTodoIfEmpty } from '../helpers/actionCreators import { thunk } from '../helpers/middleware'; describe('applyMiddleware', () => { - it('wraps dispatch method with middleware', () => { + it('wraps dispatch method with middleware once', () => { function test(spyOnMethods) { - return methods => next => action => { + return methods => { spyOnMethods(methods); - return next(action); + return next => action => next(action); }; } @@ -17,12 +17,16 @@ describe('applyMiddleware', () => { const store = applyMiddleware(test(spy), thunk)(createStore)(reducers.todos); store.dispatch(addTodo('Use Redux')); + store.dispatch(addTodo('Flux FTW!')); + + expect(spy.calls.length).toEqual(1); expect(Object.keys(spy.calls[0].arguments[0])).toEqual([ 'dispatch', 'getState' ]); - expect(store.getState()).toEqual([ { id: 1, text: 'Use Redux' } ]); + + expect(store.getState()).toEqual([ { id: 1, text: 'Use Redux' }, { id: 2, text: 'Flux FTW!' } ]); }); it('should pass recursive dispatches through the middleware chain', () => { From b4766b3dd1a61d969f19fd1f6ba6a1957a0f2f35 Mon Sep 17 00:00:00 2001 From: Terry Appleby Date: Mon, 13 Jul 2015 23:17:40 -0400 Subject: [PATCH 31/84] update applyMiddleware to only compose dispatch method once. --- src/utils/applyMiddleware.js | 19 ++++++++++--------- 1 file changed, 10 insertions(+), 9 deletions(-) diff --git a/src/utils/applyMiddleware.js b/src/utils/applyMiddleware.js index 8f52d23a51..ac1434260a 100644 --- a/src/utils/applyMiddleware.js +++ b/src/utils/applyMiddleware.js @@ -13,18 +13,19 @@ export default function applyMiddleware(...middlewares) { const store = next(...args); const middleware = composeMiddleware(...middlewares); - function dispatch(action) { - const methods = { - dispatch, - getState: store.getState - }; + let composedDispatch = null; - return compose( - middleware(methods), - store.dispatch - )(action); + function dispatch(action) { + return composedDispatch(action); } + const methods = { + dispatch, + getState: store.getState + }; + + composedDispatch = compose(middleware(methods), store.dispatch); + return { ...store, dispatch From 3481fdbce119e7e87df2a5491cfdea3de8dda220 Mon Sep 17 00:00:00 2001 From: Nicola Molinari Date: Tue, 14 Jul 2015 18:34:19 +0200 Subject: [PATCH 32/84] Use npm for tasks --- .gitignore | 4 ++-- .mversionrc | 9 +++++++++ .npmignore | 4 ++++ package.json | 21 +++++++++++++-------- scripts/browser | 8 -------- scripts/build | 4 ---- scripts/clean | 3 --- scripts/lint | 3 --- scripts/prepublish | 6 ------ scripts/test | 3 --- scripts/test-cov | 3 --- scripts/test-watch | 3 --- 12 files changed, 28 insertions(+), 43 deletions(-) create mode 100644 .mversionrc delete mode 100755 scripts/browser delete mode 100755 scripts/build delete mode 100755 scripts/clean delete mode 100755 scripts/lint delete mode 100755 scripts/prepublish delete mode 100755 scripts/test delete mode 100755 scripts/test-cov delete mode 100755 scripts/test-watch diff --git a/.gitignore b/.gitignore index dbb9d4c83b..ecb01c5ef8 100644 --- a/.gitignore +++ b/.gitignore @@ -1,6 +1,6 @@ -node_modules -npm-debug.log .DS_Store +*.log +node_modules dist lib coverage diff --git a/.mversionrc b/.mversionrc new file mode 100644 index 0000000000..3cde5c762b --- /dev/null +++ b/.mversionrc @@ -0,0 +1,9 @@ +{ + "commitMessage": "%s", + "tagName": "v%s", + "scripts": { + "preupdate": "npm run build && npm test", + "postcommit": "git push && git push --tags && npm publish", + "postupdate": "echo 'New version %s tagged and released'" + } +} diff --git a/.npmignore b/.npmignore index 9458ec8184..af369acc92 100644 --- a/.npmignore +++ b/.npmignore @@ -1,2 +1,6 @@ +.DS_Store +*.log +node_modules src examples +coverage \ No newline at end of file diff --git a/package.json b/package.json index cc2955e334..c39a74ff95 100644 --- a/package.json +++ b/package.json @@ -4,14 +4,18 @@ "description": "Atomic Flux with hot reloading", "main": "lib/index.js", "scripts": { - "browser": "scripts/browser", - "build": "scripts/build", - "clean": "scripts/clean", - "lint": "scripts/lint", - "prepublish": "scripts/prepublish", - "test": "scripts/test", - "test:watch": "scripts/test-watch", - "test:cov": "scripts/test-cov" + "build": "npm run clean && babel src --out-dir lib && npm run build:umd", + "build:umd": "webpack src/index.js dist/redux.js && NODE_ENV=production webpack src/index.js dist/redux.min.js", + "clean": "rimraf lib dist coverage", + "lint": "eslint src test examples", + "prepublish": "npm run build", + "release": "mversion patch -m", + "release:patch": "mversion patch -m", + "release:minor": "mversion minor -m", + "release:major": "mversion major -m", + "test": "npm run lint && NODE_ENV=test mocha --compilers js:babel/register --recursive", + "test:watch": "npm test -- --watch", + "test:cov": "npm run lint && babel-node ./node_modules/.bin/isparta cover ./node_modules/.bin/_mocha -- --recursive" }, "repository": { "type": "git", @@ -47,6 +51,7 @@ "expect": "^1.6.0", "isparta": "^3.0.3", "mocha": "^2.2.5", + "mversion": "^1.10.0", "rimraf": "^2.3.4", "webpack": "^1.9.6", "webpack-dev-server": "^1.8.2" diff --git a/scripts/browser b/scripts/browser deleted file mode 100755 index 22bad9a9fe..0000000000 --- a/scripts/browser +++ /dev/null @@ -1,8 +0,0 @@ -#!/bin/sh -e - -WEBPACK_CMD=node_modules/.bin/webpack - -mkdir -p dist - -$WEBPACK_CMD src/index.js dist/redux.js -NODE_ENV=production $WEBPACK_CMD src/index.js dist/redux.min.js diff --git a/scripts/build b/scripts/build deleted file mode 100755 index 50a3a47dcd..0000000000 --- a/scripts/build +++ /dev/null @@ -1,4 +0,0 @@ -#!/bin/sh -e - -rm -rf lib -`npm bin`/babel src --out-dir lib diff --git a/scripts/clean b/scripts/clean deleted file mode 100755 index 4f674f6e10..0000000000 --- a/scripts/clean +++ /dev/null @@ -1,3 +0,0 @@ -#!/bin/sh - -`npm bin`/rimraf ./lib diff --git a/scripts/lint b/scripts/lint deleted file mode 100755 index a109587ffa..0000000000 --- a/scripts/lint +++ /dev/null @@ -1,3 +0,0 @@ -#!/bin/sh - -`npm bin`/eslint src test examples diff --git a/scripts/prepublish b/scripts/prepublish deleted file mode 100755 index 00260c55fa..0000000000 --- a/scripts/prepublish +++ /dev/null @@ -1,6 +0,0 @@ -#!/bin/sh -e - -sh scripts/lint -sh scripts/clean -sh scripts/browser -sh scripts/build diff --git a/scripts/test b/scripts/test deleted file mode 100755 index 8f4715ad68..0000000000 --- a/scripts/test +++ /dev/null @@ -1,3 +0,0 @@ -#!/bin/sh - -NODE_ENV=test `npm bin`/mocha --compilers js:babel/register --recursive \ No newline at end of file diff --git a/scripts/test-cov b/scripts/test-cov deleted file mode 100755 index f9be36d7a1..0000000000 --- a/scripts/test-cov +++ /dev/null @@ -1,3 +0,0 @@ -#!/bin/sh - -`npm bin`/babel-node `npm bin`/isparta cover `npm bin`/_mocha -- --recursive diff --git a/scripts/test-watch b/scripts/test-watch deleted file mode 100755 index ce4725bedb..0000000000 --- a/scripts/test-watch +++ /dev/null @@ -1,3 +0,0 @@ -#!/bin/sh - -NODE_ENV=test `npm bin`/mocha --compilers js:babel/register --recursive --watch \ No newline at end of file From 0623cf4dffd49d3bae987764beef2047c6822b58 Mon Sep 17 00:00:00 2001 From: Dan Abramov Date: Sun, 19 Jul 2015 19:33:14 +0300 Subject: [PATCH 33/84] Replace class with a factory function --- src/Store.js | 57 -------------------------- src/createStore.js | 64 ++++++++++++++++++++++++++---- src/utils/combineReducers.js | 2 +- test/utils/combineReducers.spec.js | 2 +- 4 files changed, 59 insertions(+), 66 deletions(-) delete mode 100644 src/Store.js diff --git a/src/Store.js b/src/Store.js deleted file mode 100644 index 2459fe8d31..0000000000 --- a/src/Store.js +++ /dev/null @@ -1,57 +0,0 @@ -import invariant from 'invariant'; -import isPlainObject from './utils/isPlainObject'; - -// Don't ever try to handle these action types in your code. They are private. -// For any unknown actions, you must return the current state. -// If the current state is undefined, you must return the initial state. -export const ActionTypes = { - INIT: '@@redux/INIT' -}; - -export default class Store { - constructor(reducer, initialState) { - invariant( - typeof reducer === 'function', - 'Expected the reducer to be a function.' - ); - - this.state = initialState; - this.listeners = []; - this.replaceReducer(reducer); - } - - getReducer() { - return this.reducer; - } - - replaceReducer(nextReducer) { - this.reducer = nextReducer; - this.dispatch({ type: ActionTypes.INIT }); - } - - dispatch(action) { - invariant( - isPlainObject(action), - 'Actions must be plain objects. Use custom middleware for async actions.' - ); - - const { reducer } = this; - this.state = reducer(this.state, action); - this.listeners.forEach(listener => listener()); - return action; - } - - getState() { - return this.state; - } - - subscribe(listener) { - const { listeners } = this; - listeners.push(listener); - - return function unsubscribe() { - const index = listeners.indexOf(listener); - listeners.splice(index, 1); - }; - } -} diff --git a/src/createStore.js b/src/createStore.js index 5344d42088..78ab81f1d7 100644 --- a/src/createStore.js +++ b/src/createStore.js @@ -1,13 +1,63 @@ -import Store from './Store'; +import invariant from 'invariant'; +import isPlainObject from './utils/isPlainObject'; + +// Don't ever try to handle these action types in your code. They are private. +// For any unknown actions, you must return the current state. +// If the current state is undefined, you must return the initial state. +export const ActionTypes = { + INIT: '@@redux/INIT' +}; export default function createStore(reducer, initialState) { - const store = new Store(reducer, initialState); + invariant( + typeof reducer === 'function', + 'Expected the reducer to be a function.' + ); + + let currentReducer = null; + let currentState = initialState; + let listeners = []; + + function getState() { + return currentState; + } + + function subscribe(listener) { + listeners.push(listener); + + return function unsubscribe() { + const index = listeners.indexOf(listener); + listeners.splice(index, 1); + }; + } + + function dispatch(action) { + invariant( + isPlainObject(action), + 'Actions must be plain objects. Use custom middleware for async actions.' + ); + + currentState = currentReducer(currentState, action); + listeners.forEach(listener => listener()); + return action; + } + + function getReducer() { + return currentReducer; + } + + function replaceReducer(nextReducer) { + currentReducer = nextReducer; + dispatch({ type: ActionTypes.INIT }); + } + + replaceReducer(reducer); return { - dispatch: ::store.dispatch, - subscribe: ::store.subscribe, - getState: ::store.getState, - getReducer: ::store.getReducer, - replaceReducer: ::store.replaceReducer + dispatch, + subscribe, + getState, + getReducer, + replaceReducer }; } diff --git a/src/utils/combineReducers.js b/src/utils/combineReducers.js index 4008a5587e..dbf6c966ec 100644 --- a/src/utils/combineReducers.js +++ b/src/utils/combineReducers.js @@ -1,7 +1,7 @@ import mapValues from '../utils/mapValues'; import pick from '../utils/pick'; import invariant from 'invariant'; -import { ActionTypes } from '../Store'; +import { ActionTypes } from '../createStore'; function getErrorMessage(key, action) { const actionType = action && action.type; diff --git a/test/utils/combineReducers.spec.js b/test/utils/combineReducers.spec.js index b72429e38a..66d1e4e7f0 100644 --- a/test/utils/combineReducers.spec.js +++ b/test/utils/combineReducers.spec.js @@ -1,6 +1,6 @@ import expect from 'expect'; import { combineReducers } from '../../src'; -import { ActionTypes } from '../../src/Store'; +import { ActionTypes } from '../../src/createStore'; describe('Utils', () => { describe('combineReducers', () => { From ac8a1841fbdae5f6c94363583cdfaaf792ffba90 Mon Sep 17 00:00:00 2001 From: Dan Abramov Date: Sun, 19 Jul 2015 22:07:25 +0300 Subject: [PATCH 34/84] Fix the missing Flow annotations --- src/createStore.js | 20 +++++++-------- src/index.js | 4 +-- src/types.js | 40 ++++++++++++++++++++++++++---- src/utils/applyMiddleware.js | 22 ++++++++++------ src/utils/combineReducers.js | 14 +++++------ src/utils/composeMiddleware.js | 10 +++++--- test/utils/applyMiddleware.spec.js | 4 +-- 7 files changed, 76 insertions(+), 38 deletions(-) diff --git a/src/createStore.js b/src/createStore.js index 4bb312d8f5..9e116a1ae9 100644 --- a/src/createStore.js +++ b/src/createStore.js @@ -1,6 +1,6 @@ /* @flow */ /*eslint-disable */ -import type { State, Reducer, Store } from './types'; +import type { State, Reducer, Action, IntermediateAction, Store } from './types'; /*eslint-enable */ import invariant from 'invariant'; @@ -9,7 +9,7 @@ import isPlainObject from './utils/isPlainObject'; // Don't ever try to handle these action types in your code. They are private. // For any unknown actions, you must return the current state. // If the current state is undefined, you must return the initial state. -export const ActionTypes = { +export var ActionTypes = { INIT: '@@redux/INIT' }; @@ -22,24 +22,24 @@ export default function createStore( 'Expected the reducer to be a function.' ); - let currentReducer = null; - let currentState = initialState; - let listeners = []; + var currentReducer = reducer; + var currentState = initialState; + var listeners = []; function getState() { return currentState; } - function subscribe(listener) { + function subscribe(listener: Function) { listeners.push(listener); return function unsubscribe() { - const index = listeners.indexOf(listener); + var index = listeners.indexOf(listener); listeners.splice(index, 1); }; } - function dispatch(action) { + function dispatch(action: Action) { invariant( isPlainObject(action), 'Actions must be plain objects. Use custom middleware for async actions.' @@ -54,12 +54,12 @@ export default function createStore( return currentReducer; } - function replaceReducer(nextReducer) { + function replaceReducer(nextReducer: Reducer) { currentReducer = nextReducer; dispatch({ type: ActionTypes.INIT }); } - replaceReducer(reducer); + dispatch({ type: ActionTypes.INIT }); return { dispatch, diff --git a/src/index.js b/src/index.js index 1cfbbf6489..dd0cdd5f11 100644 --- a/src/index.js +++ b/src/index.js @@ -1,7 +1,7 @@ -// Core +/* @flow */ + import createStore from './createStore'; -// Utilities import compose from './utils/compose'; import combineReducers from './utils/combineReducers'; import bindActionCreators from './utils/bindActionCreators'; diff --git a/src/types.js b/src/types.js index fb4c576fbd..740c1b039f 100644 --- a/src/types.js +++ b/src/types.js @@ -1,10 +1,40 @@ +/* @flow */ + export type State = any; + export type Action = Object; + export type IntermediateAction = any; + export type Dispatch = (a: Action | IntermediateAction) => any; + export type Reducer = (state: S, action: A) => S; -export type ActionCreator = (...args: any) => Action | IntermediateAction; -export type Middleware = (methods: { dispatch: Dispatch, getState: () => State }) => (next: Dispatch) => Dispatch; -export type Store = { dispatch: Dispatch, getState: State, subscribe: Function, getReducer: Reducer, replaceReducer: void }; -export type CreateStore = (reducer: Function, initialState: any) => Store; -export type HigherOrderStore = (next: CreateStore) => CreateStore; + +export type ActionCreator = (...args: any) => + Action | IntermediateAction; + +export type MiddlewareArgs = { + dispatch: Dispatch; + getState: () => State; +}; + +export type Middleware = (args: MiddlewareArgs) => + (next: Dispatch) => + Dispatch; + +export type Store = { + dispatch: Dispatch; + getState: () => State; + getReducer: Reducer; + replaceReducer: (nextReducer: Reducer) => void; + subscribe: (listener: () => void) => () => void; +}; + +export type CreateStore = ( + reducer: Reducer, + initialState: State +) => Store; + +export type HigherOrderStore = ( + next: CreateStore +) => CreateStore; diff --git a/src/utils/applyMiddleware.js b/src/utils/applyMiddleware.js index 2fb49c31b5..00140faf27 100644 --- a/src/utils/applyMiddleware.js +++ b/src/utils/applyMiddleware.js @@ -1,6 +1,9 @@ /* @flow */ /*eslint-disable */ -import type { Dispatch, CreateStore, Middleware } from '../types'; +import type { + Dispatch, Middleware, Reducer, State, + Store, CreateStore, HigherOrderStore +} from '../types'; /*eslint-enable */ import compose from './compose'; @@ -15,22 +18,25 @@ import composeMiddleware from './composeMiddleware'; */ export default function applyMiddleware( ...middlewares: Array -): CreateStore { - return (next: CreateStore) => (reducer, initialState) => { +): HigherOrderStore { + return (next: CreateStore) => (reducer: Reducer, initialState: State) => { var store = next(reducer, initialState); var middleware = composeMiddleware(...middlewares); - var composedDispatch = null; + var composedDispatch = () => {}; function dispatch(action) { return composedDispatch(action); } - var methods = { - dispatch, - getState: store.getState + var middlewareAPI = { + getState: store.getState, + dispatch }; - composedDispatch = compose(middleware(methods), store.dispatch); + composedDispatch = compose( + middleware(middlewareAPI), + store.dispatch + ); return { ...store, diff --git a/src/utils/combineReducers.js b/src/utils/combineReducers.js index 5f823b1ca2..54a74a4c82 100644 --- a/src/utils/combineReducers.js +++ b/src/utils/combineReducers.js @@ -8,9 +8,9 @@ import pick from '../utils/pick'; import invariant from 'invariant'; import { ActionTypes } from '../createStore'; -function getErrorMessage(key: String, action: Action): String { - const actionType = action && action.type; - const actionName = actionType && `"${actionType}"` || 'an action'; +function getErrorMessage(key: String, action: Action): string { + var actionType = action && action.type; + var actionName = actionType && `"${actionType}"` || 'an action'; return ( `Reducer "${key}" returned undefined handling ${actionName}. ` + @@ -19,10 +19,10 @@ function getErrorMessage(key: String, action: Action): String { } export default function combineReducers(reducers: Object): Reducer { - const finalReducers = pick(reducers, (val) => typeof val === 'function'); + var finalReducers = pick(reducers, (val) => typeof val === 'function'); Object.keys(finalReducers).forEach(key => { - const reducer = finalReducers[key]; + var reducer = finalReducers[key]; invariant( typeof reducer(undefined, { type: ActionTypes.INIT }) !== 'undefined', `Reducer "${key}" returned undefined during initialization. ` + @@ -31,7 +31,7 @@ export default function combineReducers(reducers: Object): Reducer { `not be undefined.` ); - const type = Math.random().toString(36).substring(7).split('').join('.'); + var type = Math.random().toString(36).substring(7).split('').join('.'); invariant( typeof reducer(undefined, { type }) !== 'undefined', `Reducer "${key}" returned undefined when probed with a random type. ` + @@ -45,7 +45,7 @@ export default function combineReducers(reducers: Object): Reducer { return function composition(state: State = {}, action: Action): State { return mapValues(finalReducers, (reducer, key) => { - const newState = reducer(state[key], action); + var newState = reducer(state[key], action); invariant( typeof newState !== 'undefined', getErrorMessage(key, action) diff --git a/src/utils/composeMiddleware.js b/src/utils/composeMiddleware.js index a3680d3b5c..8eee92b28c 100644 --- a/src/utils/composeMiddleware.js +++ b/src/utils/composeMiddleware.js @@ -1,7 +1,6 @@ /* @flow */ /*eslint-disable */ -import type { Dispatch, Middleware } from '../types'; -type StoreMethods = { dispatch: Dispatch, getState: () => State }; +import type { Dispatch, Middleware, MiddlewareArgs } from '../types'; /*eslint-enable */ import compose from './compose'; @@ -14,6 +13,9 @@ import compose from './compose'; export default function composeMiddleware( ...middlewares: Array ): Middleware { - return (methods: StoreMethods) => (next: Dispatch) => - compose(...middlewares.map(m => m(methods)), next); + return (args: MiddlewareArgs) => (next: Dispatch) => { + var dispatchChain = middlewares.map(middleware => middleware(args)); + dispatchChain.push(next); + return compose.apply(null, dispatchChain); + }; } diff --git a/test/utils/applyMiddleware.spec.js b/test/utils/applyMiddleware.spec.js index d3f8f60ea5..0ca73d84c0 100644 --- a/test/utils/applyMiddleware.spec.js +++ b/test/utils/applyMiddleware.spec.js @@ -22,8 +22,8 @@ describe('applyMiddleware', () => { expect(spy.calls.length).toEqual(1); expect(Object.keys(spy.calls[0].arguments[0])).toEqual([ - 'dispatch', - 'getState' + 'getState', + 'dispatch' ]); expect(store.getState()).toEqual([ { id: 1, text: 'Use Redux' }, { id: 2, text: 'Flux FTW!' } ]); From e355ccddfc93c5ec1bad3cac06ed98dc1964b097 Mon Sep 17 00:00:00 2001 From: Dan Abramov Date: Tue, 21 Jul 2015 20:39:57 +0300 Subject: [PATCH 35/84] Move Flow type definitions into index --- src/createStore.js | 2 +- src/index.js | 34 +++++++++++++++++++++++++++- src/types.js | 40 --------------------------------- src/utils/applyMiddleware.js | 2 +- src/utils/bindActionCreators.js | 2 +- src/utils/combineReducers.js | 2 +- src/utils/composeMiddleware.js | 2 +- 7 files changed, 38 insertions(+), 46 deletions(-) delete mode 100644 src/types.js diff --git a/src/createStore.js b/src/createStore.js index 9e116a1ae9..bc80d9d893 100644 --- a/src/createStore.js +++ b/src/createStore.js @@ -1,6 +1,6 @@ /* @flow */ /*eslint-disable */ -import type { State, Reducer, Action, IntermediateAction, Store } from './types'; +import type { State, Reducer, Action, IntermediateAction, Store } from './index'; /*eslint-enable */ import invariant from 'invariant'; diff --git a/src/index.js b/src/index.js index dd0cdd5f11..10d2296fe3 100644 --- a/src/index.js +++ b/src/index.js @@ -1,7 +1,39 @@ /* @flow */ -import createStore from './createStore'; +export type State = any; +export type Action = Object; +export type IntermediateAction = any; +export type Dispatch = (a: Action | IntermediateAction) => any; +export type Reducer = (state: S, action: A) => S; +export type ActionCreator = (...args: any) => Action | IntermediateAction; + +export type MiddlewareArgs = { + dispatch: Dispatch; + getState: () => State; +}; +export type Middleware = (args: MiddlewareArgs) => + (next: Dispatch) => + Dispatch; + +export type Store = { + dispatch: Dispatch; + getState: () => State; + getReducer: Reducer; + replaceReducer: (nextReducer: Reducer) => void; + subscribe: (listener: () => void) => () => void; +}; + +export type CreateStore = ( + reducer: Reducer, + initialState: State +) => Store; + +export type HigherOrderStore = ( + next: CreateStore +) => CreateStore; + +import createStore from './createStore'; import compose from './utils/compose'; import combineReducers from './utils/combineReducers'; import bindActionCreators from './utils/bindActionCreators'; diff --git a/src/types.js b/src/types.js deleted file mode 100644 index 740c1b039f..0000000000 --- a/src/types.js +++ /dev/null @@ -1,40 +0,0 @@ -/* @flow */ - -export type State = any; - -export type Action = Object; - -export type IntermediateAction = any; - -export type Dispatch = (a: Action | IntermediateAction) => any; - -export type Reducer = (state: S, action: A) => S; - -export type ActionCreator = (...args: any) => - Action | IntermediateAction; - -export type MiddlewareArgs = { - dispatch: Dispatch; - getState: () => State; -}; - -export type Middleware = (args: MiddlewareArgs) => - (next: Dispatch) => - Dispatch; - -export type Store = { - dispatch: Dispatch; - getState: () => State; - getReducer: Reducer; - replaceReducer: (nextReducer: Reducer) => void; - subscribe: (listener: () => void) => () => void; -}; - -export type CreateStore = ( - reducer: Reducer, - initialState: State -) => Store; - -export type HigherOrderStore = ( - next: CreateStore -) => CreateStore; diff --git a/src/utils/applyMiddleware.js b/src/utils/applyMiddleware.js index 00140faf27..05194da3e1 100644 --- a/src/utils/applyMiddleware.js +++ b/src/utils/applyMiddleware.js @@ -3,7 +3,7 @@ import type { Dispatch, Middleware, Reducer, State, Store, CreateStore, HigherOrderStore -} from '../types'; +} from '../index'; /*eslint-enable */ import compose from './compose'; diff --git a/src/utils/bindActionCreators.js b/src/utils/bindActionCreators.js index 0775b9d27a..ce1c3e8a21 100644 --- a/src/utils/bindActionCreators.js +++ b/src/utils/bindActionCreators.js @@ -1,6 +1,6 @@ /* @flow */ /*eslint-disable */ -import type { Dispatch } from '../types'; +import type { Dispatch } from '../index'; /*eslint-enable */ import mapValues from '../utils/mapValues'; diff --git a/src/utils/combineReducers.js b/src/utils/combineReducers.js index 54a74a4c82..5e3209e123 100644 --- a/src/utils/combineReducers.js +++ b/src/utils/combineReducers.js @@ -1,6 +1,6 @@ /* @flow */ /*eslint-disable */ -import type { Action, State, Reducer } from '../types'; +import type { Action, State, Reducer } from '../index'; /*eslint-enable */ import mapValues from '../utils/mapValues'; diff --git a/src/utils/composeMiddleware.js b/src/utils/composeMiddleware.js index 8eee92b28c..0544f03799 100644 --- a/src/utils/composeMiddleware.js +++ b/src/utils/composeMiddleware.js @@ -1,6 +1,6 @@ /* @flow */ /*eslint-disable */ -import type { Dispatch, Middleware, MiddlewareArgs } from '../types'; +import type { Dispatch, Middleware, MiddlewareArgs } from '../index'; /*eslint-enable */ import compose from './compose'; From a4b8aff95d5e194500bc8dcc130a6d7f71aadefa Mon Sep 17 00:00:00 2001 From: Dan Abramov Date: Tue, 21 Jul 2015 20:41:58 +0300 Subject: [PATCH 36/84] Remove src from .npmignore for Flow, add test instead --- .npmignore | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.npmignore b/.npmignore index 9458ec8184..f30eb6505d 100644 --- a/.npmignore +++ b/.npmignore @@ -1,2 +1,2 @@ -src examples +test From 7ad209a22d1ee0521c74dcc75d6609af349b5e5a Mon Sep 17 00:00:00 2001 From: Justin Hewlett Date: Tue, 21 Jul 2015 23:13:51 -0600 Subject: [PATCH 37/84] Allow es6 symbols to be used as action types - Symbols are a good way to ensure uniqueness of your action types - In Chrome and Firefox, a symbol inside of a template literal throws an error. Making the string conversion explicit gets rid of the error --- src/utils/combineReducers.js | 4 ++-- test/utils/combineReducers.spec.js | 17 +++++++++++++++++ 2 files changed, 19 insertions(+), 2 deletions(-) diff --git a/src/utils/combineReducers.js b/src/utils/combineReducers.js index 54a74a4c82..3ac91cb1f3 100644 --- a/src/utils/combineReducers.js +++ b/src/utils/combineReducers.js @@ -10,8 +10,8 @@ import { ActionTypes } from '../createStore'; function getErrorMessage(key: String, action: Action): string { var actionType = action && action.type; - var actionName = actionType && `"${actionType}"` || 'an action'; - + var actionName = actionType && `"${actionType.toString()}"` || 'an action'; + return ( `Reducer "${key}" returned undefined handling ${actionName}. ` + `To ignore an action, you must explicitly return the previous state.` diff --git a/test/utils/combineReducers.spec.js b/test/utils/combineReducers.spec.js index 66d1e4e7f0..8b62e476f8 100644 --- a/test/utils/combineReducers.spec.js +++ b/test/utils/combineReducers.spec.js @@ -83,6 +83,23 @@ describe('Utils', () => { ); }); + it('should allow a symbol to be used as an action type', () => { + const increment = Symbol('INCREMENT') + + const reducer = combineReducers({ + counter(state = 0, action) { + switch (action.type) { + case increment: + return state + 1; + default: + return state; + } + } + }); + + expect(reducer(0, {type: increment}).counter).toEqual(1) + }); + it('should throw an error if a reducer attempts to handle a private action', () => { expect(() => combineReducers({ counter(state, action) { From adbfbdde0a561f25bd810b20bac03dd2be9a693c Mon Sep 17 00:00:00 2001 From: Justin Hewlett Date: Tue, 21 Jul 2015 23:40:23 -0600 Subject: [PATCH 38/84] Fix lint errors --- src/utils/combineReducers.js | 2 +- test/utils/combineReducers.spec.js | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/src/utils/combineReducers.js b/src/utils/combineReducers.js index 3ac91cb1f3..45e5943f35 100644 --- a/src/utils/combineReducers.js +++ b/src/utils/combineReducers.js @@ -11,7 +11,7 @@ import { ActionTypes } from '../createStore'; function getErrorMessage(key: String, action: Action): string { var actionType = action && action.type; var actionName = actionType && `"${actionType.toString()}"` || 'an action'; - + return ( `Reducer "${key}" returned undefined handling ${actionName}. ` + `To ignore an action, you must explicitly return the previous state.` diff --git a/test/utils/combineReducers.spec.js b/test/utils/combineReducers.spec.js index 8b62e476f8..21be0a3529 100644 --- a/test/utils/combineReducers.spec.js +++ b/test/utils/combineReducers.spec.js @@ -84,7 +84,7 @@ describe('Utils', () => { }); it('should allow a symbol to be used as an action type', () => { - const increment = Symbol('INCREMENT') + const increment = Symbol('INCREMENT'); const reducer = combineReducers({ counter(state = 0, action) { @@ -97,7 +97,7 @@ describe('Utils', () => { } }); - expect(reducer(0, {type: increment}).counter).toEqual(1) + expect(reducer(0, {type: increment}).counter).toEqual(1); }); it('should throw an error if a reducer attempts to handle a private action', () => { From 55776add68c1a13e053d6a580a1fb0fa322260a3 Mon Sep 17 00:00:00 2001 From: Dan Abramov Date: Wed, 22 Jul 2015 15:52:06 +0300 Subject: [PATCH 39/84] Fix Flow signature for Store#getReducer --- src/index.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/index.js b/src/index.js index 10d2296fe3..e616778dd3 100644 --- a/src/index.js +++ b/src/index.js @@ -19,7 +19,7 @@ export type Middleware = (args: MiddlewareArgs) => export type Store = { dispatch: Dispatch; getState: () => State; - getReducer: Reducer; + getReducer: () => Reducer; replaceReducer: (nextReducer: Reducer) => void; subscribe: (listener: () => void) => () => void; }; From 3d252c3846c85631c9eb441af7c3d6306c0ea1dc Mon Sep 17 00:00:00 2001 From: Dan Abramov Date: Wed, 22 Jul 2015 15:55:41 +0300 Subject: [PATCH 40/84] Exclude coverage files from NPM --- .npmignore | 1 + 1 file changed, 1 insertion(+) diff --git a/.npmignore b/.npmignore index f30eb6505d..2eba144fa9 100644 --- a/.npmignore +++ b/.npmignore @@ -1,2 +1,3 @@ examples test +coverage From ed3edea4ca7db4ff6f645c555da6c0d0c39d1493 Mon Sep 17 00:00:00 2001 From: Dan Abramov Date: Wed, 22 Jul 2015 16:12:36 +0300 Subject: [PATCH 41/84] Tweak build scripts --- .mversionrc | 9 --------- package.json | 20 ++++++++++---------- 2 files changed, 10 insertions(+), 19 deletions(-) delete mode 100644 .mversionrc diff --git a/.mversionrc b/.mversionrc deleted file mode 100644 index 3cde5c762b..0000000000 --- a/.mversionrc +++ /dev/null @@ -1,9 +0,0 @@ -{ - "commitMessage": "%s", - "tagName": "v%s", - "scripts": { - "preupdate": "npm run build && npm test", - "postcommit": "git push && git push --tags && npm publish", - "postupdate": "echo 'New version %s tagged and released'" - } -} diff --git a/package.json b/package.json index c39a74ff95..6c4b443531 100644 --- a/package.json +++ b/package.json @@ -4,18 +4,19 @@ "description": "Atomic Flux with hot reloading", "main": "lib/index.js", "scripts": { - "build": "npm run clean && babel src --out-dir lib && npm run build:umd", - "build:umd": "webpack src/index.js dist/redux.js && NODE_ENV=production webpack src/index.js dist/redux.min.js", "clean": "rimraf lib dist coverage", "lint": "eslint src test examples", - "prepublish": "npm run build", - "release": "mversion patch -m", - "release:patch": "mversion patch -m", - "release:minor": "mversion minor -m", - "release:major": "mversion major -m", - "test": "npm run lint && NODE_ENV=test mocha --compilers js:babel/register --recursive", + "test": "NODE_ENV=test mocha --compilers js:babel/register --recursive", "test:watch": "npm test -- --watch", - "test:cov": "npm run lint && babel-node ./node_modules/.bin/isparta cover ./node_modules/.bin/_mocha -- --recursive" + "test:cov": "babel-node ./node_modules/.bin/isparta cover ./node_modules/.bin/_mocha -- --recursive", + "check": "npm run lint && npm run test", + "build:lib": "babel src --out-dir lib", + "build:umd": "webpack src/index.js dist/redux.js && NODE_ENV=production webpack src/index.js dist/redux.min.js", + "build": "npm run build:lib && npm run build:umd", + "preversion": "npm run clean && npm run check", + "version": "npm run build", + "postversion": "git push && git push --tags && npm run clean", + "prepublish": "npm run clean && npm run build" }, "repository": { "type": "git", @@ -51,7 +52,6 @@ "expect": "^1.6.0", "isparta": "^3.0.3", "mocha": "^2.2.5", - "mversion": "^1.10.0", "rimraf": "^2.3.4", "webpack": "^1.9.6", "webpack-dev-server": "^1.8.2" From 9321deabe290775ca883656ad2ba9f047d52a328 Mon Sep 17 00:00:00 2001 From: Dan Abramov Date: Wed, 22 Jul 2015 19:17:26 +0300 Subject: [PATCH 42/84] Remove Flow types for now --- src/createStore.js | 16 ++++----------- src/index.js | 35 --------------------------------- src/utils/applyMiddleware.js | 14 ++----------- src/utils/bindActionCreators.js | 10 +--------- src/utils/combineReducers.js | 11 +++-------- src/utils/compose.js | 4 +--- src/utils/composeMiddleware.js | 14 +++---------- src/utils/isPlainObject.js | 4 +--- src/utils/mapValues.js | 4 +--- src/utils/pick.js | 4 +--- 10 files changed, 17 insertions(+), 99 deletions(-) diff --git a/src/createStore.js b/src/createStore.js index bc80d9d893..f7b78ca9fe 100644 --- a/src/createStore.js +++ b/src/createStore.js @@ -1,8 +1,3 @@ -/* @flow */ -/*eslint-disable */ -import type { State, Reducer, Action, IntermediateAction, Store } from './index'; -/*eslint-enable */ - import invariant from 'invariant'; import isPlainObject from './utils/isPlainObject'; @@ -13,10 +8,7 @@ export var ActionTypes = { INIT: '@@redux/INIT' }; -export default function createStore( - reducer: Reducer, - initialState: State -): Store { +export default function createStore(reducer, initialState) { invariant( typeof reducer === 'function', 'Expected the reducer to be a function.' @@ -30,7 +22,7 @@ export default function createStore( return currentState; } - function subscribe(listener: Function) { + function subscribe(listener) { listeners.push(listener); return function unsubscribe() { @@ -39,7 +31,7 @@ export default function createStore( }; } - function dispatch(action: Action) { + function dispatch(action) { invariant( isPlainObject(action), 'Actions must be plain objects. Use custom middleware for async actions.' @@ -54,7 +46,7 @@ export default function createStore( return currentReducer; } - function replaceReducer(nextReducer: Reducer) { + function replaceReducer(nextReducer) { currentReducer = nextReducer; dispatch({ type: ActionTypes.INIT }); } diff --git a/src/index.js b/src/index.js index e616778dd3..06efcb70d1 100644 --- a/src/index.js +++ b/src/index.js @@ -1,38 +1,3 @@ -/* @flow */ - -export type State = any; -export type Action = Object; -export type IntermediateAction = any; -export type Dispatch = (a: Action | IntermediateAction) => any; -export type Reducer = (state: S, action: A) => S; -export type ActionCreator = (...args: any) => Action | IntermediateAction; - -export type MiddlewareArgs = { - dispatch: Dispatch; - getState: () => State; -}; - -export type Middleware = (args: MiddlewareArgs) => - (next: Dispatch) => - Dispatch; - -export type Store = { - dispatch: Dispatch; - getState: () => State; - getReducer: () => Reducer; - replaceReducer: (nextReducer: Reducer) => void; - subscribe: (listener: () => void) => () => void; -}; - -export type CreateStore = ( - reducer: Reducer, - initialState: State -) => Store; - -export type HigherOrderStore = ( - next: CreateStore -) => CreateStore; - import createStore from './createStore'; import compose from './utils/compose'; import combineReducers from './utils/combineReducers'; diff --git a/src/utils/applyMiddleware.js b/src/utils/applyMiddleware.js index 05194da3e1..2cb09f9b47 100644 --- a/src/utils/applyMiddleware.js +++ b/src/utils/applyMiddleware.js @@ -1,11 +1,3 @@ -/* @flow */ -/*eslint-disable */ -import type { - Dispatch, Middleware, Reducer, State, - Store, CreateStore, HigherOrderStore -} from '../index'; -/*eslint-enable */ - import compose from './compose'; import composeMiddleware from './composeMiddleware'; @@ -16,10 +8,8 @@ import composeMiddleware from './composeMiddleware'; * @param {...Function} ...middlewares * @return {Function} A higher-order store */ -export default function applyMiddleware( - ...middlewares: Array -): HigherOrderStore { - return (next: CreateStore) => (reducer: Reducer, initialState: State) => { +export default function applyMiddleware(...middlewares) { + return (next) => (reducer, initialState) => { var store = next(reducer, initialState); var middleware = composeMiddleware(...middlewares); var composedDispatch = () => {}; diff --git a/src/utils/bindActionCreators.js b/src/utils/bindActionCreators.js index ce1c3e8a21..f9a81a5a80 100644 --- a/src/utils/bindActionCreators.js +++ b/src/utils/bindActionCreators.js @@ -1,14 +1,6 @@ -/* @flow */ -/*eslint-disable */ -import type { Dispatch } from '../index'; -/*eslint-enable */ - import mapValues from '../utils/mapValues'; -export default function bindActionCreators( - actionCreators: Object, - dispatch: Dispatch -): Object { +export default function bindActionCreators(actionCreators, dispatch) { return mapValues(actionCreators, actionCreator => (...args) => dispatch(actionCreator(...args)) ); diff --git a/src/utils/combineReducers.js b/src/utils/combineReducers.js index 5e3209e123..d5329c4248 100644 --- a/src/utils/combineReducers.js +++ b/src/utils/combineReducers.js @@ -1,14 +1,9 @@ -/* @flow */ -/*eslint-disable */ -import type { Action, State, Reducer } from '../index'; -/*eslint-enable */ - import mapValues from '../utils/mapValues'; import pick from '../utils/pick'; import invariant from 'invariant'; import { ActionTypes } from '../createStore'; -function getErrorMessage(key: String, action: Action): string { +function getErrorMessage(key, action) { var actionType = action && action.type; var actionName = actionType && `"${actionType}"` || 'an action'; @@ -18,7 +13,7 @@ function getErrorMessage(key: String, action: Action): string { ); } -export default function combineReducers(reducers: Object): Reducer { +export default function combineReducers(reducers) { var finalReducers = pick(reducers, (val) => typeof val === 'function'); Object.keys(finalReducers).forEach(key => { @@ -43,7 +38,7 @@ export default function combineReducers(reducers: Object): Reducer { ); }); - return function composition(state: State = {}, action: Action): State { + return function composition(state = {}, action) { return mapValues(finalReducers, (reducer, key) => { var newState = reducer(state[key], action); invariant( diff --git a/src/utils/compose.js b/src/utils/compose.js index b4b291e015..4db0884d3b 100644 --- a/src/utils/compose.js +++ b/src/utils/compose.js @@ -1,10 +1,8 @@ -/* @flow */ - /** * Composes functions from left to right * @param {...Function} funcs - Functions to compose * @return {Function} */ -export default function compose(...funcs: Array): Function { +export default function compose(...funcs) { return funcs.reduceRight((composed, f) => f(composed)); } diff --git a/src/utils/composeMiddleware.js b/src/utils/composeMiddleware.js index 0544f03799..98927a21b5 100644 --- a/src/utils/composeMiddleware.js +++ b/src/utils/composeMiddleware.js @@ -1,8 +1,3 @@ -/* @flow */ -/*eslint-disable */ -import type { Dispatch, Middleware, MiddlewareArgs } from '../index'; -/*eslint-enable */ - import compose from './compose'; /** @@ -10,12 +5,9 @@ import compose from './compose'; * @param {...Function} middlewares * @return {Function} */ -export default function composeMiddleware( - ...middlewares: Array -): Middleware { - return (args: MiddlewareArgs) => (next: Dispatch) => { +export default function composeMiddleware(...middlewares) { + return args => rawDispatch => { var dispatchChain = middlewares.map(middleware => middleware(args)); - dispatchChain.push(next); - return compose.apply(null, dispatchChain); + return compose(...dispatchChain, rawDispatch); }; } diff --git a/src/utils/isPlainObject.js b/src/utils/isPlainObject.js index b352d71181..e42b4a6e72 100644 --- a/src/utils/isPlainObject.js +++ b/src/utils/isPlainObject.js @@ -1,6 +1,4 @@ -/* @flow */ - -export default function isPlainObject(obj: Object): boolean { +export default function isPlainObject(obj) { if (!obj) { return false; } diff --git a/src/utils/mapValues.js b/src/utils/mapValues.js index 9e09aeaab8..29d203cf61 100644 --- a/src/utils/mapValues.js +++ b/src/utils/mapValues.js @@ -1,6 +1,4 @@ -/* @flow */ - -export default function mapValues(obj: Object, fn: Function): Object { +export default function mapValues(obj, fn) { return Object.keys(obj).reduce((result, key) => { result[key] = fn(obj[key], key); return result; diff --git a/src/utils/pick.js b/src/utils/pick.js index 7025cb35e3..2c9719c1c0 100644 --- a/src/utils/pick.js +++ b/src/utils/pick.js @@ -1,6 +1,4 @@ -/* @flow */ - -export default function pick(obj: Object, fn: Function): Object { +export default function pick(obj, fn) { return Object.keys(obj).reduce((result, key) => { if (fn(obj[key])) { result[key] = obj[key]; From 100ce3cd053a1340fe813ccb170f4427a61af2ee Mon Sep 17 00:00:00 2001 From: Dan Abramov Date: Wed, 22 Jul 2015 19:28:54 +0300 Subject: [PATCH 43/84] composeMiddleware is an implementation detail of applyMiddleware --- src/index.js | 4 +--- test/utils/composeMiddleware.spec.js | 17 ----------------- 2 files changed, 1 insertion(+), 20 deletions(-) delete mode 100644 test/utils/composeMiddleware.spec.js diff --git a/src/index.js b/src/index.js index 06efcb70d1..0679d67aab 100644 --- a/src/index.js +++ b/src/index.js @@ -3,13 +3,11 @@ import compose from './utils/compose'; import combineReducers from './utils/combineReducers'; import bindActionCreators from './utils/bindActionCreators'; import applyMiddleware from './utils/applyMiddleware'; -import composeMiddleware from './utils/composeMiddleware'; export { createStore, compose, combineReducers, bindActionCreators, - applyMiddleware, - composeMiddleware + applyMiddleware }; diff --git a/test/utils/composeMiddleware.spec.js b/test/utils/composeMiddleware.spec.js deleted file mode 100644 index 8b5192faea..0000000000 --- a/test/utils/composeMiddleware.spec.js +++ /dev/null @@ -1,17 +0,0 @@ -import expect from 'expect'; -import { composeMiddleware } from '../../src'; - -describe('Utils', () => { - describe('composeMiddleware', () => { - it('should return combined middleware that executes from left to right', () => { - const a = () => next => action => next(action + 'a'); - const b = () => next => action => next(action + 'b'); - const c = () => next => action => next(action + 'c'); - const dispatch = action => action; - - expect(composeMiddleware(a, b, c)()(dispatch)('')).toBe('abc'); - expect(composeMiddleware(b, c, a)()(dispatch)('')).toBe('bca'); - expect(composeMiddleware(c, a, b)()(dispatch)('')).toBe('cab'); - }); - }); -}); From af474ba34cb3aae31c418228561f6e23a4b66bb2 Mon Sep 17 00:00:00 2001 From: Dan Abramov Date: Wed, 22 Jul 2015 19:53:40 +0300 Subject: [PATCH 44/84] Inline composeMiddleware because it is not used outside applyMiddleware --- src/utils/applyMiddleware.js | 18 +++++------------- src/utils/composeMiddleware.js | 13 ------------- 2 files changed, 5 insertions(+), 26 deletions(-) delete mode 100644 src/utils/composeMiddleware.js diff --git a/src/utils/applyMiddleware.js b/src/utils/applyMiddleware.js index 2cb09f9b47..0f320f293e 100644 --- a/src/utils/applyMiddleware.js +++ b/src/utils/applyMiddleware.js @@ -1,5 +1,4 @@ import compose from './compose'; -import composeMiddleware from './composeMiddleware'; /** * Creates a higher-order store that applies middleware to a store's dispatch. @@ -11,22 +10,15 @@ import composeMiddleware from './composeMiddleware'; export default function applyMiddleware(...middlewares) { return (next) => (reducer, initialState) => { var store = next(reducer, initialState); - var middleware = composeMiddleware(...middlewares); - var composedDispatch = () => {}; - - function dispatch(action) { - return composedDispatch(action); - } + var dispatch = store.dispatch; + var chain = []; var middlewareAPI = { getState: store.getState, - dispatch + dispatch: (action) => dispatch(action) }; - - composedDispatch = compose( - middleware(middlewareAPI), - store.dispatch - ); + chain = middlewares.map(middleware => middleware(middlewareAPI)); + dispatch = compose(...chain, store.dispatch); return { ...store, diff --git a/src/utils/composeMiddleware.js b/src/utils/composeMiddleware.js deleted file mode 100644 index 98927a21b5..0000000000 --- a/src/utils/composeMiddleware.js +++ /dev/null @@ -1,13 +0,0 @@ -import compose from './compose'; - -/** - * Compose middleware from left to right - * @param {...Function} middlewares - * @return {Function} - */ -export default function composeMiddleware(...middlewares) { - return args => rawDispatch => { - var dispatchChain = middlewares.map(middleware => middleware(args)); - return compose(...dispatchChain, rawDispatch); - }; -} From 27752e6033e9b5f27ed58a0f56919171f5a04c89 Mon Sep 17 00:00:00 2001 From: Dan Abramov Date: Wed, 22 Jul 2015 19:56:12 +0300 Subject: [PATCH 45/84] Style fixup --- test/utils/combineReducers.spec.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test/utils/combineReducers.spec.js b/test/utils/combineReducers.spec.js index 21be0a3529..dba2069dcf 100644 --- a/test/utils/combineReducers.spec.js +++ b/test/utils/combineReducers.spec.js @@ -97,7 +97,7 @@ describe('Utils', () => { } }); - expect(reducer(0, {type: increment}).counter).toEqual(1); + expect(reducer(0, { type: increment }).counter).toEqual(1); }); it('should throw an error if a reducer attempts to handle a private action', () => { From 259c6bfe152f7fa7065b0ac2e038466c100fa18b Mon Sep 17 00:00:00 2001 From: Dan Abramov Date: Wed, 22 Jul 2015 21:20:24 +0300 Subject: [PATCH 46/84] Add JSDoc annotations --- .eslintrc | 2 + src/createStore.js | 80 +++++++++++++++++++++++++++++++-- src/index.js | 6 +-- src/utils/applyMiddleware.js | 12 +++-- src/utils/bindActionCreators.js | 15 +++++++ src/utils/combineReducers.js | 18 +++++++- src/utils/compose.js | 9 ++-- src/utils/isPlainObject.js | 4 ++ src/utils/mapValues.js | 7 +++ src/utils/pick.js | 7 +++ 10 files changed, 147 insertions(+), 13 deletions(-) diff --git a/.eslintrc b/.eslintrc index 90e3903312..c501c8bf4c 100644 --- a/.eslintrc +++ b/.eslintrc @@ -6,6 +6,8 @@ "node": true }, "rules": { + "valid-jsdoc": 2, + "react/jsx-uses-react": 2, "react/jsx-uses-vars": 2, "react/react-in-jsx-scope": 2, diff --git a/src/createStore.js b/src/createStore.js index f7b78ca9fe..2cbfd1b18b 100644 --- a/src/createStore.js +++ b/src/createStore.js @@ -1,13 +1,34 @@ import invariant from 'invariant'; import isPlainObject from './utils/isPlainObject'; -// Don't ever try to handle these action types in your code. They are private. -// For any unknown actions, you must return the current state. -// If the current state is undefined, you must return the initial state. +/** + * These are private action types reserved by Redux. + * For any unknown actions, you must return the current state. + * If the current state is undefined, you must return the initial state. + * Do not reference these action types directly in your code. + */ export var ActionTypes = { INIT: '@@redux/INIT' }; +/** + * Creates a Redux store that holds the state tree. + * The only way to change the data in the store is to call `dispatch()` on it. + * + * There should only be a single store in your app. To specify how different + * parts of the state tree respond to actions, you may combine several reducers + * into a single reducer function by using `combineReducers`. + * + * @param {Function} reducer A function that returns the next state tree, given + * the current state tree and the action to handle. + * + * @param {any} initialState The initial state. You may optionally specify it + * to hydrate the state from the server in universal apps, or to restore a + * previously serialized user session. + * + * @returns {Store} A Redux store that lets you read the state, dispatch actions + * and subscribe to changes. + */ export default function createStore(reducer, initialState) { invariant( typeof reducer === 'function', @@ -18,10 +39,23 @@ export default function createStore(reducer, initialState) { var currentState = initialState; var listeners = []; + /** + * Reads the state tree managed by the store. + * + * @returns {any} The current state tree of your application. + */ function getState() { return currentState; } + /** + * Adds a change listener. It will be called any time an action is dispatched, + * and some part of the state tree may potentially have changed. You may then + * call `getState()` to read the current state tree inside the callback. + * + * @param {Function} listener A callback to be invoked on every dispatch. + * @returns {Function} A function to remove this change listener. + */ function subscribe(listener) { listeners.push(listener); @@ -31,6 +65,28 @@ export default function createStore(reducer, initialState) { }; } + /** + * Dispatches an action. It is the only way to trigger a state change. + * + * The `reducer` function the store was created with will be called with the + * current state tree and the and the given `action`. Its return value will + * be considered the next state of the tree, and the change listeners will be + * notified. + * + * The base implementation only supports plain object actions. If you want to + * dispatch a promise, an observable, a thunk, or something else, you need to + * wrap your store creating function into the corresponding middleware. For + * example, see the documentation for the `redux-thunk` package. Even the + * middleware will eventually dispatch plain object actions using this method. + * + * @param {Object} action A plain object representing “what changed”. It is + * a good idea to keep actions serializable so you can record and replay user + * sessions, or use the time travelling Redux developer tools. + * + * @returns {Object} For convenience, the same action object you dispatched. + * Note that, if you use a custom middleware, it may wrap `dispatch()` to + * return something else (for example, a Promise you can await). + */ function dispatch(action) { invariant( isPlainObject(action), @@ -42,10 +98,28 @@ export default function createStore(reducer, initialState) { return action; } + /** + * Returns the reducer currently used by the store to calculate the state. + * + * It is likely that you will only need this function if you implement a hot + * reloading mechanism for Redux. + * + * @returns {Function} The reducer used by the current store. + */ function getReducer() { return currentReducer; } + /** + * Replaces the reducer currently used by the store to calculate the state. + * + * You might need this if your app implements code splitting and you want to + * load some of the reducers dynamically. You might also need this if you + * implement a hot reloading mechanism for Redux. + * + * @param {Function} nextReducer The reducer for the store to use instead. + * @returns {void} + */ function replaceReducer(nextReducer) { currentReducer = nextReducer; dispatch({ type: ActionTypes.INIT }); diff --git a/src/index.js b/src/index.js index 0679d67aab..6bfc4d80f9 100644 --- a/src/index.js +++ b/src/index.js @@ -1,13 +1,13 @@ import createStore from './createStore'; -import compose from './utils/compose'; import combineReducers from './utils/combineReducers'; import bindActionCreators from './utils/bindActionCreators'; import applyMiddleware from './utils/applyMiddleware'; +import compose from './utils/compose'; export { createStore, - compose, combineReducers, bindActionCreators, - applyMiddleware + applyMiddleware, + compose }; diff --git a/src/utils/applyMiddleware.js b/src/utils/applyMiddleware.js index 0f320f293e..f3852d1d51 100644 --- a/src/utils/applyMiddleware.js +++ b/src/utils/applyMiddleware.js @@ -1,11 +1,17 @@ import compose from './compose'; /** - * Creates a higher-order store that applies middleware to a store's dispatch. + * Creates a higher-order store that applies middleware to the dispatch method + * of the Redux store. This is handy for a variety of tasks, such as expressing + * asynchronous actions in a concise manner, or logging every action payload. + * + * See `redux-thunk` package as an example of the Redux middleware. + * * Because middleware is potentially asynchronous, this should be the first * higher-order store in the composition chain. - * @param {...Function} ...middlewares - * @return {Function} A higher-order store + * + * @param {...Function} middlewares The middleware chain to be applied. + * @returns {Function} A higher-order store. */ export default function applyMiddleware(...middlewares) { return (next) => (reducer, initialState) => { diff --git a/src/utils/bindActionCreators.js b/src/utils/bindActionCreators.js index f9a81a5a80..a895a7ebb1 100644 --- a/src/utils/bindActionCreators.js +++ b/src/utils/bindActionCreators.js @@ -1,5 +1,20 @@ import mapValues from '../utils/mapValues'; +/** + * Turns an object whose values are action creators, into an object with the + * same keys, but with every function wrapped into a `dispatch` call so they + * may be invoked directly. This is just a convenience method, as you can call + * `store.dispatch(MyActionCreators.doSomething())` yourself just fine. + * + * @param {Object} actionCreators An object whose values are action creator + * functions. One handy way to obtain it is to use ES6 `import * as` syntax. + * + * @param {Function} dispatch The `dispatch` function available on your Redux + * store. + * + * @returns {Object} The object mimicking the original object, but with every + * action creator wrapped into the `dispatch` call. + */ export default function bindActionCreators(actionCreators, dispatch) { return mapValues(actionCreators, actionCreator => (...args) => dispatch(actionCreator(...args)) diff --git a/src/utils/combineReducers.js b/src/utils/combineReducers.js index 84904e7c95..e59e4d0976 100644 --- a/src/utils/combineReducers.js +++ b/src/utils/combineReducers.js @@ -13,6 +13,22 @@ function getErrorMessage(key, action) { ); } +/** + * Turns an object whose values are different reducer functions, into a single + * reducer function. It will call every child reducer, and gather their results + * into a single state object, whose keys correspond to the keys of the passed + * reducer functions. + * + * @param {Object} reducers An object whose values correspond to different + * reducer functions that need to be combined into one. One handy way to obtain + * it is to use ES6 `import * as reducers` syntax. The reducers may never return + * undefined for any action. Instead, they should return their initial state + * if the state passed to them was undefined, and the current state for any + * unrecognized action. + * + * @returns {Function} A reducer function that invokes every reducer inside the + * passed object, and builds a state object with the same shape. + */ export default function combineReducers(reducers) { var finalReducers = pick(reducers, (val) => typeof val === 'function'); @@ -38,7 +54,7 @@ export default function combineReducers(reducers) { ); }); - return function composition(state = {}, action) { + return function combination(state = {}, action) { return mapValues(finalReducers, (reducer, key) => { var newState = reducer(state[key], action); invariant( diff --git a/src/utils/compose.js b/src/utils/compose.js index 4db0884d3b..9d241b32cd 100644 --- a/src/utils/compose.js +++ b/src/utils/compose.js @@ -1,7 +1,10 @@ /** - * Composes functions from left to right - * @param {...Function} funcs - Functions to compose - * @return {Function} + * Composes functions from left to right. + * + * @param {...Function} funcs - The functions to compose. + * @returns {Function} A function that passes its only argument to the first of + * the `funcs`, then pipes its return value to the second one, and so on, until + * the last of the `funcs` is called, and its result is returned. */ export default function compose(...funcs) { return funcs.reduceRight((composed, f) => f(composed)); diff --git a/src/utils/isPlainObject.js b/src/utils/isPlainObject.js index e42b4a6e72..a5896655e5 100644 --- a/src/utils/isPlainObject.js +++ b/src/utils/isPlainObject.js @@ -1,3 +1,7 @@ +/** + * @param {any} obj The object to inspect. + * @returns {boolean} True if the argument appears to be a plain object. + */ export default function isPlainObject(obj) { if (!obj) { return false; diff --git a/src/utils/mapValues.js b/src/utils/mapValues.js index 29d203cf61..bb87ac551d 100644 --- a/src/utils/mapValues.js +++ b/src/utils/mapValues.js @@ -1,3 +1,10 @@ +/** + * Applies a function to every key-value pair inside an object. + * + * @param {Object} obj The source object. + * @param {Function} fn The mapper function taht receives the value and the key. + * @returns {Object} A new object that contains the mapped values for the keys. + */ export default function mapValues(obj, fn) { return Object.keys(obj).reduce((result, key) => { result[key] = fn(obj[key], key); diff --git a/src/utils/pick.js b/src/utils/pick.js index 2c9719c1c0..518c055e61 100644 --- a/src/utils/pick.js +++ b/src/utils/pick.js @@ -1,3 +1,10 @@ +/** + * Picks key-value pairs from an object where values satisfy a predicate. + * + * @param {Object} obj The object to pick from. + * @param {Function} fn The predicate the values must satisfy to be copied. + * @returns {Object} The object with the values that satisfied the predicate. + */ export default function pick(obj, fn) { return Object.keys(obj).reduce((result, key) => { if (fn(obj[key])) { From be4b589fd76f20d00aacf86c886807df0289baf3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Michal=20Kvasni=C4=8D=C3=A1k?= Date: Thu, 23 Jul 2015 16:15:22 +0200 Subject: [PATCH 47/84] fix isPlainObject, is comparing object stringified object constructors instead of prototypes (did not work from iframes because Object in iframe is not the same as in parent) --- src/utils/isPlainObject.js | 17 ++++++++++++++--- 1 file changed, 14 insertions(+), 3 deletions(-) diff --git a/src/utils/isPlainObject.js b/src/utils/isPlainObject.js index a5896655e5..faa72663a8 100644 --- a/src/utils/isPlainObject.js +++ b/src/utils/isPlainObject.js @@ -1,12 +1,23 @@ +const fnToString = (fn) => Function.prototype.toString.call(fn); + /** * @param {any} obj The object to inspect. * @returns {boolean} True if the argument appears to be a plain object. */ export default function isPlainObject(obj) { - if (!obj) { + if (!obj || typeof obj !== 'object') { return false; } - return typeof obj === 'object' && - Object.getPrototypeOf(obj) === Object.prototype; + const proto = typeof obj.constructor === 'function' ? Object.getPrototypeOf(obj) : Object.prototype; + + if (proto === null) { + return true; + } + + var constructor = proto.constructor; + + return typeof constructor === 'function' + && constructor instanceof constructor + && fnToString(constructor) === fnToString(Object); } From 0c87ead7294dba218278a5839fdd4aae759a0fd2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Michal=20Kvasni=C4=8D=C3=A1k?= Date: Thu, 23 Jul 2015 16:50:33 +0200 Subject: [PATCH 48/84] add contextify to simulate another realm (context) and test case where plain object from another realm is checked --- package.json | 1 + test/utils/isPlainObject.spec.js | 7 +++++++ 2 files changed, 8 insertions(+) diff --git a/package.json b/package.json index 6c4b443531..c9c8325172 100644 --- a/package.json +++ b/package.json @@ -46,6 +46,7 @@ "babel-core": "^5.6.18", "babel-eslint": "^3.1.15", "babel-loader": "^5.1.4", + "contextify": "^0.1.14", "eslint": "^0.23", "eslint-config-airbnb": "0.0.6", "eslint-plugin-react": "^2.3.0", diff --git a/test/utils/isPlainObject.spec.js b/test/utils/isPlainObject.spec.js index 24180258aa..963c7f610c 100644 --- a/test/utils/isPlainObject.spec.js +++ b/test/utils/isPlainObject.spec.js @@ -1,5 +1,6 @@ import expect from 'expect'; import isPlainObject from '../../src/utils/isPlainObject'; +import contextify from 'contextify'; describe('isPlainObject', () => { it('should return true only if plain object', () => { @@ -7,11 +8,17 @@ describe('isPlainObject', () => { this.prop = 1; } + const sandbox = contextify(); + sandbox.run('var fromAnotherRealm = {};'); + + expect(isPlainObject(sandbox.fromAnotherRealm)).toBe(true); expect(isPlainObject(new Test())).toBe(false); expect(isPlainObject(new Date())).toBe(false); expect(isPlainObject([1, 2, 3])).toBe(false); expect(isPlainObject(null)).toBe(false); expect(isPlainObject()).toBe(false); expect(isPlainObject({ 'x': 1, 'y': 2 })).toBe(true); + + sandbox.dispose(); }); }); From 57347795ee05a41317404bd75f080f407f3b1561 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Michal=20Kvasni=C4=8D=C3=A1k?= Date: Thu, 23 Jul 2015 16:57:18 +0200 Subject: [PATCH 49/84] replace const with var for flow-friendly code --- src/utils/isPlainObject.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/utils/isPlainObject.js b/src/utils/isPlainObject.js index faa72663a8..db3f6a0113 100644 --- a/src/utils/isPlainObject.js +++ b/src/utils/isPlainObject.js @@ -1,4 +1,4 @@ -const fnToString = (fn) => Function.prototype.toString.call(fn); +var fnToString = (fn) => Function.prototype.toString.call(fn); /** * @param {any} obj The object to inspect. @@ -9,7 +9,7 @@ export default function isPlainObject(obj) { return false; } - const proto = typeof obj.constructor === 'function' ? Object.getPrototypeOf(obj) : Object.prototype; + var proto = typeof obj.constructor === 'function' ? Object.getPrototypeOf(obj) : Object.prototype; if (proto === null) { return true; From d92f08e804a8b2f91d7f77a8bb10ee1c279cee6a Mon Sep 17 00:00:00 2001 From: Michael Contento Date: Wed, 29 Jul 2015 13:51:10 +0200 Subject: [PATCH 50/84] Allow `bindActionCreators` to be used with function as actionCreator With this change you can use `bindActionCreators` to either bind a object of actionCreator functions to dispatch or only a single actionCreator directly. import { bindActionCreators } from 'redux'; import { addTodo } from './actions/todoActions'; const boundAddTodo = bindActionCreators(addTodo, dispatch); boundAddTodo('Hello'); --- src/utils/bindActionCreators.js | 15 ++++++++++++--- test/utils/bindActionCreators.spec.js | 11 +++++++++++ 2 files changed, 23 insertions(+), 3 deletions(-) diff --git a/src/utils/bindActionCreators.js b/src/utils/bindActionCreators.js index a895a7ebb1..d8aa71d542 100644 --- a/src/utils/bindActionCreators.js +++ b/src/utils/bindActionCreators.js @@ -1,13 +1,18 @@ import mapValues from '../utils/mapValues'; +function bindActionCreator(actionCreator, dispatch) { + return (...args) => dispatch(actionCreator(...args)); +} + /** * Turns an object whose values are action creators, into an object with the * same keys, but with every function wrapped into a `dispatch` call so they * may be invoked directly. This is just a convenience method, as you can call * `store.dispatch(MyActionCreators.doSomething())` yourself just fine. * - * @param {Object} actionCreators An object whose values are action creator - * functions. One handy way to obtain it is to use ES6 `import * as` syntax. + * @param {Object|Function} actionCreators An object whose values are action + * creator functions. One handy way to obtain it is to use ES6 `import * as` + * syntax. It also supports binding only a single function. * * @param {Function} dispatch The `dispatch` function available on your Redux * store. @@ -16,7 +21,11 @@ import mapValues from '../utils/mapValues'; * action creator wrapped into the `dispatch` call. */ export default function bindActionCreators(actionCreators, dispatch) { + if (typeof actionCreators === 'function') { + return bindActionCreator(actionCreators, dispatch); + } + return mapValues(actionCreators, actionCreator => - (...args) => dispatch(actionCreator(...args)) + bindActionCreator(actionCreator, dispatch) ); } diff --git a/test/utils/bindActionCreators.spec.js b/test/utils/bindActionCreators.spec.js index 3405f5c284..07ae851e6b 100644 --- a/test/utils/bindActionCreators.spec.js +++ b/test/utils/bindActionCreators.spec.js @@ -26,4 +26,15 @@ describe('bindActionCreators', () => { { id: 1, text: 'Hello' } ]); }); + + it('should support wrapping a single function only', () => { + const actionCreator = actionCreators.addTodo; + const boundActionCreator = bindActionCreators(actionCreator, store.dispatch); + + const action = boundActionCreator('Hello'); + expect(action).toEqual(actionCreator('Hello')); + expect(store.getState()).toEqual([ + { id: 1, text: 'Hello' } + ]); + }); }); From fca60c8f5c4e9aef06e5460e68e44fe61c506c0b Mon Sep 17 00:00:00 2001 From: Lee Bannard Date: Wed, 22 Jul 2015 22:45:30 +0100 Subject: [PATCH 51/84] add verifyStateShape function and tests --- package.json | 3 +- src/utils/combineReducers.js | 54 +++++++++++++++++++++-- test/utils/combineReducers.spec.js | 71 +++++++++++++++++++++++++++++- 3 files changed, 123 insertions(+), 5 deletions(-) diff --git a/package.json b/package.json index c9c8325172..10e77d595d 100644 --- a/package.json +++ b/package.json @@ -58,7 +58,8 @@ "webpack-dev-server": "^1.8.2" }, "dependencies": { - "invariant": "^2.0.0" + "invariant": "^2.0.0", + "warning": "^2.0.0" }, "npmName": "redux", "npmFileMap": [ diff --git a/src/utils/combineReducers.js b/src/utils/combineReducers.js index e59e4d0976..8ba298991a 100644 --- a/src/utils/combineReducers.js +++ b/src/utils/combineReducers.js @@ -1,7 +1,9 @@ +import { ActionTypes } from '../createStore'; +import isPlainObject from '../utils/isPlainObject'; import mapValues from '../utils/mapValues'; import pick from '../utils/pick'; import invariant from 'invariant'; -import { ActionTypes } from '../createStore'; +import warning from 'warning'; function getErrorMessage(key, action) { var actionType = action && action.type; @@ -13,6 +15,41 @@ function getErrorMessage(key, action) { ); } +function verifyStateShape(initialState, currentState) { + var reducerKeys = Object.keys(currentState); + + if (reducerKeys.length === 0) { + warning( + false, + 'Store does not have a valid reducer. Make sure the argument passed ' + + 'to combineReducers is an object whose values are reducers.' + ); + return; + } + + if (!isPlainObject(initialState)) { + warning( + false, + 'initialState has unexpected type of "' + + ({}).toString.call(initialState).match(/\s([a-z|A-Z]+)/)[1] + + '". Expected initialState to be an object with the following ' + + `keys: "${reducerKeys.join('", "')}"` + ); + return; + } + + var unexpectedKeys = Object.keys(initialState).filter( + key => reducerKeys.indexOf(key) < 0 + ); + + warning( + unexpectedKeys.length === 0, + `Unexpected ${unexpectedKeys.length > 1 ? 'keys' : 'key'} ` + + `"${unexpectedKeys.join('", "')}" in initialState will be ignored. ` + + `Expected to find one of the known reducer keys instead: "${reducerKeys.join('", "')}"` + ); +} + /** * Turns an object whose values are different reducer functions, into a single * reducer function. It will call every child reducer, and gather their results @@ -29,6 +66,7 @@ function getErrorMessage(key, action) { * @returns {Function} A reducer function that invokes every reducer inside the * passed object, and builds a state object with the same shape. */ + export default function combineReducers(reducers) { var finalReducers = pick(reducers, (val) => typeof val === 'function'); @@ -54,8 +92,11 @@ export default function combineReducers(reducers) { ); }); - return function combination(state = {}, action) { - return mapValues(finalReducers, (reducer, key) => { + var defaultState = mapValues(finalReducers, () => undefined); + var stateShapeVerified; + + return function combination(state = defaultState, action) { + var finalState = mapValues(finalReducers, (reducer, key) => { var newState = reducer(state[key], action); invariant( typeof newState !== 'undefined', @@ -63,5 +104,12 @@ export default function combineReducers(reducers) { ); return newState; }); + + if (process.env.NODE_ENV !== 'production' && !stateShapeVerified) { + verifyStateShape(state, finalState); + stateShapeVerified = true; + } + + return finalState; }; } diff --git a/test/utils/combineReducers.spec.js b/test/utils/combineReducers.spec.js index dba2069dcf..d6ae96cba4 100644 --- a/test/utils/combineReducers.spec.js +++ b/test/utils/combineReducers.spec.js @@ -97,7 +97,7 @@ describe('Utils', () => { } }); - expect(reducer(0, { type: increment }).counter).toEqual(1); + expect(reducer({counter: 0}, { type: increment }).counter).toEqual(1); }); it('should throw an error if a reducer attempts to handle a private action', () => { @@ -119,5 +119,74 @@ describe('Utils', () => { /"counter".*private/ ); }); + + it('should warn if no reducers are passed to combineReducers', () => { + const spy = expect.spyOn(console, 'error'); + const reducer = combineReducers({}); + reducer({}); + expect(spy.calls[0].arguments[0]).toMatch( + /Store does not have a valid reducer/ + ); + spy.restore(); + }); + + it('should warn if initial state object does not match state object returned by reducer', () => { + const spy = expect.spyOn(console, 'error'); + const reducerCreator = () => { + return combineReducers({ + foo(state = {bar: 1}) { + return state; + }, + baz(state = {qux: 3}) { + return state; + } + }); + }; + + reducerCreator()({foo: {bar: 2}}); + expect(spy.calls.length).toBe(0); + + reducerCreator()({ + foo: {bar: 2}, + baz: {qux: 4} + }); + expect(spy.calls.length).toBe(0); + + reducerCreator()({bar: 2}); + expect(spy.calls[0].arguments[0]).toMatch( + /Unexpected key "bar".*instead: "foo", "baz"/ + ); + + reducerCreator()({bar: 2, qux: 4}); + expect(spy.calls[1].arguments[0]).toMatch( + /Unexpected keys "bar", "qux".*instead: "foo", "baz"/ + ); + + reducerCreator()(1); + expect(spy.calls[2].arguments[0]).toMatch( + /unexpected type of "Number".*keys: "foo", "baz"/ + ); + + spy.restore(); + }); + + it('should only check state shape on init', () => { + const spy = expect.spyOn(console, 'error'); + const reducer = combineReducers({ + foo(state = {bar: 1}) { + return state; + } + }); + + reducer({bar: 1}); + expect(spy.calls[0].arguments[0]).toMatch( + /Unexpected key "bar".*instead: "foo"/ + ); + + reducer({bar: 1}); + expect(spy.calls.length).toBe(1); + + spy.restore(); + }); }); }); From 9a1ad4ffc2acdf071c82bc245a4e616704490740 Mon Sep 17 00:00:00 2001 From: Rodrigo Willrich Date: Thu, 30 Jul 2015 19:20:37 -0300 Subject: [PATCH 52/84] Remove ES7 features from counter example --- examples/counter/.babelrc | 2 +- examples/counter/components/Counter.js | 18 ++++++++++-------- examples/counter/containers/CounterApp.js | 9 +++++---- 3 files changed, 16 insertions(+), 13 deletions(-) diff --git a/examples/counter/.babelrc b/examples/counter/.babelrc index b0b9a96ef0..cab5d10d92 100644 --- a/examples/counter/.babelrc +++ b/examples/counter/.babelrc @@ -1,3 +1,3 @@ { - "stage": 0 + "stage": 2 } diff --git a/examples/counter/components/Counter.js b/examples/counter/components/Counter.js index c58dc31626..1588d82105 100644 --- a/examples/counter/components/Counter.js +++ b/examples/counter/components/Counter.js @@ -1,13 +1,6 @@ import React, { Component, PropTypes } from 'react'; -export default class Counter extends Component { - static propTypes = { - increment: PropTypes.func.isRequired, - incrementIfOdd: PropTypes.func.isRequired, - decrement: PropTypes.func.isRequired, - counter: PropTypes.number.isRequired - }; - +class Counter extends Component { render() { const { increment, incrementIfOdd, decrement, counter } = this.props; return ( @@ -23,3 +16,12 @@ export default class Counter extends Component { ); } } + +Counter.propTypes = { + increment: PropTypes.func.isRequired, + incrementIfOdd: PropTypes.func.isRequired, + decrement: PropTypes.func.isRequired, + counter: PropTypes.number.isRequired +}; + +export default Counter; diff --git a/examples/counter/containers/CounterApp.js b/examples/counter/containers/CounterApp.js index f0813c895d..55d6f238d2 100644 --- a/examples/counter/containers/CounterApp.js +++ b/examples/counter/containers/CounterApp.js @@ -4,10 +4,7 @@ import { connect } from 'react-redux'; import Counter from '../components/Counter'; import * as CounterActions from '../actions/CounterActions'; -@connect(state => ({ - counter: state.counter -})) -export default class CounterApp extends Component { +class CounterApp extends Component { render() { const { counter, dispatch } = this.props; return ( @@ -16,3 +13,7 @@ export default class CounterApp extends Component { ); } } + +export default connect(state => ({ + counter: state.counter +}))(CounterApp); From 3dfbb1f6ae3b302765c71c6bfccd06cb4777176d Mon Sep 17 00:00:00 2001 From: Rodrigo Willrich Date: Thu, 30 Jul 2015 19:50:11 -0300 Subject: [PATCH 53/84] Remove ES7 features from todomvc example --- examples/todomvc/.babelrc | 2 +- examples/todomvc/components/Footer.js | 20 ++++++++------- examples/todomvc/components/Header.js | 14 ++++++----- examples/todomvc/components/MainSection.js | 14 ++++++----- examples/todomvc/components/TodoItem.js | 20 ++++++++------- examples/todomvc/components/TodoTextInput.js | 26 +++++++++++--------- examples/todomvc/index.js | 2 ++ examples/todomvc/reducers/todos.js | 7 +++--- 8 files changed, 58 insertions(+), 47 deletions(-) diff --git a/examples/todomvc/.babelrc b/examples/todomvc/.babelrc index b0b9a96ef0..cab5d10d92 100644 --- a/examples/todomvc/.babelrc +++ b/examples/todomvc/.babelrc @@ -1,3 +1,3 @@ { - "stage": 0 + "stage": 2 } diff --git a/examples/todomvc/components/Footer.js b/examples/todomvc/components/Footer.js index b8feaa92c0..264c938a14 100644 --- a/examples/todomvc/components/Footer.js +++ b/examples/todomvc/components/Footer.js @@ -8,15 +8,7 @@ const FILTER_TITLES = { [SHOW_MARKED]: 'Completed' }; -export default class Footer extends Component { - static propTypes = { - markedCount: PropTypes.number.isRequired, - unmarkedCount: PropTypes.number.isRequired, - filter: PropTypes.string.isRequired, - onClearMarked: PropTypes.func.isRequired, - onShow: PropTypes.func.isRequired - } - +class Footer extends Component { render() { return (