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/App.js b/examples/containers/App.js index d5deab5690..f50ebadfae 100644 --- a/examples/containers/App.js +++ b/examples/containers/App.js @@ -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() { @@ -14,6 +34,7 @@ export default class App {
+
} diff --git a/examples/containers/CounterApp.js b/examples/containers/CounterApp.js index 79adc6c38b..fbf14ad7b0 100644 --- a/examples/containers/CounterApp.js +++ b/examples/containers/CounterApp.js @@ -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'; @@ -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 ( + {...actionCreators} + incrementIfOdd={() => actionCreators.incrementIfOdd(counter)} /> ); } } diff --git a/examples/containers/TodoApp.js b/examples/containers/TodoApp.js index f952e2678f..550769d05e 100644 --- a/examples/containers/TodoApp.js +++ b/examples/containers/TodoApp.js @@ -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'; @@ -14,7 +14,7 @@ export default class TodoApp { } renderChild({ todos, dispatcher }) { - const actions = bindActions(TodoActions, dispatcher); + const actions = bindActionCreators(TodoActions, dispatcher.dispatch); 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); +}