reduxjs / react-redux

Official React bindings for Redux
https://react-redux.js.org
MIT License
23.37k stars 3.37k forks source link

Alternative API proposals #1

Closed gaearon closed 9 years ago

gaearon commented 9 years ago

This API is taken from Redux <Provider>, <Connector>, @provide, @connect. Right now I don't have examples in this repo but it matches what you've seen in Redux so far.

It's the best we've got now, but I think we can do better!

Common pain points:

Let's go wild here. Post your alternative API suggestions.

They should satisfy the following criteria:

gaearon commented 9 years ago

For example, we could drop <Connector> altogether and encourage people to use @connect decorator as a function in another module.

Common case

A dumb component would look exactly as it does now. A smart component would look like

import React from 'react';
import { connect } from 'react-redux';
import Counter from '../components/Counter';
import * as CounterActions from '../actions/CounterActions';

export default connect(
  state => ({
    counter: state.counter
  }),
  dispatch => ({
    increment: dispatch(CounterActions.increment()),
    decrement: dispatch(CounterActions.decrement())
  })
)(Counter);

Note that the smart component doesn't have to be declared as a component. Also note that state => ... and dispatch => ... is all it accepts.

Case with more control

Want more customization? Want a componentDidUpdate hook? Want to select different things depending on the current props? Well, maybe you need to put a component in the middle then:

import React from 'react';
import connect from './connect';
import { bindActionCreators } from 'redux';
import Counter from '../components/Counter';
import * as CounterActions from '../actions/CounterActions';

class CounterContainer {
  componentDidUpdate() {
    ...
  }

  render() {
    const props = somehowSelectChildProps(this.props);
    return <Counter {...props} />
  }
}

export default connect(
  state => ({
    counter: state.counter
  }),
  dispatch => ({
    increment: () => dispatch(CounterActions.increment()),
    decrement: () => dispatch(CounterActions.decrement())
  })
)(CounterContainer);

This is an “explicit” smart component that is required for more advanced cases. Note that you didn't have to move files or refactor anything. You just put a component in the middle into the same file.

Shortcuts

Finally, we can still offer bindActionCreators, but with a actionCreators => dispatch => obj signature, so that the result is usable as the second parameter:

import React from 'react';
import { connect } from 'react-redux';
import Counter from '../components/Counter';
import * as CounterActions from '../actions/CounterActions';

export default connect(
  state => ({  counter: state.counter }),
  bindActionCreators(CounterActions)
)(Counter);

Perhaps we can even go further and bind automatically if an object is passed.

import React from 'react';
import { connect } from 'react-redux';
import Counter from '../components/Counter';
import * as CounterActions from '../actions/CounterActions';

export default connect(
  state => ({  counter: state.counter }),
  CounterActions
)(Counter);

“Wait!”, I hear you say. What if an action depends on some prop from the state? Well, in this case you put a component in the middle like I described above.

import React from 'react';
import { connect } from 'react-redux';
import Counter from '../components/Counter';
import * as CounterActions from '../actions/CounterActions';

class CounterContainer {
  increment() {
    this.props.increment(this.props.id);
  }

  render() {
    return <Counter {...this.props} increment={this.increment} />
  }
}

export default connect(
  state => ({  counter: state.counter }),
  CounterActions
)(CounterContainer);

Any sufficiently complicated case => component in the middle. Easy!

Am I missing something?

acdlite commented 9 years ago

:+1: I like this. It's essentially the same API as https://github.com/acdlite/redux-rx#createconnectorselectstate-render, just without streams.

acdlite commented 9 years ago

Ah, only thing I see missing is a way to access props from owner — e.g. if you're wrapped by Relay.

gaearon commented 9 years ago

Can you clarify? Isn't this “Case with more control” above?

acdlite commented 9 years ago

That works, I'm just not sure I like it... Seems like too common a case to necessitate what is essentially two smart components.

acdlite commented 9 years ago

Could we just pass the owner props as the second argument?

gaearon commented 9 years ago

From the user point of view they're just declaring one “real” component so no big deal IMO. On the other hand once you start selecting data in a tricky way, you begin to want finer control over perf optimizations, lifecycle hooks and maybe passing data down in a trickier way so it's likely you'll want a component anyway.

Once we start passing props to the state getter, we'll also probably want to pass props to the action creators getter. However, this forces us to bind on every prop change, which is a perf hit and unfriendly to shouldComponentUpdate optimizations down the rendering chain.

Example: you might want to read from props if you have a route handler that gets specific data related to the current route parameters. But then you already want to fire an action in componentWillReceiveProps and componentDidMount to fetch the data! So you already need an intermediate component anyway.

skevy commented 9 years ago

So are you just removing the idea of a higher order component here? The signature of "connect" seems the same as what was proposed in #86 (at least the shorthand one you suggested where you just pass an object of actions as the second param).

acdlite commented 9 years ago

@gaearon

On the other hand once you start selecting data in a tricky way, you begin to want finer control over perf optimizations, lifecycle hooks and maybe passing data down in a trickier way so it's likely you'll want a component anyway.

I really think accessing props from the owner is a much more common case that using lifecycle hooks. Props passing is the fundamental contract of React. We'll soon live in a world where pure functions are valid React components. The fewer "smart" components the better — creating class components will start to become a low-level implementation detai. Function components will be the new default. (At least that's what should happen. We'll see if the community catches on.)

However, this forces us to bind on every prop change, which is a perf hit and unfriendly to shouldComponentUpdate optimizations down the rendering chain.

This seems like a micro-optimization but okay. You could get around this by binding once if an object is passed ("bind automatically if an object is passed") but bind every time if its a function. Also if an action creator depends on a prop it's going to lead to updates further down the rendering chain, anyway.

acdlite commented 9 years ago

^ The reason I say it's a micro-optimization is you're rarely going to pass action creators more than one level down the component tree.

skevy commented 9 years ago

@acdlite also, it's easy enough to prevent constant rebinding with memoization. We had explored this a bit in redux#86

ryancole commented 9 years ago

I also agree with the sentiment that you'll rarely pass action creators more than one level down a component tree. This makes me question the over all usefulness of bindActionCreators. It seems to contrast the simplicity and obviousness of redux in general. It seems like it'd be clearer to just force users to pass dispatch down as a prop to every component, as described in the current readme file.

As a user, it seems like if dispatch is always needed to call an action, then maybe some way to remove the onus on the developer to pass dispatch down the tree. It'd be cool if you could just import your action methods and call them, no?

acdlite commented 9 years ago

@ryancole I still think bindActionCreators() (or some equivalent) is useful. You shouldn't pass dispatch() to a dumb component; you should bind it in a smart component first. E.g. your dumb components should look like this:

class CounterButton extends Component {
  render() {
    const { increment } = this.props;
    <button onClick={increment} />; 
  }
}

Rather than this:

class CounterButton extends Component {
  render() {
    const { dispatch } = this.props;
    <button onClick={dispatch(increment())} />; 
  }
}

It may seem like a trivial difference, but the first version is more separated from the implementation details of how it receives its callback. This leads to more scalable, maintainable, testable code.

It'd be cool if you could just import your action methods and call them, no?

You have to bind them somewhere. Remember, action creators in Redux are simply pure functions. Either we bind them in smart components, or we have to bind them at store creation time, in which case we'd need some sort of API for accessing the bound components. Or you could use singletons, but yuck.

acdlite commented 9 years ago

I like @gaearon's idea of passing action creators as the second param and auto-binding:

export default connect(
  state => ({  counter: state.counter }),
  CounterActions
)(CounterContainer);

That way we only need to bind once, and the user doesn't need to worry about bindActionCreators.

I would amend that proposal to also support a second form, where you pass a function that maps to unbound action creators:

export default connect(
  state => ({  counter: state.counter }),
  (state, props) => ({
    increment: () => CounterActions.increment(props.something)
  })
)(CounterContainer);

that way you can access the store state and props, if needed. bindActionCreators() becomes an implementation detail.

ryancole commented 9 years ago

@acdlite I agree with how you explained why bindActionCreators is needed, now. I wasn't thinking in terms of smart and dumb components.

Although something about the idea of a tree of components having most of the parent, outer-most components as smart components and then all the edge node components as dumb (this is what I think this smart / dumb component pattern lends itself to) kind of seems like a stink. I don't have an optimal pattern in mind, and I know smart / dumb components are a current popular pattern, but this as a pattern seems like it creates scenarios where if a dumb component is way down the tree you'll have to pass down action methods or dispatch all the way down the tree to get to it, thus making the components on the way to that component possibly carry along unneeded props just to satisfy their children. Maybe this is result of bad component tree design or something, though, on my part.

aaronjensen commented 9 years ago

Just thinking outside the box here, but what if bound actions were just a part of the state:

export default connect(
  state => ({ counter: state.counter,
    increment: state.actions.counter.increment(state.something) })
)(CounterContainer);

or, less radical:

export default connect(
  (state, bind) => ({ counter: state.counter, 
    increment: bind(CounterActions.increment(state.something)) })
)(CounterContainer);

It seems weird to me to do generate two separate objects that are ultimately merged into the child's props.

acdlite commented 9 years ago

@aaronjensen Regarding your first proposal:

First of all, the state object is not necessarily a plain JavaScript object. Redux makes no assumptions about the type of state returned from the reducer. For instance, you could use an Immutable Map.

Second of all, where are the action creators coming from? You say they're part of the state, but how did they get there? We'd need some sort of system for registering action creators at the global level.

aaronjensen commented 9 years ago

@acdlite Yep, that's right. We have a system for registering reducers, it didn't seem a long stretch to have one for actions. And you're right re: state not being a plain JS object of course. In that case two arguments could come along: (state, actions) or actions could have their own reducer but that seems a little odd.

Tbh, I can't think of a particularly compelling reason for it other than slight convenience at that point at the cost of required registration.

acdlite commented 9 years ago

How is globally registering action creators more elegant than this?

export default connect(
  state => ({  counter: state.counter }),
  CounterActions
)(CounterContainer);

EDIT: nevermind, I see that you changed your mind :)

aaronjensen commented 9 years ago

Yeah, that solution does look good. It didn't sit well with me at first that props were being combined in secret, but being able to handle the binding automatically makes it worth it. I also like your proposal for the second form which takes a function.

It'd probably be a bad idea to assume that all functions are action creators, yea?

export default connect(
  (state, props) => ({  
    counter: state.counter,
    increment: () => CounterActions.increment(props.something)
  })
)(CounterContainer);

If not, then it allows for everything w/ one argument:

export default connect(
  state => ({
    counter: state.counter
    ...CounterActions,
    ...OtherActions,
  }),
)(CounterContainer);

You could also make it an n-arity function and just merge all of the objects (binding all functions automatically).

This only works if it's safe to assume all functions are action creators though...

emmenko commented 9 years ago

we could drop <Connector> altogether and encourage people to use @connect decorator

I think it's a good idea, and we can do the same for Provider. This will simplify the API and create less confusion.

This leads to more scalable, maintainable, testable code

@acdlite I was also thinking of dropping bindActionCreators, as it's just syntactic sugar for dispatch(myAction()), but you make a valid point.

And passing the actions as a second argument of connect makes it a good API, given that the binding becomes an implementation detail of the decorator and the user doesn't care about it.

One thing I would also like to have is namespacing props. Basically instead of just spreading the actions or whatever to this.props, we can have a actions object that contains all the actions, and just pass the object to props. Same thing could be done for state. I think this is important when you start having other data in props (e.g.: router) and helps avoiding possible conflicts when merging props. Here an example:

// Instead of a flat structure like
this.props = {
  children: Object,

  dispatch: Function,
  // state
  todos: Object,
  counter: Object,
  // actions
  addTodo: Object,
  increment: Object,
  decrement: Object,

  // router
  isTransitioning: Boolean,
  location: Object,
  params: Object,
  route: Object,
  routeParams: Object,

  // custom props
  foo: Object,
  onClick: Function
}

// we could have a much cleaner structure
this.props = {
  children: Object,

  dispatch: Function,
  // state
  state: {
    todos: Object,
    counter: Object
  },
  // actions
  actions: {
    addTodo: Object,
    increment: Object,
    decrement: Object
  },

  // router
  router: {
    isTransitioning: Boolean,
    location: Object,
    params: Object,
    route: Object,
    routeParams: Object
  },

  // custom props
  foo: Object,
  onClick: Function
}

Thoughts?

gaearon commented 9 years ago

I really think accessing props from the owner is a much more common case that using lifecycle hooks.

Generally connecting to Redux should be done at route handler level, and in this case you need the hooks too. Smart components close to the top level rarely receive props that somehow uniquely identify them, so even if they have props, I doubt these props are so relevant to selecting the state. For example, you won't connect <TodoItem>—you'll probably connect the whole <TodoList> in which case props are irrelevant, as you want to select the whole related slice of state.

Can you please help me by providing a few examples where props are important at the connect level?

This seems like a micro-optimization

It's really not. :-) It seems like a micro-optimization but it's the beginning of death by a thousand cuts. Redux connector sits close to the top of the application, and if at this level we're getting new functions on every prop change, no component down the chain can benefit from shouldComponentUpdate. The whole subtree is contaminated by a few changing functions.

Surely you won't see this problem at the beginning, but as soon as you get a perf bottleneck in one of your components, adding PureRenderMixin to it won't “just work” anymore because Redux rebinds these actions on every prop change. We don't want people to end up in this situation.

You could get around this by binding once if an object is passed ("bind automatically if an object is passed") but bind every time if its a function.

I can.. But then changing two seemingly equivalent signatures will have bad perf consequences for the whole rendering chain. It's too easy to make this mistake and later have no idea how to optimize your app because shouldComponentUpdate stopped helping anywhere down the chain.

On the other hand, if we force user to create a component, this won't be a problem, as they will pass the component's functions down. And the component's functions can look into props just fine.

Also if an action creator depends on a prop it's going to lead to updates further down the rendering chain, anyway.

Yes, but in a way totally manageable by shouldComponentUpdate! If unrelatedToHeavyComponents state changes too often, but <HeavyComponents> down the chain accept increment, they won't update every time unrelatedToHeavyComponents changes. On the other hand, if we go with binding on prop change, <HeavyComponents> will receive new increment every time unrelatedToHeavyComponents changes and they'll have to re-render. We don't know for sure which props are used by which action creators. I still think never binding is the easiest solution. It's not too hard to write a component, and you're in full control if you do.

^ The reason I say it's a micro-optimization is you're rarely going to pass action creators more than one level down the component tree.

Can you elaborate on that? I usually pass them several levels down (at some point they turn into dumb on* props but I still pass them down).

it's easy enough to prevent constant rebinding with memoization

Memoization only avoids “rebind on every render” in favor of “rebind on every prop change”. It's certainly better than nothing, but worse than “never rebind”.

gaearon commented 9 years ago

@emmenko Namespacing will also kill shouldComponentUpdate optimizations because you can't shallowly compare props anymore and nobody will write code to handle namespaces separately.

emmenko commented 9 years ago

@gaearon right, haven't consider this. I guess there's no much way around it then...

gaearon commented 9 years ago

How about we add a third parameter: merge.

Default:

export default connect(
  state => ({ counter: state.counter }),
  CounterActions,
  (state, actions, props) => ({
    ...state,
    ...actions
  })
)(Counter);

But you can also...

export default connect(
  state => ({ counter: state.counter }),
  CounterActions,
  (state, actions, props) => ({
    ...state,
    ...actions,
    increment: (...args) => actions.increment(props.counterId, ...args)
  })
)(Counter);

This gives you the same control, but any binding is explicit and performed by you.

gaearon commented 9 years ago

In fact as long as you don't care for lifecycle hooks you can do this:

export default connect(
  state => ({ counters: state.counters }),
  CounterActions,
  (state, actions, props) => ({
    counter: state.counters[props.counterId],
    increment: () => actions.increment(props.counterId)
  })
)(Counter);

Binding is performed by you instead of the library, so you can easily find where it happens if you have shouldComponentUpdate problems. If this begins to work bad performance-wise, you have a clear upgrade path: turn merge function into a component.

emmenko commented 9 years ago

So to be clear, connect would have following signature now?

function connect (state: Function, actions: Object, merge: Function)

This gives you the same control, but any binding is explicit and performed by you.

Not sure exactly what do you mean by binding though. This has nothing to do with "binding the dispatch", right?

you have a clear upgrade path: turn merge function into a component

You mean by putting a component in the middle like in your first example? Or do you mean something else?

Thanks!

skevy commented 9 years ago

You said above that "connecting" components using usually happens at or near the route handler level.

I wholeheartedly agree with this, and is definitely what I've experienced in general. When you bind to Redux (or really, binding to any other Flux library) too far down, then changes are difficult to trace and weird things start to happen.

However, that doesn't mean that you wouldn't want to provide the action creators deeper in the tree. That leads me to believe that maybe we can keep "connect" simple - don't worry about binding any action creators with it - and then introduce some other helper that can be a convienence to bind action creators on a component level.

I worry that connect is becoming too heavy, and originally I really liked how simple connect was to understand. It's almost self-documenting.

skevy commented 9 years ago

An example: if I have a , obviously I want to bind to Redux near the top. Perhaps with the component itself. But what if (because I'm making a sufficiently complicated TodoList) I have the button that I want to check off the todo three levels deeper. In that case, if I'm not connecting the component that deep (which would be silly anyway), I would have to pass "dispatch" down three levels explicitly in props so that I could call the action creator when the button is clicked.

The only thing I really want to get around here is just that explicit passing. I think the dispatching is an implementation detail (albeit an important one). As the user of Redux, I just want to be able to call my action creator, not pass dispatch three layers deep explicitly through props.

gaearon commented 9 years ago

So to be clear, connect would have following signature now?

function connect (state: Function, actions: Object, merge: Function)

To be honest I'd still rather go with:

connect(
  State => StateProps,
  ?(Dispatch => DispatchProps),
  ?(StateProps, DispatchProps, ParentProps => Props)
)

The reason I don't want “magic” behavior for objects is people will see this once and use everywhere. It won't be obvious that you don't have to pass an object.

In most cases I'll do this:

connect(
  select,
  bindActionCreators(CounterActions)
)

But there are cases where another option is nicer, for example:

connect(
  select,
  dispatch => ({ dispatch })
)(MySmartComponent) // I want to use vanilla this.props.dispatch

See, I might not want binding a particular set of action creators at all!

Or maybe parameterized actions:

connect(
  select,
  dispatch => dispatch,
  (state, dispatch, props) => ({
    ...state,
    ...bindActionCreators(createMyActions(props.apiUrl))
  })
)(MySmartComponent)

(Again, this is not super performant but my conscience is clear because binding occurs in user code.)

So I'd rather not have the shortcut, and instead force user to understand how bindActionCreator works as a utility. This goes in line with some other shortcuts we're removing in Redux 1.0 API in favor of explicit equivalents.

Not sure exactly what do you mean by binding though. This has nothing to do with "binding the dispatch", right?

Why, that's exactly what I mean :-). In my example, increment: (...args) => actions.increment(props.counterId, ...args) is akin to function binding, but it's performed in your code instead of library code, so it's easier to find this as a perf regression culprit.

You mean by putting a component in the middle like in your first example? Or do you mean something else?

Yes. As soon as you have perf problems you just “upgrade” (or maybe “downgrade” :-) your function to a proper component. It's similar to how you'd probably “upgrade” pure functions to components after React 0.14 if there are some perf optimizations you can't do in a pure function for whatever reason.

emmenko commented 9 years ago

@gaearon @acdlite considering another approach, what would be the pros / cons of using context?

Currently we have getState and dispatch there. Would it make sense to have also getActions? Then you don't have to pass them around anymore via props, you can just get them from the context. This would also keep connect a simple component that subscribes to changes.

I don't know, just thinking out loud...

skevy commented 9 years ago

@emmenko this is kind of what I'm getting at as well, but admittedly have not though through possible cons of the "context" idea.

gaearon commented 9 years ago

However, that doesn't mean that you wouldn't want to provide the action creators deeper in the tree.

I think binding action creators at different tree levels is an anti-pattern. You should strive to do this at the top. Passing additional props as callbacks is really what React is all about.

You can always get around it by having connect(() => ({}), bindActionCreators(MyActions)) in the middle of the tree but it looks weird and I think that's a good thing, as it will encourage people to follow React's explicit way.

I think the dispatching is an implementation detail (albeit an important one). As the user of Redux, I just want to be able to call my action creator, not pass dispatch three layers deep explicitly through props.

That leads me to believe that maybe we can keep "connect" simple - don't worry about binding any action creators with it - and then introduce some other helper that can be a convienence to bind action creators on a component level.

I used to think about connect as a way of subscribing to changes, but I don't anymore. I think connect should be the boundary between Redux-aware and Redux-unaware parts of your component tree. Therefore it fits it to be slightly more powerful, so that components below are completely untied of Redux. Now I think that wherever you materialize the state, you should materialize the actions too.

gaearon commented 9 years ago

Would it make sense to have also getActions?

Action creators are not a core concept of Redux. There is no reason for them to live at the top. They are just convenience. Sometimes you want a factory of action creators that depends on some props. Can't do this at the very top.

After all if you really want those action creators to be in context, you can do this yourself: https://gist.github.com/mattapperson/db45538f14d6de52f6ad

But it's on your conscience. :-)

gaearon commented 9 years ago

I'd love to hear some thoughts from @faassen here.

gaearon commented 9 years ago

Also at this point wrapActionCreators might be a better naming than bindActionCreators.

emmenko commented 9 years ago

After all if you really want those action creators to be in context, you can do this yourself

Sure, I'm just always a bit afraid how much is it ok to use context. But just to understand, if actions were a core concept, would it have been ok to put them into context and just access them wherever I want?

Anyway, are we heading now towards this connect signature and give it more responsibilities, as you said?

connect(
  State => StateProps,
  ?(Dispatch => DispatchProps),
  ?(StateProps, DispatchProps, ParentProps => Props)
)
gaearon commented 9 years ago

But just to understand, if actions were a core concept, would it have been ok to put them into context and just access them wherever I want?

Can you rephrase? I don't understand what exactly you are asking.

Anyway, are we heading now towards this connect signature and give it more responsibilities, as you said?

This signature is what feels right to me at the moment. Waiting for @acdlite to take a look.

gaearon commented 9 years ago

In fact this is more correct signature:

connect(
  State => StateAny,
  ?(Dispatch => DispatchAny),
  ?(StateAny, DispatchAny, ParentProps => Props)
)

We don't really care if they are objects until the third function is applied.

For example, you can do:

connect(
  state => state.counter,
  wrapActionCreators(CounterActions),
  (counter, actions) => ({ counter, ...actions })
)

or:

connect(
  state => state.counter,
  dispatch => dispatch,
  (counter, dispatch) => ({ counter, dispatch })
)

but also:

connect(
  state => ({ counter: state.counter }),
  wrapActionCreators(CounterActions)
)

as before.

emmenko commented 9 years ago

Just wanted to know how much one is it ok to use context and if it would have been a good approach, in case actions were considered an important part of redux. Just for my understanding of designing a good API :)

Hope it's clear now, thanks!

gaearon commented 9 years ago

Named arguments:

Vanilla form

connect({
  state: state => ({ state.counter }),
  actions: dispatch => ({
    increment: dispatch(CounterActions.increment())
  })
})(Counter) 

wrapActionCreators sugar

connect({
  state: state => ({ state.counter }),
  actions: wrapActionCreators(CounterActions)
})(Counter)

I want just the state

connect({
  state: state => ({ state.counter })
})(Counter) 

I want just some action creators

connect({
  actions: wrapActionCreators(CounterActions)
})(Counter) 

I want just the dispatch function

connect({
  actions: dispatch => ({ dispatch })
})(Counter) 

I want to pass several action creator groups down

function wrapActionCreatorMap(map) {
  return dispatch => mapValues(map, actionCreators => wrapActionCreators(actionCreators)(dispatch));
}

connect({
  state: state => state.counter,
  actions: wrapActionCreatorMap({
    counterActions: CounterActions,
    userActions: UserActions
  })
})(Counter)

I want to merge several action creator groups

connect({
  state: state => state.counter,
  actions: wrapActionCreators(...CounterActions, ...UserActions)
})(Counter)

I want to pass actions down in a single object

function merge(counter, actions, props) {
  return { counter, actions, ...props };
}

connect({
  state: state => state.counter,
  actions: wrapActionCreators(CounterActions)
}, merge)(Counter)

I want to use props for getting state and tweaking actions

function merge(counters, actions, props) {
  const { counterId, ...rest } = props;
  const { increment, decrement } = actions;
  return {
    counter: counters[counterId],
    increment: () => increment(counterId),
    decrement: () => decrement(counterId),
    ...rest
  };
}

connect({
  state: state => state.counters,
  actions: wrapActionCreators(CounterActions)
}, merge)(Counter)
gaearon commented 9 years ago

Normal arguments:

Vanilla form

connect(
  state => ({ state.counter }),
  dispatch => ({
    increment: () => dispatch(CounterActions.increment())
  })
)(Counter) 

wrapActionCreators sugar

connect({
  state => ({ state.counter }),
  wrapActionCreators(CounterActions)
})(Counter)

I want just the state

connect(
  state => ({ state.counter })
)(Counter) 

I want just some action creators

connect(
  null, // means "no need to subscribe at all"
  wrapActionCreators(CounterActions)
)(Counter) 

I want just the dispatch function

connect(
  null,
  dispatch => ({ dispatch })
)(Counter) 

I want to pass several action creator groups down

function wrapActionCreatorMap(map) {
  return dispatch => mapValues(map, actionCreators => wrapActionCreators(actionCreators)(dispatch));
}

connect(
  state => state.counter,
  wrapActionCreatorMap({
    counterActions: CounterActions,
    userActions: UserActions
  })
)(Counter)

I want to merge several action creator groups

connect(
  state => state.counter,
  wrapActionCreators(...CounterActions, ...UserActions)
)(Counter)

I want to pass actions down in a single object

function merge(counter, actions, props) {
  return { counter, actions, ...props };
}

connect(
  state => state.counter,
  wrapActionCreators(CounterActions),
  merge
)(Counter)

I want to use props for getting state and tweaking actions

function merge(counters, actions, props) {
  const { counterId, ...rest } = props;
  const { increment, decrement } = actions;
  return {
    counter: counters[counterId],
    increment: () => increment(counterId),
    decrement: () => decrement(counterId),
    ...rest
  };
}

connect(
  state => state.counters,
  wrapActionCreators(CounterActions),
  merge
)(Counter)
emmenko commented 9 years ago

I like this! :+1:

emmenko commented 9 years ago

I mean the named args (sorry, didn't see you posted the other approach)

gaearon commented 9 years ago

I prefer the second version because:

acdlite commented 9 years ago

:+1: I prefer the second version, too. It addresses my primary concern from above, which is to access to owner props without needing an extra smart component in the middle, without sacrificing default performance optimizations.

gaearon commented 9 years ago

@acdlite

If we agree on that, what do you think is reasonable release-wise? We could release this version as react-redux@1.0.0, or we could release the current version as 1.0 but jump to 2.0 with this proposal.

It probably makes sense to use the new version in new docs..

emmenko commented 9 years ago

@gaearon do you think connect is still a correct name for the new signature?

acdlite commented 9 years ago

@gaearon I think we should hold of moving react-redux to 1.0 for a while until we've settled on the new API. The current API is not a 1.0 in my view, and I don't see any problem in keeping react-redux at 0.12 (or whatever) even while Redux gets a 1.0 release. That's the advantage of separating the projects.

FAQinghere commented 9 years ago

Random musings...

<Provider> is a bit generic, would <Redux> work? For somebody new to the framework, it helps them immediately get oriented.

I like 'connect', as in 'connect to Redux, the state manager.' Just brainstorming, but 'inject' might also work... inject (state and wrapped action creators).

Regarding:

connect({
  state: state => ({ state.counter }),
  actions: wrapActionCreators(CounterActions)
})(Counter)

I know it's longer, but I think it would be better to use actionCreators: wrapActionCreators(...). First, this is already the less-preferred, more typerific version of the proposed API. Second, elsewhere we are careful to differentiate between actions and action creators, so I think the value in maintaining the distinction in the API is worth the extra typing for those who want to use this more explicit syntax.

Also, in this version of the API, could we not just pass an array of actionCreators?

connect({
  state: state => ({ state.counter }),
  actionCreators: CounterActions
})(Counter)

Regarding the <Connector> syntax, select= feels a little off for some reason. Hmmm, maybe it's an echo from using Ruby years ago, but select makes me feel like the function is filtering an enumeration, rather than just directly grabbing a slice of the passed in state parameter. Perhaps slice or pick or something along those lines. Then again we're passing in a function, so maybe the term should make that clear, so more like slicer= or picker=? Eg. the slicer function returns the slice of state we want access to.

Just out of curiosity, why was wrapActionCreators better than bindActionCreators?