From c2516ebe7242a17ac79acc47d7248dc2e57ceb58 Mon Sep 17 00:00:00 2001 From: Andrew Clark Date: Tue, 16 Jun 2015 22:44:06 -0700 Subject: [PATCH 1/4] Update dispatcher interface so that it can be stateless --- src/Redux.js | 8 ++++++-- src/createDispatcher.js | 12 +++--------- test/components/Connector.spec.js | 4 ++-- test/createDispatcher.spec.js | 25 ++++++++++++------------- 4 files changed, 23 insertions(+), 26 deletions(-) diff --git a/src/Redux.js b/src/Redux.js index f60cc5aa52..604f28dbec 100644 --- a/src/Redux.js +++ b/src/Redux.js @@ -8,7 +8,7 @@ export default class Redux { // A shortcut notation to use the default dispatcher dispatcher = createDispatcher( composeStores(dispatcher), - getState => [thunkMiddleware(getState)] + ({ getState }) => [thunkMiddleware(getState)] ); } @@ -23,7 +23,11 @@ export default class Redux { replaceDispatcher(nextDispatcher) { this.dispatcher = nextDispatcher; - this.dispatchFn = nextDispatcher(this.state, ::this.setState); + this.dispatchFn = nextDispatcher({ + getState: ::this.getState, + setState: ::this.setState + }); + this.dispatch({}); } dispatch(action) { diff --git a/src/createDispatcher.js b/src/createDispatcher.js index 5b1b36bac9..b9b56445c6 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 } ] }); From ed76d4fd457897b3777d4172e37bd60699b55e2b Mon Sep 17 00:00:00 2001 From: Andrew Clark Date: Tue, 16 Jun 2015 23:12:09 -0700 Subject: [PATCH 2/4] Add prepareState to Redux class --- src/Redux.js | 12 +++++++++--- 1 file changed, 9 insertions(+), 3 deletions(-) diff --git a/src/Redux.js b/src/Redux.js index 604f28dbec..29f10af1c7 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); } @@ -24,7 +26,7 @@ export default class Redux { replaceDispatcher(nextDispatcher) { this.dispatcher = nextDispatcher; this.dispatchFn = nextDispatcher({ - getState: ::this.getState, + getState: ::this.getRawState, setState: ::this.setState }); this.dispatch({}); @@ -34,10 +36,14 @@ export default class Redux { 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()); From 1babcaa23b11c2c24d8a8d42d5018d6b92ff4ac5 Mon Sep 17 00:00:00 2001 From: Andrew Clark Date: Tue, 16 Jun 2015 23:30:06 -0700 Subject: [PATCH 3/4] Revert middleware API change to submit as separate PR --- src/Redux.js | 2 +- src/createDispatcher.js | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/src/Redux.js b/src/Redux.js index 29f10af1c7..010312f659 100644 --- a/src/Redux.js +++ b/src/Redux.js @@ -9,7 +9,7 @@ export default class Redux { // A shortcut notation to use the default dispatcher dispatcher = createDispatcher( composeStores(dispatcher), - ({ getState }) => [thunkMiddleware(getState)] + getState => [thunkMiddleware(getState)] ); } diff --git a/src/createDispatcher.js b/src/createDispatcher.js index b9b56445c6..50689d27c5 100644 --- a/src/createDispatcher.js +++ b/src/createDispatcher.js @@ -8,7 +8,7 @@ export default function createDispatcher(store, middlewares = []) { } const finalMiddlewares = typeof middlewares === 'function' ? - middlewares({ getState, setState, dispatch }) : + middlewares(getState, setState, dispatch) : middlewares; return composeMiddleware(...finalMiddlewares, dispatch); From b71a7fc0ca64a8ce86b67b178ab602d065c6492e Mon Sep 17 00:00:00 2001 From: Andrew Clark Date: Wed, 17 Jun 2015 09:15:02 -0700 Subject: [PATCH 4/4] Add test from #110 to show that it passes --- test/createRedux.spec.js | 25 +++++++++++++++++++++++++ 1 file changed, 25 insertions(+) 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}); + }); });