From 3e0eee99b8ac7c6d2518eb66de5fbc13ebf7203e Mon Sep 17 00:00:00 2001 From: Adam Miskiewicz Date: Sat, 13 Jun 2015 15:00:12 -0400 Subject: [PATCH 1/6] Allow for passing of actionCreators through `Connector` and `@connect` decorator --- src/components/createConnectDecorator.js | 7 +++- src/components/createConnector.js | 22 +++++++++- test/components/Connector.spec.js | 52 +++++++++++++++++++++++- 3 files changed, 76 insertions(+), 5 deletions(-) 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 6ae91ee45d..04b711d67b 100644 --- a/src/components/createConnector.js +++ b/src/components/createConnector.js @@ -1,5 +1,7 @@ import identity from 'lodash/utility/identity'; import shallowEqual from '../utils/shallowEqual'; +import bindActionCreators from '../utils/bindActionCreators'; + export default function createConnector(React) { const { Component, PropTypes } = React; @@ -11,7 +13,11 @@ export default function createConnector(React) { static propTypes = { children: PropTypes.func.isRequired, - select: PropTypes.func.isRequired + select: PropTypes.func.isRequired, + actionCreators: PropTypes.oneOfType([ + PropTypes.func, + PropTypes.object + ]) }; static defaultProps = { @@ -62,12 +68,24 @@ export default function createConnector(React) { return { slice }; } + bindActionCreators(dispatch) { + if (!this.props.actionCreators) { + return {}; + } + + if (typeof this.props.actionCreators === 'function') { + return { actions: this.props.actionCreators(dispatch) }; + } else { + return { actions: bindActionCreators(this.props.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.bindActionCreators(dispatch) }); } }; } diff --git a/test/components/Connector.spec.js b/test/components/Connector.spec.js index b274136d52..c0b7c24e3a 100644 --- a/test/components/Connector.spec.js +++ b/test/components/Connector.spec.js @@ -1,7 +1,7 @@ import expect from 'expect'; import jsdom from 'mocha-jsdom'; import React, { PropTypes, Component } from 'react/addons'; -import { createRedux } from '../../src'; +import { createRedux, bindActionCreators } from '../../src'; import { Connector } from '../../src/react'; const { TestUtils } = React.addons; @@ -144,5 +144,55 @@ describe('React', () => { const div = TestUtils.findRenderedDOMComponentWithTag(tree, 'div'); 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('properly binds and passes action creators when passed as a function', () => { + const redux = createRedux({ test: () => 'test'}); + + const testActions = { + anAction: () => { + return { type: 'TEST_ACTION' } + } + }; + + const tree = TestUtils.renderIntoDocument( + + {() => ( + ({ + anAction: (...args) => dispatch(testActions.anAction(...args)) + })}> + {({ 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'); + }); }); }); From eb52e83f052e28269b1b2e783b99f0f341a5fa85 Mon Sep 17 00:00:00 2001 From: Adam Miskiewicz Date: Sat, 13 Jun 2015 15:02:09 -0400 Subject: [PATCH 2/6] add comment about why we allow passing a function to actionCreators prop --- src/components/createConnector.js | 3 +++ 1 file changed, 3 insertions(+) diff --git a/src/components/createConnector.js b/src/components/createConnector.js index 04b711d67b..a01541be3d 100644 --- a/src/components/createConnector.js +++ b/src/components/createConnector.js @@ -73,6 +73,9 @@ export default function createConnector(React) { return {}; } + // Handle `function` version of prop that allows user + // to do fancy things, like change how actions are bound or + // change the shape of the `actions` object. if (typeof this.props.actionCreators === 'function') { return { actions: this.props.actionCreators(dispatch) }; } else { From f000e0c65a684b5f0ce54d975294c9e6e6f4de17 Mon Sep 17 00:00:00 2001 From: Adam Miskiewicz Date: Sat, 13 Jun 2015 15:07:54 -0400 Subject: [PATCH 3/6] fixing linting erros --- test/components/Connector.spec.js | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/test/components/Connector.spec.js b/test/components/Connector.spec.js index c0b7c24e3a..36fc79a8e3 100644 --- a/test/components/Connector.spec.js +++ b/test/components/Connector.spec.js @@ -1,7 +1,7 @@ import expect from 'expect'; import jsdom from 'mocha-jsdom'; import React, { PropTypes, Component } from 'react/addons'; -import { createRedux, bindActionCreators } from '../../src'; +import { createRedux } from '../../src'; import { Connector } from '../../src/react'; const { TestUtils } = React.addons; @@ -150,7 +150,7 @@ describe('React', () => { const testActions = { anAction: () => { - return { type: 'TEST_ACTION' } + return { type: 'TEST_ACTION' }; } }; @@ -165,7 +165,7 @@ describe('React', () => { ); const div = TestUtils.findRenderedDOMComponentWithTag(tree, 'div'); - expect(Object.keys(div.props.actions)).toEqual(Object.keys(testActions)) + expect(Object.keys(div.props.actions)).toEqual(Object.keys(testActions)); expect(div.props.actions.anAction).toBeA('function'); }); @@ -174,7 +174,7 @@ describe('React', () => { const testActions = { anAction: () => { - return { type: 'TEST_ACTION' } + return { type: 'TEST_ACTION' }; } }; @@ -191,7 +191,7 @@ describe('React', () => { ); const div = TestUtils.findRenderedDOMComponentWithTag(tree, 'div'); - expect(Object.keys(div.props.actions)).toEqual(Object.keys(testActions)) + expect(Object.keys(div.props.actions)).toEqual(Object.keys(testActions)); expect(div.props.actions.anAction).toBeA('function'); }); }); From 134eeb33f9abec541d0d7d5e03fca089caf50b05 Mon Sep 17 00:00:00 2001 From: Adam Miskiewicz Date: Sun, 14 Jun 2015 15:19:29 -0400 Subject: [PATCH 4/6] adding bound action creator memoization --- src/components/createConnector.js | 32 +++++++++++++------------------ test/components/Connector.spec.js | 26 ------------------------- 2 files changed, 13 insertions(+), 45 deletions(-) diff --git a/src/components/createConnector.js b/src/components/createConnector.js index a01541be3d..04e825ba15 100644 --- a/src/components/createConnector.js +++ b/src/components/createConnector.js @@ -14,14 +14,12 @@ export default function createConnector(React) { static propTypes = { children: PropTypes.func.isRequired, select: PropTypes.func.isRequired, - actionCreators: PropTypes.oneOfType([ - PropTypes.func, - PropTypes.object - ]) + actionCreators: PropTypes.object }; static defaultProps = { - select: identity + select: identity, + actionCreators: {} }; shouldComponentUpdate(nextProps, nextState) { @@ -45,6 +43,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) { @@ -52,6 +51,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() { @@ -68,19 +71,10 @@ export default function createConnector(React) { return { slice }; } - bindActionCreators(dispatch) { - if (!this.props.actionCreators) { - return {}; - } - - // Handle `function` version of prop that allows user - // to do fancy things, like change how actions are bound or - // change the shape of the `actions` object. - if (typeof this.props.actionCreators === 'function') { - return { actions: this.props.actionCreators(dispatch) }; - } else { - return { actions: bindActionCreators(this.props.actionCreators, dispatch) }; - } + bindActionCreators = (actionCreators, dispatch) => { + return { + actions: bindActionCreators(actionCreators, dispatch) + }; } render() { @@ -88,7 +82,7 @@ export default function createConnector(React) { const { slice } = this.state; const { redux: { dispatch } } = this.context; - return children({ dispatch, ...slice, ...this.bindActionCreators(dispatch) }); + return children({ dispatch, ...slice, ...this.boundActions }); } }; } diff --git a/test/components/Connector.spec.js b/test/components/Connector.spec.js index 36fc79a8e3..bb5a101bf0 100644 --- a/test/components/Connector.spec.js +++ b/test/components/Connector.spec.js @@ -168,31 +168,5 @@ describe('React', () => { expect(Object.keys(div.props.actions)).toEqual(Object.keys(testActions)); expect(div.props.actions.anAction).toBeA('function'); }); - - it('properly binds and passes action creators when passed as a function', () => { - const redux = createRedux({ test: () => 'test'}); - - const testActions = { - anAction: () => { - return { type: 'TEST_ACTION' }; - } - }; - - const tree = TestUtils.renderIntoDocument( - - {() => ( - ({ - anAction: (...args) => dispatch(testActions.anAction(...args)) - })}> - {({ 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'); - }); }); }); From 19070b7e446badfb506c47b0d7cb841ed8f4e1ed Mon Sep 17 00:00:00 2001 From: Adam Miskiewicz Date: Tue, 16 Jun 2015 00:39:36 -0400 Subject: [PATCH 5/6] first pass at readme updates --- README.md | 31 ++++++++++++++++--------------- 1 file changed, 16 insertions(+), 15 deletions(-) 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 ( - + ); } } From 3a00ff9d62f2ba1100fcc84c6b0cee829067f711 Mon Sep 17 00:00:00 2001 From: Adam Miskiewicz Date: Tue, 16 Jun 2015 00:41:55 -0400 Subject: [PATCH 6/6] updating examples with new syntax --- examples/containers/CounterApp.js | 8 +++----- examples/containers/TodoApp.js | 6 ++---- 2 files changed, 5 insertions(+), 9 deletions(-) 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 (