-
-
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
Completely rewrite connect() to offer advanced API, separate concerns, and (probably) resolve a lot of those pesky edge cases #407
Comments
The first concern would probably be that you're explicitly depending on Reselect. While Reselect is heavily used in the Redux ecosystem, I'd be hesitant to make it a direct dependency. (Of course, I'm also just an opinion, hardly the final arbiter.) |
What would be the cause for hesitation? |
The connect code is a bit hard to work with in tests. It would be really nice to be able to set So I think a bit of a refactor there would be nice but your own |
@btipling I'm all for opening it up as much as possible. But I'm not sure what you're not able to access that you expect to. The Connect component has to be inside the closure function because it depends on the functions parameters. It can't be outside because each call to connect creates a unique class. |
@reactjs/redux, @Phoenixmatrix, @aweary: would be particularly interested in other thoughts and opinions on this, especially from people who have actually been hands-on with I wrote up my initial thoughts in Reactiflux (chat transcript at https://gist.github.com/markerikson/561ce2d8830a34c35701ea77564c7073). Way TL;DR:
But:
|
Do you have any documentation on the new advanced API and/or a changelog? It's a pretty big change and kind of hard to parse everything at once, it will likely be easier to provide feedback once we can see an actual diff when you open your PR. I'm also a little hesitant about using Reselect by default. What if someone wanted to implement their own caching/memoization strategy, would they be able to do that? |
Here's your diff, but it's basically all new. I'm not sure this is actually more performant too. |
It definitely depends on whether this brings any real-world advantages. |
@aweary I've been working on adding comments to the code to explain as much as possible, but I can provide an executive overview:
Looking at a diff probably won't be super useful, since it's a total rewrite. It's probably better to look at the new files as a whole. @timdorr Can you elaborate on why you think this implementation would fall through more often? Unless I'm missing something, I expect render to NEVER execute unnecessarily. |
First performance test using https://github.com/broadsw0rd/react-redux-perf.
Running each version in their own Chrome window simultaneously on their own monitor, full screen
So for this particular test, my rewrite is getting a few FPS better, and is using slightly less RAM. But this test is fairly simple. It's a single connected component that passes props to many child components. I'm gonna tinker with the code and see if I can make it run 100s of connected components and see what kind of stats I get. I haven't been able to get https://github.com/jscriptcoder/stressing-redux to work. It just throws an error on startup. (With official react-redux... didn't even make it to trying my own) |
The examples in http://somebody32.github.io/high-performance-redux/ would also be very relevant here. Code for the examples is at https://github.com/somebody32/high-performance-redux/tree/gh-pages/assets, i think. |
Latest test... changed https://github.com/broadsw0rd/react-redux-perf. so that each item was a connected component (181 total connected). First results were that my rewrite was getting about 1-2 FPS less, so looking into it... excess
|
I made a small change in if (pure) { // removed && !this.doStatePropsDependOnOwnProps)
var haveStatePropsChanged = tryCatch(this.updateStatePropsIfNeeded, this); I'm not aware of the repercussions of this change, as I just start messing with the code here, well, 30 mins ago. I just started using React, Redux, etc. etc. this week, so I'm not quite up-to-speed on the performance problems here, but it didn't take long to run into them. Glad I'm not alone! Anyway, I'm happy to see that this thread is pretty active, so I wanted to take the opportunity to post my 2 cents and see what others thought before I spend the rest of the weekend catching up on this particular issue. Thanks, all! |
@jonkelling Would you be interested in helping me test my changes? At least by trying them in your app? |
@jimbolla I'll do what I can! I was able to get it functioning, but the first go at it saw decreased performance. I may not be doing it the way you intended, so I can try it again a bit differently if this seems off. connectAdvanced(() => (state, props, dispatch) => ({
active: state.on && state.activeId === props.id,
id: props.id,
children: props.children,
onMouseOver: (id, left, top, width, height) => dispatch(toggleHighlight(id, true, left, top, width, height)),
onMouseOut: id => dispatch(toggleHighlight(id, false))
}))(HighlightSomething); A couple of things:
Let me know if I can help any other way! |
@jonkelling Ah yes, if you're going to use connectAdvanced(({ dispatch }) => {
const onMouseOver = (id, left, top, width, height) => dispatch(toggleHighlight(id, true, left, top, width, height));
const onMouseOut = id => dispatch(toggleHighlight(id, false));
return (state, props) => ({
...props,
active: state.on && state.activeId === props.id,
onMouseOver,
onMouseOut,
})
})(HighlightSomething); Note that with the spread operator (...props) approach, all props will be considered for equality check, so if your component gets other things passed to it that it's not interested in, that would still trigger a rerender. That would be motivation to cherry pick your props so that only the ones you're interested in. connect(
(state, props) => ({
active: state.on && state.activeId === props.id,
}),
(dispatch) => ({
onMouseOver: (id, left, top, width, height) => dispatch(toggleHighlight(id, true, left, top, width, height)),
onMouseOut: id => dispatch(toggleHighlight(id, false)),
})
)(HighlightSomething); And if the id param of your handlers is the same id from props, you could do this: connect(
(state, props) => ({
active: state.on && state.activeId === props.id,
}),
(dispatch, props) => ({
onMouseOver: (left, top, width, height) => dispatch(toggleHighlight(props.id, true, left, top, width, height)),
onMouseOut: () => dispatch(toggleHighlight(props.id, false)),
})
)(HighlightSomething); I could go on about how to use reselect to build your selector as well, but I don't want to go to far off tangent right here. Feel free to submit an issue on my fork if you want to go in depth there. |
I removed the dependency on reselect and handrolled memoization for the relevant parts. I'm using this project to perf test the difference between current and this rewrite. With 331 connected components, i'm currently seeing 5-7 FPS with current, and 50-53 FPS with the rewrite. |
Well, that's certainly a very intriguing result. I very briefly skimmed the new code in the fork. There's definitely a lot more code overall, but on the flip side, a lot of that seems to be selectors. The individual bits of code also seem fairly well-written at first glance. Will need to spend time reading through that more carefully to see what's actually going on. (Could also use something of a diagram or dependency tree to tell me how all these different selectors fit together.) Again, I'm certainly not the guy who makes the final call, just the one who happens to be actively responding to the issue. That said, at this point I'd say the proposal seems to have some potential benefits, and warrants serious consideration and discussion. |
@markerikson I'm still refactoring for clarity, and need to add some comments, but I'm getting close to being ready to submit as a PR. Any feedback you have is greatly welcome. |
Haven't had a chance to really dig through the code yet. My main suggestion at this point would be to document this to ridiculous levels:
I know that's asking for a good chunk of work, but given that you're proposing to swap out an entire existing implementation that's been reasonably battle-tested with a brand-new codebase, the burden of proof is basically on you to fully justify the switch and make sure that it's sufficiently maintainable in the future. |
@markerikson I totally agree, and was already thinking all those things. It's just a matter of time. I'm hoping to have all that by some time this weekend, optimistically. The code is more-or-less complete, barring any renames to make things clearer. Next up is comments to clarify things at the local level, where needed. Then I'll work on the "conceptual" documentation. Do you think it makes sense to write that as an .md file to be included with the code? |
Could you possibly write up the documentation in a gist and just link it here? If it's beneficial to include it in the docs I'm sure it could be pulled in. |
If I wanted to make a diagram to show the relationship between the various functions, what would be a good tool to do so? |
Not sure on the diagram thing. This FPS measurement utility might be useful: https://www.reddit.com/r/reactjs/comments/4os0lu/looking_for_a_reliable_way_to_measure_framerate/ |
The one used by react-redux-perf is this one. It seems to do a good job. At this point I'm feeling pretty strong about significant perf gains, by avoiding unnecessary calls to setState/render. I'll elaborate in the eventual gist. |
I'm with @jimbolla on avoiding unnecessary calls to setState and render. Might I also suggest calling out the specific cases and adding unit tests for them. My own change to avoid extra calls to setState breaks one of existing tests in react-redux. When I get the time, I hope to understand the intention behind that test case and the original code so I can decide how I really want to/should handle it. I look forward to seeing what you come up with! |
@jonkelling : yeah, the snippet you commented out is to specifically handle the case where a parent component is rendering a connected child component, and passing some props in. For example, the parent might render something like: return <SomeConnectedChild itemID={someID} /> If the child's const mapState = (state) => state.someData[ownProps.itemID] In that case, every time the parent re-renders, the connected child would need to re-run And yes, MOAR UNIT TESTS!!!! :) |
Hmm. More use of context? Do we need to be concerned with any of the "context doesn't deal well with |
Also, does this make any changes to the approach for testing connected components? Does it still handle looking for |
@markerikson I never modify the context so no need to trigger an update. That should be a non issue. I followed the same "props.store" or "context.store" pattern like the original. |
Cool. Just tossing that out as a thought to be aware of. |
It's all good. The more scrutiny the better. Believe me, I'm more worried that I missed something than you are. haha. Current benchmarks using jimbolla/react-redux-perf, all 6 tabs opens at once, 331 connected components each: Frames per second:
Milliseconds to render a frame:
Same tests, this time with a Blocker component between the parent and children that always returns false for shouldComponentUpdate: Frames per second:
Milliseconds to render a frame:
Fascinatingly, current did worse with the blocker component, while the rewrite does even better. I think this is because in the rewrite, my current bottleneck is the shallowEqual compare on ownProps. With the Blocker component, the old props === new props and avoids the complicated part of shallowEqual. |
How do you all feel about using jsdoc for commenting code? Or is there something newer/better? |
Don't think there's any specific tool used for code docs at the moment in any of the ReactJS repos. I'd say go ahead for now, and worst case someone might change it later (but I'd think that would probably be okay as-is). Changing topics: I'd like to pre-emptively thank you for the time and effort you've put into this, and the attitude and approach you've taken. When I saw your first issue about the But: when the suggestion of making your implementation API-compatible with Now, as I said earlier, as the guy proposing we take on a big chunk of code you've written to replace an existing implementation, the burden of proof and work is indeed on you. But, you've definitely won me over on the basis of your approach and your code. Again, I don't have the final say on whether or not this would finally get merged in, but at this point I'm certainly in favor of a very serious discussion on this approach at a minimum, and think it has a good shot at being accepted overall. |
having gone through your implementation of connectAdvanced (and to some extend the connect api reimplementation) I have the following very disjointed thoughts and questions.
I'm torn because the risk of major reimplementations is real but the simplicity of the new implementation is valuable too. Part of me feels like if the new implementation is worth adopting we ought to deprecate the existing api in a major and move over to the new api in another major. In my mind most of the complexity in the new implementation is in trying to keep backwards compatibility which is a valuable target but if the new API turns out to be genuinely better (which we would of course need plenty of time in production use to adequately gauge this) then why hide it behind a veil of 'advanced-users-only' connotations. I don't make decisions, just been around this project for a bit. take my opinions for what little they are worth :) |
@jimbolla, I've got your latest code. Kudos on making it work with connect()! My own performance problems resolved 👍 I'll work on test cases for ya this weekend. Thanks for all of your work on this! |
@markerikson Thank you for your all your help. Your participation has helped me flesh out my ideas and force me to critically think about what I'm trying to do and how to do it. I understand the potential impact of such a large change, and therefore the need to make a strong justification for it. I'm working on that "justification" doc now, and even as I'm writing it, I'm tweaking my design to make the explanation less awkward. I'm still hoping to get it done by this weekend. @gnoff @jonkelling That's great. I'm encouraged by your positive feedback. Test cases are certainly welcome. |
I was about to write an RFC issue to discuss rewriting My approach was going to be a bit different than @jimbolla's. My inspiration came from wanting to write a lightweight version of import { connect as connect1 } from 'react-redux';
import { connect as connect2 } from 'react-redux-graphql';
import MyComponent from './components/MyComponent';
/* ...define props maps... */
const container = connect1(mapStateToProps, mapDispatchToProps)(MyComponent);
export default connect2(mapQueriesToProps, mapMutationsToProps)(container); Ideally it would be something like this: (inspired by redux store enhancers) export default connect(
mapStateToProps,
mapDispatchToProps,
mapQueriesToProps,
mapMutationsToProps,
connectEnhancer,
)(MyComponent); Or something similar (this doesn't have to be the final signature). Now, I could just use another higher order component and do: export default connectEnhancer(
mapStateToProps,
mapDispatchToProps,
mapQueriesToProps,
mapMutationsToProps,
)(connect)(MyComponent); You can basically do anything you want with higher order components but I thought that maybe baking that functionality into So should we leave it for other developers to create higher order components or should we bake in functionality to enhance |
At the moment I'd say that that would be going too far to be an acceptable set of changes. The current PR is being looked at because it's been made API-compatible with the existing version of I'd say HoCs are your best bet for now. |
I apologize if I wasn't clear but I wasn't suggesting that we add GraphQL specific functionality, but more so adding the ability to add an extra argument to It would also allow for People that want to opt-in to this new argument would write: connect(mapStateToProps, mapDispatchToProps, mergeProps, { connectEnhancer }); or leave the option out if they want to. This would allow connect(mapStateToProps, mapDispatchToProps, mergeProps, { connectEnhancer: advancedConnect }); The change in Correct me if I'm wrong, but the best option would be to minimally change the current code instead of an entire rewrite? Doing something like this would require minimal source code change but allow complete customization. EDIT: I'm going to do a quick implementation to show what I mean and will post here. |
Yeah, that's my point. We've already got several overloads in React Redux's API (like being able to pass an object full of functions as the Like I said, the only reason #416 is being considered right now is that despite the major internal changes, the external API is staying the same, and the benchmarks are indicating some pretty major potential performance improvements. Also note Dan's comments at #416 (comment) and reduxjs/redux#1813 (comment) regarding new/different APIs. |
Sounds good. And thanks for the prompt replies @markerikson! Here's the change migueloller@1c8d940 All tests are passing. Just wanted to get some comments on what people thought. The change now allows for enhancing connect (if you want to) in any way you want, allowing for what I mentioned above. If this ends up being ignored in the end that's ok. Just thought that this would be a good improvement. 😁 |
I forgot to mention that the arguments of |
@migueloller If #416 gets accepted, then you will probably be able to create your desired API by wrapping the new |
@jimbolla That would be an option but it's not much different from wrapping It doesn't look like there is interest to mirror store enhancer functionality for EDIT: It's probably worth mentioning that all the benefits that you get from connect(/* advancedConnect arguments */, { enhancer: advancedConnect })(SomeComponent) |
I'm gonna close this now that #416 exists. |
I rewrote connect() and extracted a connectAdvanced() It now uses reselect to handle merging/caching of the state/props/dispatch. It also extracts out a
connectAdvanced()
function thatconnect()
just delegates to.I'm not quite ready to ready to submit a PR (still tweaking), but I'd like to suggest that this eventually become the new implementation.
It passes tests and linting,
with the only breaking change to the existing API being related to the "advanced scenarios" for factories that require setting another options flag. (I haven't figured out how to detect the factory methods automatically... will give it another go, but it's been tricky given how radically different my approach is.)solvedThings I'm still need to do:
connect()
that I'd like to make reusable for someone who wants extend its functionality in userspaceThe text was updated successfully, but these errors were encountered: