diff --git a/README.md b/README.md index e9394082fb..7e3910e62e 100644 --- a/README.md +++ b/README.md @@ -4,9 +4,9 @@ redux [![build status](https://img.shields.io/travis/gaearon/redux.svg?style=flat-square)](https://travis-ci.org/gaearon/redux) [![npm version](https://img.shields.io/npm/v/redux.svg?style=flat-square)](https://www.npmjs.com/package/redux) -An experiment in fully hot-reloadable Flux. +An experiment in fully hot-reloadable Flux. -**The API might change any day.** +**The API might change any day.** _**Don't use in production just yet.**_ ## Why another Flux framework? @@ -140,10 +140,8 @@ export default class Counter { ```js // The smart component may observe stores using ``, -// and bind actions to the dispatcher with `bindActionCreators`. import React from 'react'; -import { bindActionCreators } from 'redux'; import { Connector } from 'redux/react'; import Counter from '../components/Counter'; import * as CounterActions from '../actions/CounterActions'; @@ -158,11 +156,13 @@ function select(state) { export default class CounterApp { render() { return ( - - {({ counter, dispatch }) => + // Passing an object of action creators here automatically binds them + // with Redux's dispatcher and passes them to the child function + // (so they can be passed as props to child components) + + {({ counter, actions }) => /* Yes this is child as a function. */ - + } ); @@ -176,22 +176,23 @@ The `@connect` decorator lets you create smart components less verbosely: ```js import React from 'react'; -import { bindActionCreators } from 'redux'; import { connect } from 'redux/react'; import Counter from '../components/Counter'; import * as CounterActions from '../actions/CounterActions'; +// Pass an object of action creators you want to pass +// as the `actions` prop to your component as the (optional) +// second parameter of @connect @connect(state => ({ counter: state.counter -})) +}), CounterActions) export default class CounterApp { render() { - const { counter, dispatch } = this.props; - // Instead of `bindActionCreators`, you may also pass `dispatch` as a prop - // to your component and call `dispatch(CounterActions.increment())` + const { counter, actions, dispatch } = this.props; + // Instead of passing action creators as a parameter to @connect to autobind them, you may also + // pass `dispatch` as a prop to your component and call `dispatch(CounterActions.increment())` return ( - + ); } } diff --git a/examples/containers/CounterApp.js b/examples/containers/CounterApp.js index f60d74c746..ac0540f8c1 100644 --- a/examples/containers/CounterApp.js +++ b/examples/containers/CounterApp.js @@ -1,18 +1,16 @@ import React from 'react'; -import { bindActionCreators } from 'redux'; import { connect } from 'redux/react'; import Counter from '../components/Counter'; import * as CounterActions from '../actions/CounterActions'; @connect(state => ({ counter: state.counter -})) +}), CounterActions) export default class CounterApp { render() { - const { counter, dispatch } = this.props; + const { counter, actions } = this.props; return ( - + ); } } diff --git a/examples/containers/TodoApp.js b/examples/containers/TodoApp.js index 9a474d072b..6b7bb224f7 100644 --- a/examples/containers/TodoApp.js +++ b/examples/containers/TodoApp.js @@ -1,5 +1,4 @@ import React from 'react'; -import { bindActionCreators } from 'redux'; import { Connector } from 'redux/react'; import AddTodo from '../components/AddTodo'; import TodoList from '../components/TodoList'; @@ -8,14 +7,13 @@ import * as TodoActions from '../actions/TodoActions'; export default class TodoApp { render() { return ( - ({ todos: state.todos })}> + ({ todos: state.todos })} actionCreators={TodoActions}> {this.renderChild} ); } - renderChild({ todos, dispatch }) { - const actions = bindActionCreators(TodoActions, dispatch); + renderChild({ todos, actions }) { return (
diff --git a/src/components/createConnectDecorator.js b/src/components/createConnectDecorator.js index cdd501d67e..4977f6799e 100644 --- a/src/components/createConnectDecorator.js +++ b/src/components/createConnectDecorator.js @@ -4,7 +4,7 @@ import shallowEqualScalar from '../utils/shallowEqualScalar'; export default function createConnectDecorator(React, Connector) { const { Component } = React; - return function connect(select) { + return function connect(select, actionCreators) { return DecoratedComponent => class ConnectorDecorator extends Component { static displayName = `Connector(${getDisplayName(DecoratedComponent)})`; @@ -14,7 +14,10 @@ export default function createConnectDecorator(React, Connector) { render() { return ( - select(state, this.props)}> + select(state, this.props)} + actionCreators={actionCreators} + > {stuff => } ); diff --git a/src/components/createConnector.js b/src/components/createConnector.js index 0d21f49c50..465d2296c8 100644 --- a/src/components/createConnector.js +++ b/src/components/createConnector.js @@ -1,8 +1,11 @@ import identity from 'lodash/utility/identity'; import shallowEqual from '../utils/shallowEqual'; +import bindActionCreators from '../utils/bindActionCreators'; + import isPlainObject from 'lodash/lang/isPlainObject'; import invariant from 'invariant'; + export default function createConnector(React) { const { Component, PropTypes } = React; @@ -13,11 +16,13 @@ export default function createConnector(React) { static propTypes = { children: PropTypes.func.isRequired, - select: PropTypes.func.isRequired + select: PropTypes.func.isRequired, + actionCreators: PropTypes.object }; static defaultProps = { - select: identity + select: identity, + actionCreators: {} }; shouldComponentUpdate(nextProps, nextState) { @@ -41,6 +46,7 @@ export default function createConnector(React) { this.unsubscribe = context.redux.subscribe(::this.handleChange); this.state = this.selectState({ context, props }); + this.boundActions = this.bindActionCreators(props.actionCreators, context.redux.dispatch); } componentWillReceiveProps(nextProps) { @@ -48,6 +54,10 @@ export default function createConnector(React) { // Force the state slice recalculation this.handleChange(); } + + if (nextProps.actionCreators !== this.props.actionCreators) { + this.boundActions = this.bindActionCreators(nextProps.actionCreators, this.context.redux.dispatch); + } } componentWillUnmount() { @@ -71,12 +81,18 @@ export default function createConnector(React) { return { slice }; } + bindActionCreators = (actionCreators, dispatch) => { + return { + actions: bindActionCreators(actionCreators, dispatch) + }; + } + render() { const { children } = this.props; const { slice } = this.state; const { redux: { dispatch } } = this.context; - return children({ dispatch, ...slice }); + return children({ dispatch, ...slice, ...this.boundActions }); } }; } diff --git a/test/components/Connector.spec.js b/test/components/Connector.spec.js index 9b135d7296..4362bd9bfa 100644 --- a/test/components/Connector.spec.js +++ b/test/components/Connector.spec.js @@ -145,6 +145,30 @@ describe('React', () => { expect(div.props.dispatch).toBe(redux.dispatch); }); + it('properly binds and passes action creators when passed as an object', () => { + const redux = createRedux({ test: () => 'test'}); + + const testActions = { + anAction: () => { + return { type: 'TEST_ACTION' }; + } + }; + + const tree = TestUtils.renderIntoDocument( + + {() => ( + + {({ dispatch, actions }) =>
} + + )} + + ); + + const div = TestUtils.findRenderedDOMComponentWithTag(tree, 'div'); + expect(Object.keys(div.props.actions)).toEqual(Object.keys(testActions)); + expect(div.props.actions.anAction).toBeA('function'); + }); + it('should throw an error if `state` returns anything but a plain object', () => { const redux = createRedux(() => {});