diff --git a/src/Redux.js b/src/Redux.js index f60cc5aa52..010312f659 100644 --- a/src/Redux.js +++ b/src/Redux.js @@ -1,9 +1,10 @@ import createDispatcher from './createDispatcher'; import composeStores from './utils/composeStores'; import thunkMiddleware from './middleware/thunk'; +import identity from 'lodash/utility/identity'; export default class Redux { - constructor(dispatcher, initialState) { + constructor(dispatcher, initialState, prepareState = identity) { if (typeof dispatcher === 'object') { // A shortcut notation to use the default dispatcher dispatcher = createDispatcher( @@ -13,6 +14,7 @@ export default class Redux { } this.state = initialState; + this.prepareState = prepareState; this.listeners = []; this.replaceDispatcher(dispatcher); } @@ -23,17 +25,25 @@ export default class Redux { replaceDispatcher(nextDispatcher) { this.dispatcher = nextDispatcher; - this.dispatchFn = nextDispatcher(this.state, ::this.setState); + this.dispatchFn = nextDispatcher({ + getState: ::this.getRawState, + setState: ::this.setState + }); + this.dispatch({}); } dispatch(action) { return this.dispatchFn(action); } - getState() { + getRawState() { return this.state; } + getState() { + return this.prepareState(this.state); + } + setState(nextState) { this.state = nextState; this.listeners.forEach(listener => listener()); diff --git a/src/createDispatcher.js b/src/createDispatcher.js index 5b1b36bac9..50689d27c5 100644 --- a/src/createDispatcher.js +++ b/src/createDispatcher.js @@ -1,20 +1,14 @@ import composeMiddleware from './utils/composeMiddleware'; export default function createDispatcher(store, middlewares = []) { - return function dispatcher(initialState, setState) { - let state = setState(store(initialState, {})); - + return function dispatcher({ getState, setState }) { function dispatch(action) { - state = setState(store(state, action)); + setState(store(getState(), action)); return action; } - function getState() { - return state; - } - const finalMiddlewares = typeof middlewares === 'function' ? - middlewares(getState) : + middlewares(getState, setState, dispatch) : middlewares; return composeMiddleware(...finalMiddlewares, dispatch); diff --git a/test/components/Connector.spec.js b/test/components/Connector.spec.js index 04998f9741..79709078ab 100644 --- a/test/components/Connector.spec.js +++ b/test/components/Connector.spec.js @@ -188,13 +188,13 @@ describe('React', () => { }); it('should throw an error if `state` returns anything but a plain object', () => { - const redux = createRedux(() => {}); + const redux = createRedux({ test: () => 'test' }); expect(() => { TestUtils.renderIntoDocument( {() => ( - 1}> + 1}> {() =>
} )} diff --git a/test/createDispatcher.spec.js b/test/createDispatcher.spec.js index 40991bfcec..74911e6604 100644 --- a/test/createDispatcher.spec.js +++ b/test/createDispatcher.spec.js @@ -8,34 +8,33 @@ 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)] + ({ _getState, _dispatch }) => [thunkMiddleware(_getState, _dispatch)] ); expect(dispatcher).toBeA('function'); - const dispatchFn = dispatcher(undefined, spy); - expect(spy.calls.length).toBe(1); - expect(spy).toHaveBeenCalledWith({ todoStore: [] }); + // Mock Redux interface + let state, dispatchFn; + const getState = () => state; + const dispatch = action => dispatchFn(action); + const setState = newState => state = newState; + + dispatchFn = dispatcher({ getState, setState, dispatch }); + dispatchFn({}); // Initial dispatch + expect(state).toEqual({ todoStore: [] }); const addTodoAction = dispatchFn(addTodo(defaultText)); expect(addTodoAction).toEqual({ type: ADD_TODO, text: defaultText }); - expect(spy.calls.length).toBe(2); - expect(spy).toHaveBeenCalledWith({ todoStore: [ + expect(state).toEqual({ todoStore: [ { id: 1, text: defaultText } ] }); dispatchFn(addTodoAsync(('Say hi!'), () => { - expect(spy.calls.length).toBe(3); - expect(spy).toHaveBeenCalledWith({ todoStore: [ + expect(state).toEqual({ todoStore: [ { id: 2, text: 'Say hi!' }, { id: 1, text: defaultText } ] }); diff --git a/test/createRedux.spec.js b/test/createRedux.spec.js index 1900de351c..f2dc80d299 100644 --- a/test/createRedux.spec.js +++ b/test/createRedux.spec.js @@ -69,4 +69,29 @@ describe('createRedux', () => { expect(state).toEqual(redux.getState().todoStore); }); + + 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; + } + + redux = createRedux({ foo, bar }); + + redux.subscribe(() => { + // What the Connector ends up doing. + const state = redux.getState(); + if (state.bar === 0) { + redux.dispatch({type: 'bar'}); + } + }); + + redux.dispatch({type: 'foo'}); + + // Either this or throw an error when nesting dispatchers + expect(redux.getState()).toEqual({foo: 1, bar: 2}); + }); });