Skip to content

Middleware all the things #55

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

Closed
wants to merge 13 commits into from
Closed
15 changes: 8 additions & 7 deletions examples/actions/CounterActions.js
Original file line number Diff line number Diff line change
Expand Up @@ -6,8 +6,8 @@ export function increment() {
};
}

export function incrementIfOdd() {
return (perform, { counter }) => {
export function incrementIfOdd(counter) {
return perform => {
if (counter % 2 === 0) {
return;
}
Expand All @@ -17,11 +17,12 @@ export function incrementIfOdd() {
}

export function incrementAsync() {
return perform => {
setTimeout(() => {
perform(increment());
}, 1000);
};
return new Promise(resolve =>
setTimeout(
() => resolve(increment()),
1000
)
);
}

export function decrement() {
Expand Down
12 changes: 11 additions & 1 deletion examples/components/Counter.js
Original file line number Diff line number Diff line change
Expand Up @@ -4,18 +4,28 @@ export default class Counter {
static propTypes = {
increment: PropTypes.func.isRequired,
decrement: PropTypes.func.isRequired,
incrementIfOdd: PropTypes.func.isRequired,
incrementAsync: PropTypes.func.isRequired,
counter: PropTypes.number.isRequired
};

render() {
const { increment, decrement, counter } = this.props;
const {
increment,
decrement,
incrementIfOdd,
incrementAsync,
counter } = this.props;
return (
<p>
Clicked: {counter} times
{' '}
<button onClick={increment}>+</button>
{' '}
<button onClick={decrement}>-</button>
{' '}
<button onClick={incrementIfOdd}>Increment if odd</button>
<button onClick={incrementAsync}>Increment async</button>
</p>
);
}
Expand Down
24 changes: 24 additions & 0 deletions examples/components/Transactions.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
import React, { PropTypes } from 'react';
import DOMify from 'react-domify';

export default class Transactions {
static propTypes = {
status: PropTypes.object.isRequired,
commit: PropTypes.func.isRequired,
begin: PropTypes.func.isRequired,
rollback: PropTypes.func.isRequired
};

render() {
const { status, commit, begin, rollback } = this.props;

return (
<p>
<button onClick={begin}>Begin</button>
<button onClick={commit}>Commit</button>
<button onClick={rollback}>Rollback</button>
<DOMify value={status} />
</p>
);
}
}
25 changes: 23 additions & 2 deletions examples/containers/App.js
Original file line number Diff line number Diff line change
@@ -1,10 +1,30 @@
import React from 'react';
import { createDispatcher, Provider, composeStores } from 'redux';
import { createDispatcher, Provider, composeStores, compose } from 'redux';
import CounterApp from './CounterApp';
import TodoApp from './TodoApp';
import TransactionsApp from './TransactionsApp';
import * as stores from '../stores/index';
import createTransactor from '../middleware/createTransactor';
import callbackMiddleware from 'redux/middleware/callback';

// Naive implementation of promise middleware
// Leave full implementation to userland
function promiseMiddleware(next) {
return action =>
action && typeof action.then === 'function'
? action.then(next)
: next(action);
}

const store = composeStores(stores);
const transactor = createTransactor();

const dispatcher = createDispatcher({
store,
reducer: transactor,
middleware: [ promiseMiddleware, callbackMiddleware ]
});

const dispatcher = createDispatcher(composeStores(stores));

export default class App {
render() {
Expand All @@ -14,6 +34,7 @@ export default class App {
<div>
<CounterApp />
<TodoApp />
<TransactionsApp transactor={transactor} />
</div>
}
</Provider>
Expand Down
6 changes: 4 additions & 2 deletions examples/containers/CounterApp.js
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import React from 'react';
import { connect, bindActions } from 'redux';
import { connect, bindActionCreators } from 'redux';
import Counter from '../components/Counter';
import * as CounterActions from '../actions/CounterActions';

Expand All @@ -9,9 +9,11 @@ import * as CounterActions from '../actions/CounterActions';
export default class CounterApp {
render() {
const { counter, dispatcher } = this.props;
const actionCreators = bindActionCreators(CounterActions, dispatcher.dispatch);
return (
<Counter counter={counter}
{...bindActions(CounterActions, dispatcher)} />
{...actionCreators}
incrementIfOdd={() => actionCreators.incrementIfOdd(counter)} />
);
}
}
4 changes: 2 additions & 2 deletions examples/containers/TodoApp.js
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import React from 'react';
import { bindActions, Connector } from 'redux';
import { bindActionCreators, Connector } from 'redux';
import AddTodo from '../components/AddTodo';
import TodoList from '../components/TodoList';
import * as TodoActions from '../actions/TodoActions';
Expand All @@ -14,7 +14,7 @@ export default class TodoApp {
}

renderChild({ todos, dispatcher }) {
const actions = bindActions(TodoActions, dispatcher);
const actions = bindActionCreators(TodoActions, dispatcher.dispatch);
return (
<div>
<AddTodo {...actions} />
Expand Down
30 changes: 30 additions & 0 deletions examples/containers/TransactionsApp.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
import React, { PropTypes } from 'react';
import { Connector } from 'redux';
import Transactions from '../components/Transactions';

export default class TransactionsApp {
static propTypes = {
transactor: PropTypes.func
};

render() {
return (
<Connector select={state => {
return { status: state.transactorStatus };
}}>
{::this.renderChild}
</Connector>
);
}

renderChild({ status }) {
const { transactor: { begin, commit, rollback } } = this.props;

return (
<Transactions status={status}
commit={commit}
begin={begin}
rollback={rollback} />
);
}
}
100 changes: 100 additions & 0 deletions examples/middleware/createTransactor.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,100 @@
function noop() {}

class Transactor {
inProgress = false;
actions = [];

middleware(store, state, dispatch) {
this.store = store;
this.dispatch = dispatch;
this.currentState = state;

if (this.isCapturingState) {
this.isCapturingState = false;
return () => () => this.sendToNext(state);
}

return next => {
this.next = next;

return action => {
if (this.inProgress) {
this.actions.push(action);
const nextState = this.reduceActions(this.headState, this.actions);
return this.sendToNext(nextState);
} else {
const nextState = this.reduceActions(this.currentState, [ action ]);
return this.sendToNext(nextState);
}
};
};
}

// Get the current state atom by running a dummy dispatch.
getCurrentState() {
this.isCapturingState = true;
this.dispatch();
return this.currentState;
}

reduceActions(initialState, actions) {
return actions.reduce(
(state, action) => this.store(state, action),
initialState
);
}

sendToNext(nextState) {
// Something about this feels wrong, but it will work for now
this.next({
...nextState,
transactorStatus: this.getStatus()
});
}

begin() {
if (!this.inProgress) {
this.inProgress = true;
this.actions = [];
this.headState = this.getCurrentState();
this.sendToNext(this.headState);
}
}

commit() {
if (this.inProgress) {
this.inProgress = false;
this.actions = [];
delete this.headState;
this.sendToNext(this.getCurrentState());
}
}

rollback() {
if (this.inProgress) {
this.inProgress = false;
this.actions = [];
this.currentState = this.headState;
delete this.headState;
return this.sendToNext(this.currentState);
}
}

getStatus() {
const { inProgress, actions, headState } = this;

return { inProgress, actions, headState };
}
}

export default function createTransactor() {
const transactor = new Transactor();

const middleware = ::transactor.middleware;
middleware.begin = ::transactor.begin;
middleware.commit = ::transactor.commit;
middleware.rollback = ::transactor.rollback;
middleware.getStatus = ::transactor.getStatus;

return middleware;
}
1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,7 @@
"babel-loader": "^5.1.2",
"eslint": "^0.22.1",
"eslint-plugin-react": "^2.3.0",
"react-domify": "~0.2.1",
"react-hot-loader": "^1.2.7",
"rimraf": "^2.3.4",
"webpack": "^1.9.6",
Expand Down
32 changes: 14 additions & 18 deletions src/Dispatcher.js
Original file line number Diff line number Diff line change
@@ -1,36 +1,32 @@
function dispatch(store, atom, action) {
return store(atom, action);
}
import compose from './utils/compose';
import storeReducer from './storeReducer';

export default class Dispatcher {
constructor(store) {
this.perform = this.perform.bind(this);
constructor({ store, reducer = storeReducer, middleware } = {}) {
this.store = store;
this.initialize();
this.middleware = compose(...middleware);
this.initialize({ reducer });
}

initialize({ atom, subscriptions = [] } = {}) {
initialize({ atom, subscriptions = [], reducer } = {}) {
this.atom = atom;
this.subscriptions = subscriptions;
this.reducer = reducer;
this.dispatch({});
}

dispose() {
const { atom, subscriptions } = this;
delete this.atom;
const { atom, subscriptions, reducer } = this;
this.subscriptions = [];
return { atom, subscriptions };
return { atom, subscriptions, reducer };
}

dispatch(action) {
const nextAtom = dispatch(this.store, this.atom, action);
this.setAtom(nextAtom);
}

perform(action) {
return typeof action === 'function'
? action(this.perform, this.atom)
: this.dispatch(action);
this.middleware(
_action => this.reducer(this.store, this.getAtom(), ::this.dispatch)(
nextAtom => this.setAtom(nextAtom)
)(_action)
)(action);
}

getAtom() {
Expand Down
6 changes: 1 addition & 5 deletions src/components/Provider.js
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ import { PropTypes } from 'react';

const dispatcherShape = PropTypes.shape({
subscribe: PropTypes.func.isRequired,
perform: PropTypes.func.isRequired,
dispatch: PropTypes.func.isRequired,
getAtom: PropTypes.func.isRequired
});

Expand Down Expand Up @@ -38,10 +38,6 @@ export default class Provider {
return this.props.dispatcher.dispatch(action);
}

perform(actionCreator, ...args) {
return this.props.dispatcher.perform(actionCreator, ...args);
}

getAtom() {
return this.props.dispatcher.getAtom();
}
Expand Down
2 changes: 1 addition & 1 deletion src/createDispatcher.js
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ export default function createDispatcher(...args) {

return {
subscribe: ::dispatcher.subscribe,
perform: ::dispatcher.perform,
dispatch: ::dispatcher.dispatch,
getAtom: ::dispatcher.getAtom,
setAtom: ::dispatcher.setAtom,
initialize: ::dispatcher.initialize,
Expand Down
4 changes: 3 additions & 1 deletion src/index.js
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
// Core
export createDispatcher from './createDispatcher';
export storeReducer from './storeReducer';

// Wrapper components
export Provider from './components/Provider';
Expand All @@ -11,4 +12,5 @@ export connect from './components/connect';

// Utilities
export composeStores from './utils/composeStores';
export bindActions from './utils/bindActions';
export compose from './utils/compose';
export bindActionCreators from './utils/bindActionCreators';
Loading