diff --git a/examples/actions/CounterActions.js b/examples/actions/CounterActions.js
index de00586298..c78fc93c25 100644
--- a/examples/actions/CounterActions.js
+++ b/examples/actions/CounterActions.js
@@ -6,8 +6,8 @@ export function increment() {
};
}
-export function incrementIfOdd() {
- return (perform, { counter }) => {
+export function incrementIfOdd(counter) {
+ return perform => {
if (counter % 2 === 0) {
return;
}
@@ -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() {
diff --git a/examples/components/Counter.js b/examples/components/Counter.js
index 2f7eae6905..cb33fcee19 100644
--- a/examples/components/Counter.js
+++ b/examples/components/Counter.js
@@ -4,11 +4,18 @@ 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 (
Clicked: {counter} times
@@ -16,6 +23,9 @@ export default class Counter {
{' '}
+ {' '}
+
+
);
}
diff --git a/examples/components/Transactions.js b/examples/components/Transactions.js
new file mode 100644
index 0000000000..d8fd3e24e3
--- /dev/null
+++ b/examples/components/Transactions.js
@@ -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 (
+
diff --git a/examples/containers/TransactionsApp.js b/examples/containers/TransactionsApp.js
new file mode 100644
index 0000000000..2f4dd01203
--- /dev/null
+++ b/examples/containers/TransactionsApp.js
@@ -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 (
+
{
+ return { status: state.transactorStatus };
+ }}>
+ {::this.renderChild}
+
+ );
+ }
+
+ renderChild({ status }) {
+ const { transactor: { begin, commit, rollback } } = this.props;
+
+ return (
+
+ );
+ }
+}
diff --git a/examples/middleware/createTransactor.js b/examples/middleware/createTransactor.js
new file mode 100644
index 0000000000..b6342c4399
--- /dev/null
+++ b/examples/middleware/createTransactor.js
@@ -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;
+}
diff --git a/package.json b/package.json
index e54a4358a5..d4bbff0a03 100644
--- a/package.json
+++ b/package.json
@@ -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",
diff --git a/src/Dispatcher.js b/src/Dispatcher.js
index 7f230bad0f..4a1c070397 100644
--- a/src/Dispatcher.js
+++ b/src/Dispatcher.js
@@ -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() {
diff --git a/src/components/Provider.js b/src/components/Provider.js
index 06b6a07b25..0d28a32715 100644
--- a/src/components/Provider.js
+++ b/src/components/Provider.js
@@ -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
});
@@ -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();
}
diff --git a/src/createDispatcher.js b/src/createDispatcher.js
index f0af386a85..a2a1739da1 100644
--- a/src/createDispatcher.js
+++ b/src/createDispatcher.js
@@ -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,
diff --git a/src/index.js b/src/index.js
index 5a0febaf33..f4cedf9aaa 100644
--- a/src/index.js
+++ b/src/index.js
@@ -1,5 +1,6 @@
// Core
export createDispatcher from './createDispatcher';
+export storeReducer from './storeReducer';
// Wrapper components
export Provider from './components/Provider';
@@ -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';
diff --git a/src/middleware/callback.js b/src/middleware/callback.js
new file mode 100644
index 0000000000..2827b97d13
--- /dev/null
+++ b/src/middleware/callback.js
@@ -0,0 +1,6 @@
+export default function callbackMiddleware(next) {
+ return action =>
+ typeof action === 'function'
+ ? action(next)
+ : next(action);
+}
diff --git a/src/storeReducer.js b/src/storeReducer.js
new file mode 100644
index 0000000000..cc1909da1f
--- /dev/null
+++ b/src/storeReducer.js
@@ -0,0 +1,4 @@
+export default function storeReducer(store, state) {
+ return next => action =>
+ next(store(state, action));
+}
diff --git a/src/utils/bindActionCreators.js b/src/utils/bindActionCreators.js
new file mode 100644
index 0000000000..801665492c
--- /dev/null
+++ b/src/utils/bindActionCreators.js
@@ -0,0 +1,7 @@
+import mapValues from 'lodash/object/mapValues';
+
+export default function bindActionCreators(actionCreators, dispatch) {
+ return mapValues(actionCreators, actionCreator =>
+ (...args) => dispatch(actionCreator(...args))
+ );
+}
diff --git a/src/utils/bindActions.js b/src/utils/bindActions.js
deleted file mode 100644
index c7f3a2321f..0000000000
--- a/src/utils/bindActions.js
+++ /dev/null
@@ -1,7 +0,0 @@
-import mapValues from 'lodash/object/mapValues';
-
-export default function bindActions(actionCreators, dispatcher) {
- return mapValues(actionCreators, actionCreator =>
- (...args) => dispatcher.perform(actionCreator(...args))
- );
-}
diff --git a/src/utils/compose.js b/src/utils/compose.js
new file mode 100644
index 0000000000..e7f4553702
--- /dev/null
+++ b/src/utils/compose.js
@@ -0,0 +1,4 @@
+export default function compose(...middlewares) {
+ return next =>
+ middlewares.reduceRight((_next, middleware) => middleware(_next), next);
+}