Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
31 changes: 16 additions & 15 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Whitespace is significant in Markdown :-). I have these double spaces for a reason here. Can you please change them back?


**The API might change any day.**
**The API might change any day.**
_**Don't use in production just yet.**_

## Why another Flux framework?
Expand Down Expand Up @@ -140,10 +140,8 @@ export default class Counter {

```js
// The smart component may observe stores using `<Connector />`,
// 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';
Expand All @@ -158,11 +156,13 @@ function select(state) {
export default class CounterApp {
render() {
return (
<Connector select={select}>
{({ 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)
<Connector select={select} actionCreators={CounterActions}>
{({ counter, actions }) =>
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Shouldn't this be actionCreators for consistency then? cc @acdlite

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Quick question, isn't select a bit too generic? Flummox is calling the stores and actions, I think the distinction is worth it. The way I see it, it makes the overall code simpler to read and follow. Am I missing something and is there any specific reasons for the current names?

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think if we call it “stores” people will be tempted to pass actual Store functions there.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I see your point, I just think that select is probably too broad and can work for both: actions and stores and that might be confusing too

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Maybe something with a more contextual name (e.g.: selectStoresByName) ?

/* Yes this is child as a function. */
<Counter counter={counter}
{...bindActionCreators(CounterActions, dispatch)} />
<Counter counter={counter} {...actions} />
}
</Connector>
);
Expand All @@ -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 (
<Counter counter={counter}
{...bindActionCreators(CounterActions, dispatch)} />
<Counter counter={counter} {...actions} />
);
}
}
Expand Down
8 changes: 3 additions & 5 deletions examples/containers/CounterApp.js
Original file line number Diff line number Diff line change
@@ -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 (
<Counter counter={counter}
{...bindActionCreators(CounterActions, dispatch)} />
<Counter counter={counter} {...actions} />
);
}
}
6 changes: 2 additions & 4 deletions examples/containers/TodoApp.js
Original file line number Diff line number Diff line change
@@ -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';
Expand All @@ -8,14 +7,13 @@ import * as TodoActions from '../actions/TodoActions';
export default class TodoApp {
render() {
return (
<Connector select={state => ({ todos: state.todos })}>
<Connector select={state => ({ todos: state.todos })} actionCreators={TodoActions}>
{this.renderChild}
</Connector>
);
}

renderChild({ todos, dispatch }) {
const actions = bindActionCreators(TodoActions, dispatch);
renderChild({ todos, actions }) {
return (
<div>
<AddTodo {...actions} />
Expand Down
7 changes: 5 additions & 2 deletions src/components/createConnectDecorator.js
Original file line number Diff line number Diff line change
Expand Up @@ -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)})`;

Expand All @@ -14,7 +14,10 @@ export default function createConnectDecorator(React, Connector) {

render() {
return (
<Connector select={state => select(state, this.props)}>
<Connector
select={state => select(state, this.props)}
actionCreators={actionCreators}
>
{stuff => <DecoratedComponent {...stuff} {...this.props} />}
</Connector>
);
Expand Down
22 changes: 19 additions & 3 deletions src/components/createConnector.js
Original file line number Diff line number Diff line change
@@ -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;

Expand All @@ -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) {
Expand All @@ -41,13 +46,18 @@ 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) {
if (nextProps.select !== this.props.select) {
// Force the state slice recalculation
this.handleChange();
}

if (nextProps.actionCreators !== this.props.actionCreators) {
this.boundActions = this.bindActionCreators(nextProps.actionCreators, this.context.redux.dispatch);
}
}

componentWillUnmount() {
Expand All @@ -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 });
}
};
}
24 changes: 24 additions & 0 deletions test/components/Connector.spec.js
Original file line number Diff line number Diff line change
Expand Up @@ -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(
<Provider redux={redux}>
{() => (
<Connector actionCreators={testActions}>
{({ dispatch, actions }) => <div dispatch={dispatch} actions={actions} />}
</Connector>
)}
</Provider>
);

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(() => {});

Expand Down