-
-
Notifications
You must be signed in to change notification settings - Fork 3.4k
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
MapStateToProps shorthand syntax: Supporting factory selectors #724
MapStateToProps shorthand syntax: Supporting factory selectors #724
Conversation
a0a7d99
to
ead6fde
Compare
docs/api.md
Outdated
@@ -56,6 +56,8 @@ It does not modify the component class passed to it; instead, it *returns* a new | |||
|
|||
If your `mapStateToProps` function is declared as taking two parameters, it will be called with the store state as the first parameter and the props passed to the connected component as the second parameter, and will also be re-invoked whenever the connected component receives new props as determined by shallow equality comparisons. (The second parameter is normally referred to as `ownProps` by convention.) | |||
|
|||
If an object is passed, each function inside it is assumed to be a Redux selector or a Factory function. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
don't forget on line 55 to document the argument as [mapStateToProps(state, [ownProps]): stateProps] \(*Object* or *Function*)
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Done! Thanks!
Am I right to assume that, using this shorthand, a selector could not return a function that wasn't intended to be a factory?
I would remove the factory function feature from the PR because I think it would be simpler and more easily justify the inclusion of the shorthand. If you're looking to construct selectors per component instance, you can use @connectAdvanced. It looks like you have the last PR standing, but I bet it's going to be here for awhile with such a substantial change to the API. |
Yeah, I'm afraid I've been focusing on other tasks and not actively trying to get this (and other long-standing PRs) pushed through to completion. I can't promise a specific deadline, but I will put reviewing this as a high priority and will do my best to get to it within the next week-ish. |
Yes.
That's fair, but you could also argue that in that case you could just use the normal connector: @connect(state => ({
getterFnForFooState: state => () => state.foo
})) Not that I've actually ever run into a situation where I need to do that... On the other hand I use very often factory selectors. I would find it very handy to be able to have some properties that come from a factory selector and some properties that come from a normal selector, that's a situation that I run pretty often into and this PR would make that possible.
I don't think that this is a substantial change to the API at all as these changes are backwards compatible. This is just a small addition to the API, just an arguably handy shorthand... The API remains almost the same. Notice that the changes into the documentation are minimal, that itself indicates that the API doesn't change much. cc @pcwinters |
It would be great to see this get landed - I'd love to help if there's anything remaining to be done. I did want to weigh in against the factory properties, though:
|
Well, I clearly didn't put reviewing this as a high priority, unfortunately. Sorry :( But, I am actually trying to review this now. I'm not 100% familiar with the current React-Redux v5 implementation to begin with, so I'll need some time to understand how this actually changes things. |
Initial thoughts:
I'm not ready to merge this in yet, but I think it's on the right path. I think if we can add some more tests around both the existing behavior and the new behavior, and improve the docs appropriately, we can get this in. Poking @jimbolla to get his thoughts on the PR. |
I probably should have worded this more clearly. What I mean is that I would expect this: connect(state => ({
foo: 'bar',
hello: () => console.log('world')
})) ...to be connect({
foo: 'bar',
hello: () => console.log('world')
}) In other words, I would expect I see your point that function property values aren't an intended use case, but FWIW, I've personally shoved function properties in there in cases where I didn't actually need direct access to Aside from introducing what I'd argue is unexpected behavior, I don't really see the benefit of property-level factory functions. From a verbosity perspective, they're essentially a tie when you have a single property: // 54 chars
connect(state => ({
foo: selectors.getFoo(state)
}))
// vs...
// 53 chars
connect({
foo: state => selectors.getFoo(state)
}) ...and once you get past a single property, the new shorthand syntax starts to lose badly on that front. TL;DR: it might be a corner case, but the property level factory methods seem surprising to me, and they render the "shorthand" syntax more verbose if people were to actually use them. |
@jmar777 : if you want to pass in a function, why not just pass it in as a normal prop? The use case for the property-level factory functions here, I think, is the same as the overall factory function capability already supported by it('should allow providing a factory function to mapStateToProps object', () => {
let updatedCount = 0
let memoizedReturnCount = 0
const store = createStore(() => ({ value: 1 }))
const mapStateFactory = () => {
let lastVal, lastProp
return (state, props) => {
if (props.name === lastProp && lastVal === state.value) {
memoizedReturnCount++
return lastProp
}
lastVal = state.value
return lastProp = props.name
}
}
@connect({ name: mapStateFactory })
class Container extends Component {
componentWillUpdate() {
updatedCount++
}
render() {
return <Passthrough {...this.props} />
}
}
TestUtils.renderIntoDocument(
<ProviderMock store={store}>
<div>
<Container name="a" />
<Container name="b" />
</div>
</ProviderMock>
)
store.dispatch({ type: 'test' })
expect(updatedCount).toBe(0)
expect(memoizedReturnCount).toBe(2)
}) So, |
connect({
foo: 'bar',
hello: () => console.log('world')
}) This seems not a good idea because on every state change, the connect component would always re-render because it gets a new hello function everytime. |
@slorber : to be honest, I'm not even sure exactly what that syntax would do. I think, given this PR, it would be interpreted as a selector, and wind up returning a prop of |
Code organization / separation of concerns, primarily. Obviously depends on the specific semantics of the component and the function in question.
Right - this was just a minimal example that would exhibit what I believe is surprisingly behavior. The same issue would be exhibited by, e.g.: const logWorld = () => console.log('world');
connect({ foo: 'bar', hello: logWorld }); Edit: on second thought, I'm not 100% that's true with the shorthand syntax. I'd need to trace through the PR, but it seems likely that the same function reference would be reused.
Correct, and that's what I think would be surprising, as returning that same object from the factory function produces different behavior: const obj = { onClick: someHandler };
// this:
connect(obj);
// ...behaves differently than this:
connect(() => obj); At any rate, I seem to be in the minority here in expecting that objects supplied for the shorthand syntax should behave the same as objects returned from factory functions. While I appreciate the utility of property-level factory functions, inasmuch as they don't enable anything that the existing factory functions don't already enable, IMHO this just seems more likely to introduce unexpected behavior, and I'd advocate for the least surprisingly implementation. /2-cents |
agree with @jmar777 A simple solution that solves 90% of usecases can be merged right now without risk, until we figure out if we can support more advanced API. Honestly it's just reducing boilerplate so I'm not even sure it's so interesting to support more advanced usecases as it seems not so easy and those usecases are already enabled by more verbose syntax. The initial simple PR I made was ready 1.5 year ago. Why not reducing the scope and merging something simple now? |
@slorber : I already said I'm pretty much okay with the implementation and scope of this PR, but I want to see more docs and tests added around this. If you'd like to see this merged, you're welcome to help out with those. |
@markerikson : sorry that it took me a while to get back to you on this. I've been quite busy these days. I will start working on this tomorrow... I'm hopping to address your comments by the end of this week. Thanks for your feedback! |
@josepot : any updates on this? |
ead6fde
to
6b82ee3
Compare
Hi and sorry @markerikson @markusjwetzel for leaving this PR a bit stale... I've been beyond busy these last months. So, I just did a rebase, added a new test and updated the docs to the best of my abilities. About the improvements that @markerikson said that were necessary before merging this PR. This is what I think:
IMO that should be done in a separate PR. I would be very happy to help with that, but first I would like to know what exactly is that we want to test and to what extend. That being said, I just added an extra test that testes the basic functionality of both shorthands.
I don't really understand what's missing... I mean, I made sure that everything that worked with the "normal"
I have no idea. But I imagine that since these changes are 100% backwards compatible they shouldn't break anything... I mean, yes, Flow and TS users won't be able to use the Ok, lets finish this already! |
fb8fcb3
to
16d6bca
Compare
16d6bca
to
0fbe4fe
Compare
Hi @markerikson! Just a friendly reminder that I'm waiting for your feedback here... Sorry if you find this comment annoying, it's just that I don't want this PR to become stale again. Thanks! |
Afraid I'm on travel at the moment, and don't have a lot of time free. I'll try to find time to look at it within the next few days. |
Hi @markerikson ! Just another friendly reminder that I'm waiting for your feedback. Sorry if you find these comments annoying... I'm aware that you must be busy and that this is probably a low-priority PR, but as I said before: I don't want this PR to become stale again. Thanks! |
It's on my radar, but I'm still on business travel and don't have much spare time. I'll try to take a look at it after I get back from this trip. |
Actually, the object, and thus the function, are created once.
From my read, it will not wrap the object because I don't even recall how I stumbled upon this PR, but I am not a fan. The shorthand for function mapDispatchToProps(dispatch) {
return { foo: () => dispatch(foo()) };
}
// to
function mapDispatchToProps(dispatch) {
return bindActionCreators({ foo }, dispatch);
}
// to
const mapDispatchToProps = { foo }; The analogy for props with selectors: function mapStateToProps(state, props) {
return { foo: getFoo(state, props) };
}
// to
function mapStateToProps(state, props) {
return bindSelectors({ foo: getFoo }, state, props);
}
// to
const mapStateToProps = { foo: getFoo }; I have to agree with the other people on this thread about the factories. They seem to complicate things and there is no analogy for mapDispatchToProps. I think its obvious what happens when function mapStateToProps(initialState, initialProps) {
// do things
return bindSelectors({ stuff });
} This feels easier to follow. And it also has the nice property that all of these are the effectively the same: const mapStateToProps1 = (state, props) => bindSelectors({ foo }, state, props);
const mapStateToProps2 = bindSelectors({ foo });
const mapStateToProps3 = { foo }; Which just feels nice IMHO. |
I have to say, I am not a fan of this PR. the number of possibilities for what can be passed for mapStateToProps and mapDispatchToProps is already a bit like reading C++ operator overloading. Better would be to provide different versions of connect that encapsulate the flexibility, or a fluent interface that makes the expected behavior explicit and errors in development if the passed argument varies from what is expected |
I do agree that I'll leave this PR open for a bit longer, but at the moment I'm leaning towards closing it. |
Hi @markerikson, can see feature this has been floating in and out of prs for last couple years. What is the status on this pr? It would a really beneficial feature, is there anything I can do to see this in? cc: @gaearon, @jimbolla , @markerikson , @slorber, @timdorr |
I think the last comment I'd made on here was that I primarily wanted someone to contribute tests and documentation before it would be considered for merging. However, at this point I'm feeling hesitant to accept it. As @cellog said, the internals of Oh hey, that was actually the last comment I'd made on here. Okay. At this point, I think someone needs to give me some good reasons why this is something we really need to merge in and add to the API, otherwise I'll close it in the near future (especially since we're focusing our attention on v6 right now). |
A quick note: in the interim, if you need this functionality, you could write a selector factory that provides it, and pass that to connect options |
I'll take the bullet. |
Like #723 but supporting factory selectors... Because I'm of the opinion that the object-shorthand syntax should also support factory selectors like @jimbolla suggested in the past. 😄
cc: @gaearon, @jimbolla , @markerikson , @slorber, @timdorr