Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Stateless dispatcher #119

Closed
wants to merge 4 commits into from
Closed
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
16 changes: 13 additions & 3 deletions src/Redux.js
Original file line number Diff line number Diff line change
@@ -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());
12 changes: 3 additions & 9 deletions src/createDispatcher.js
Original file line number Diff line number Diff line change
@@ -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);
4 changes: 2 additions & 2 deletions test/components/Connector.spec.js
Original file line number Diff line number Diff line change
@@ -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(
<Provider redux={redux}>
{() => (
<Connector state={() => 1}>
<Connector select={() => 1}>
{() => <div />}
</Connector>
)}
25 changes: 12 additions & 13 deletions test/createDispatcher.spec.js
Original file line number Diff line number Diff line change
@@ -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 }
] });
25 changes: 25 additions & 0 deletions test/createRedux.spec.js
Original file line number Diff line number Diff line change
@@ -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});
});
});