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..a8569ec2f2 100644 --- a/src/index.js +++ b/src/index.js @@ -2,13 +2,17 @@ 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'; +import composeMiddleware from './utils/composeMiddleware'; export { createStore, - composeMiddleware, + compose, composeReducers, - bindActionCreators + bindActionCreators, + 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 new file mode 100644 index 0000000000..e6c63ad0bb --- /dev/null +++ b/src/utils/applyMiddleware.js @@ -0,0 +1,31 @@ +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. + * 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 index 596403919d..bf702aae30 100644 --- a/src/utils/composeMiddleware.js +++ b/src/utils/composeMiddleware.js @@ -1,3 +1,10 @@ +import compose from './compose'; + +/** + * Compose middleware from left to right + * @param {...Function} middlewares + * @return {Function} + */ export default function composeMiddleware(...middlewares) { - return middlewares.reduceRight((composed, m) => m(composed)); + return methods => next => compose(...middlewares.map(m => m(methods)), next); } 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 index 5e39b08eab..4d291f4816 100644 --- a/test/composeMiddleware.spec.js +++ b/test/composeMiddleware.spec.js @@ -3,15 +3,15 @@ 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'); + 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'); + 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..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', () => { @@ -35,44 +35,6 @@ describe('createStore', () => { }]); }); - 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')); @@ -110,39 +72,4 @@ describe('createStore', () => { 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')); - }); });