diff --git a/README.md b/README.md index 1cdd30ba19..46e0bad165 100644 --- a/README.md +++ b/README.md @@ -54,10 +54,10 @@ export function decrement() { // Can also be async if you return a function export function incrementAsync() { - return perform => { + return dispatch => { setTimeout(() => { - // Yay! Can invoke sync or async actions with `perform` - perform(increment()); + // Yay! Can invoke sync or async actions with `dispatch` + dispatch(increment()); }, 1000); }; } @@ -65,12 +65,12 @@ export function incrementAsync() { // Could also read state of a store in the callback form export function incrementIfOdd() { - return (perform, { counter }) => { + return (dispatch, { counter }) => { if (counter % 2 === 0) { return; } - perform(increment()); + dispatch(increment()); }; } ``` @@ -137,10 +137,10 @@ export default class Counter { ```js // The smart component may observe stores using ``, -// and bind actions to the dispatcher with `bindActions`. +// and bind actions to the dispatcher with `bindActionCreators`. import React from 'react'; -import { Connector, bindActions } from 'redux'; +import { Connector, bindActionCreators } from 'redux'; import Counter from '../components/Counter'; import * as CounterActions from '../actions/CounterActions'; @@ -155,10 +155,10 @@ export default class CounterApp { render() { return ( - {({ counter, dispatcher }) => + {({ counter, dispatch }) => /* Yes this is child as a function. */ + {...bindActionCreators(CounterActions, dispatch)} /> } ); @@ -172,7 +172,7 @@ The `@connect` decorator lets you create smart components less verbosely: ```js 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'; @@ -181,42 +181,35 @@ import * as CounterActions from '../actions/CounterActions'; })) export default class CounterApp { render() { - const { counter, dispatcher } = this.props; + const { counter, dispatch } = this.props; return ( + {...bindActionCreators(CounterActions, dispatch)} /> ); } } ``` -#### The root component +#### Initializing Redux -Decorate your top-level component with `@provider(dispatcher)` (or `` inside) to bind it to a Redux dispatcher instance. - -Redux dispatcher accepts a single Store as an argument. Usually Flux apps have many Stores, so Redux provides a `composeStore` method that turns an object with Store functions as values (such as what you'd get from `import * as stores`) into a Store that [composes](https://gist.github.com/gaearon/d77ca812015c0356654f) them. - -Think of `composeStores` as a “higher-order” Store because it creates a Store from several Stores. (You don't have to use it! You can just pass your own top-level Store function if that's what you prefer.) +The simplest way to initialize a Redux instance is to give it an object whose values are your Store functions, and whose keys are their names. You may `import *` from the file with all your Store definitions to obtain such an object: ```js -import React from 'react'; -import { createDispatcher, Provider, composeStores } from 'redux'; -import CounterApp from './CounterApp'; -import TodoApp from './TodoApp'; +import { createRedux, Provider } from 'redux'; import * as stores from '../stores/index'; -const dispatcher = createDispatcher(composeStores(stores)); +const redux = createRedux(stores); +``` + +Then pass `redux` as a prop to `` component in the root component of your app, and you're all set: +```js export default class App { render() { return ( - + {() => - /* Yep, function as a child. */ -
- - -
+ }
); @@ -224,6 +217,56 @@ export default class App { } ``` +#### Running the same code on client and server + +The `redux` instance returned by `createRedux` also has the `dispatch(action)`, `subscribe()` and `getState()` methods that you may call outside the React components. + +You may optionally specify the initial state as the second argument to `createRedux`. This is useful for hydrating the state you received from running Redux on the server: + +```js +// server +const redux = createRedux(stores); +redux.dispatch(MyActionCreators.doSomething()); // fire action creators to fill the state +const state = redux.getState(); // somehow pass this state to the client + +// client +const initialState = window.STATE_FROM_SERVER; +const redux = createRedux(stores, initialState); +``` + +#### Additional customization + +There is also a longer way to do the same thing, if you need additional customization. + +This: + +```js +import { createRedux } from 'redux'; +import * as stores from '../stores/index'; + +const redux = createRedux(stores); +``` + +is in fact a shortcut for this: + +```js +import { createRedux, createDispatcher, composeStores } from 'redux'; +import * as stores from '../stores/index'; + +// Compose all your Stores into a single Store function with `composeStores`: +const store = composeStores(stores); + +// Create a Dispatcher function for your composite Store: +const dispatcher = createDispatcher(store); + +// Create a Redux instance using the dispatcher function: +const redux = createRedux(dispatcher); +``` + +Why would you want to write it longer? Maybe you're an advanced user and want to provide a custom Dispatcher function, or maybe you have a different idea of how to compose your Stores (or you're satisfied with a single Store). Redux lets you do all of this. + +When in doubt, use the shorter option! + ## FAQ ### How does hot reloading work? diff --git a/examples/actions/CounterActions.js b/examples/actions/CounterActions.js index de00586298..737f90a95d 100644 --- a/examples/actions/CounterActions.js +++ b/examples/actions/CounterActions.js @@ -7,19 +7,19 @@ export function increment() { } export function incrementIfOdd() { - return (perform, { counter }) => { + return (dispatch, { counter }) => { if (counter % 2 === 0) { return; } - perform(increment()); + dispatch(increment()); }; } export function incrementAsync() { - return perform => { + return dispatch => { setTimeout(() => { - perform(increment()); + dispatch(increment()); }, 1000); }; } diff --git a/examples/containers/App.js b/examples/containers/App.js index d5deab5690..680d7bf8b8 100644 --- a/examples/containers/App.js +++ b/examples/containers/App.js @@ -1,15 +1,15 @@ import React from 'react'; -import { createDispatcher, Provider, composeStores } from 'redux'; import CounterApp from './CounterApp'; import TodoApp from './TodoApp'; +import { createRedux, Provider } from 'redux'; import * as stores from '../stores/index'; -const dispatcher = createDispatcher(composeStores(stores)); +const redux = createRedux(stores); export default class App { render() { return ( - + {() =>
diff --git a/examples/containers/CounterApp.js b/examples/containers/CounterApp.js index 79adc6c38b..b39823c0f9 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'; @@ -8,10 +8,10 @@ import * as CounterActions from '../actions/CounterActions'; })) export default class CounterApp { render() { - const { counter, dispatcher } = this.props; + const { counter, dispatch } = this.props; return ( + {...bindActionCreators(CounterActions, dispatch)} /> ); } } diff --git a/examples/containers/TodoApp.js b/examples/containers/TodoApp.js index f952e2678f..9b66ca207f 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'; @@ -13,8 +13,8 @@ export default class TodoApp { ); } - renderChild({ todos, dispatcher }) { - const actions = bindActions(TodoActions, dispatcher); + renderChild({ todos, dispatch }) { + const actions = bindActionCreators(TodoActions, dispatch); return (
diff --git a/src/Dispatcher.js b/src/Dispatcher.js deleted file mode 100644 index 7f230bad0f..0000000000 --- a/src/Dispatcher.js +++ /dev/null @@ -1,59 +0,0 @@ -function dispatch(store, atom, action) { - return store(atom, action); -} - -export default class Dispatcher { - constructor(store) { - this.perform = this.perform.bind(this); - this.store = store; - this.initialize(); - } - - initialize({ atom, subscriptions = [] } = {}) { - this.atom = atom; - this.subscriptions = subscriptions; - this.dispatch({}); - } - - dispose() { - const { atom, subscriptions } = this; - delete this.atom; - this.subscriptions = []; - return { atom, subscriptions }; - } - - 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); - } - - getAtom() { - return this.atom; - } - - setAtom(atom) { - this.atom = atom; - this.emitChange(); - } - - subscribe(listener) { - this.subscriptions.push(listener); - listener(this.atom); - - return () => { - const index = this.subscriptions.indexOf(listener); - this.subscriptions.splice(index, 1); - }; - } - - emitChange() { - const { atom, subscriptions } = this; - subscriptions.forEach(listener => listener(atom)); - } -} diff --git a/src/Redux.js b/src/Redux.js new file mode 100644 index 0000000000..cd05470659 --- /dev/null +++ b/src/Redux.js @@ -0,0 +1,46 @@ +import createDispatcher from './createDispatcher'; +import composeStores from './utils/composeStores'; + +export default class Redux { + constructor(dispatcher, initialState) { + if (typeof dispatcher === 'object') { + // A shortcut notation to use the default dispatcher + dispatcher = createDispatcher(composeStores(dispatcher)); + } + + this.state = initialState; + this.listeners = []; + this.replaceDispatcher(dispatcher); + } + + getDispatcher() { + return this.dispatcher; + } + + replaceDispatcher(nextDispatcher) { + this.dispatcher = nextDispatcher; + this.dispatchFn = nextDispatcher(this.state, ::this.setState); + } + + dispatch(action) { + return this.dispatchFn(action); + } + + getState() { + return this.state; + } + + setState(nextState) { + this.state = nextState; + this.listeners.forEach(listener => listener()); + } + + subscribe(listener) { + this.listeners.push(listener); + + return () => { + const index = this.listeners.indexOf(listener); + this.listeners.splice(index, 1); + }; + } +} diff --git a/src/components/Connector.js b/src/components/Connector.js index cf57585a47..1dab9334dd 100644 --- a/src/components/Connector.js +++ b/src/components/Connector.js @@ -37,13 +37,13 @@ export default class Connector extends Component { this.handleChange = this.handleChange.bind(this); this.unsubscribe = context.redux.subscribe(this.handleChange); + this.handleChange(); } componentWillReceiveProps(nextProps) { if (nextProps.select !== this.props.select) { // Force the state slice recalculation - const atom = this.context.redux.getAtom(); - this.handleChange(atom); + this.handleChange(); } } @@ -51,8 +51,10 @@ export default class Connector extends Component { this.unsubscribe(); } - handleChange(atom) { - const slice = this.props.select(atom); + handleChange() { + const state = this.context.redux.getState(); + const slice = this.props.select(state); + if (this.state) { this.setState({ slice }); } else { @@ -66,7 +68,7 @@ export default class Connector extends Component { const { redux } = this.context; return children({ - dispatcher: redux, + dispatch: ::redux.dispatch, ...slice }); } diff --git a/src/components/Provider.js b/src/components/Provider.js index 06b6a07b25..3449f89eb6 100644 --- a/src/components/Provider.js +++ b/src/components/Provider.js @@ -1,49 +1,33 @@ -import { PropTypes } from 'react'; +import { Component, PropTypes } from 'react'; -const dispatcherShape = PropTypes.shape({ +const reduxShape = PropTypes.shape({ subscribe: PropTypes.func.isRequired, - perform: PropTypes.func.isRequired, - getAtom: PropTypes.func.isRequired + dispatch: PropTypes.func.isRequired, + getState: PropTypes.func.isRequired }); -export default class Provider { +export default class Provider extends Component { static propTypes = { - dispatcher: dispatcherShape.isRequired, + redux: reduxShape.isRequired, children: PropTypes.func.isRequired }; static childContextTypes = { - redux: dispatcherShape.isRequired + redux: reduxShape.isRequired }; getChildContext() { - return { redux: this }; + return { redux: this.state.redux }; } - constructor() { - this.dispatch = this.dispatch.bind(this); + constructor(props, context) { + super(props, context); + this.state = { redux: props.redux }; } componentWillReceiveProps(nextProps) { - nextProps.dispatcher.initialize( - this.props.dispatcher.dispose() - ); - } - - subscribe(listener) { - return this.props.dispatcher.subscribe(listener); - } - - dispatch(action) { - return this.props.dispatcher.dispatch(action); - } - - perform(actionCreator, ...args) { - return this.props.dispatcher.perform(actionCreator, ...args); - } - - getAtom() { - return this.props.dispatcher.getAtom(); + const nextDispatcher = nextProps.redux.getDispatcher(); + this.state.redux.replaceDispatcher(nextDispatcher); } render() { diff --git a/src/components/provide.js b/src/components/provide.js index 0bb2b242ca..8f1195b3c2 100644 --- a/src/components/provide.js +++ b/src/components/provide.js @@ -2,13 +2,13 @@ import React from 'react'; import Provider from './Provider'; import getDisplayName from '../utils/getDisplayName'; -export default function provide(store) { +export default function provide(redux) { return DecoratedComponent => class ProviderDecorator { static displayName = `Provider(${getDisplayName(DecoratedComponent)})`; render() { return ( - + {props => } ); diff --git a/src/createDispatcher.js b/src/createDispatcher.js index f0af386a85..b725dca3dd 100644 --- a/src/createDispatcher.js +++ b/src/createDispatcher.js @@ -1,14 +1,20 @@ -import Dispatcher from './Dispatcher'; +export default function createDispatcher(store) { + return function dispatcher(initialState, setState) { + let state = store(initialState, {}); + setState(state); -export default function createDispatcher(...args) { - const dispatcher = new Dispatcher(...args); + function dispatchSync(action) { + state = store(state, action); + setState(state); + return action; + } - return { - subscribe: ::dispatcher.subscribe, - perform: ::dispatcher.perform, - getAtom: ::dispatcher.getAtom, - setAtom: ::dispatcher.setAtom, - initialize: ::dispatcher.initialize, - dispose: ::dispatcher.dispose + function dispatch(action) { + return typeof action === 'function' ? + action(dispatch, state) : + dispatchSync(action); + } + + return dispatch; }; } diff --git a/src/createRedux.js b/src/createRedux.js new file mode 100644 index 0000000000..f3ee7c1347 --- /dev/null +++ b/src/createRedux.js @@ -0,0 +1,13 @@ +import Redux from './Redux'; + +export default function createRedux(...args) { + const redux = new Redux(...args); + + return { + subscribe: ::redux.subscribe, + dispatch: ::redux.dispatch, + getState: ::redux.getState, + getDispatcher: ::redux.getDispatcher, + replaceDispatcher: ::redux.replaceDispatcher + }; +} diff --git a/src/index.js b/src/index.js index 5a0febaf33..79fced8d89 100644 --- a/src/index.js +++ b/src/index.js @@ -1,4 +1,5 @@ // Core +export createRedux from './createRedux'; export createDispatcher from './createDispatcher'; // Wrapper components @@ -11,4 +12,4 @@ export connect from './components/connect'; // Utilities export composeStores from './utils/composeStores'; -export bindActions from './utils/bindActions'; +export bindActionCreators from './utils/bindActionCreators'; 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)) - ); -}