-
-
Notifications
You must be signed in to change notification settings - Fork 10.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
Make <Link> work with static containers #3429
Conversation
const isActive = isLinkActive(router, props) | ||
|
||
// The code is written this way to avoid wasted | ||
// setState() calls that get expensive in large trees. |
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.
The perf problem isn't setState()
– the slow call is router.isActive
.
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.
I understand, but we only call it once initially (which we would call anyway), and when the related props have changed (which again, we’d have to recompute anyway).
setState()
however can be slow on big trees. I’ve seen 3x difference on large trees in other project solved by avoiding setState
even despite further shouldComponentUpdate
bailout. React places setState
in a queue and marks subtrees as dirty so this might be relevant.
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.
(Previously, it would be called during render
, but now it doesn’t, so it seems like the number of useful times is equivalent. As far as I can see any other wins from not calling it are the bug, not the feature 😄 )
@@ -20,6 +20,21 @@ function isEmptyObject(object) { | |||
return true | |||
} | |||
|
|||
function isLinkActive(router, props) { |
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.
These conditions are moved from the render
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.
Doesn't this make the link always re-render twice on location updates then? Once with the old active state, once with the recalculated active state?
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.
In my testing this doesn’t seem to be the case. I think the listener fires before React has a chance to process setState
in the router so it looks like everything happens in a single batch. I might be wrong about the exact mechanics though—just saying I don’t see duplicate render
s in my example app.
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.
Note my caveat in #470 (comment) – if you hit this from clicking on a link, then stuff might well get batched together, but it won't happen if the transition is triggered from something other than a React event handler.
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.
Ahh. I get it now.
I do not think this is the correct approach here. It's difficult to allow multiple transition manager subscribers without edge cases (for example, right now, if you try to attach a second listener while the initial match is still running, that second listener will kick off a second match). I feel like we can do this in a generic way by introducing a concept of a "subscribable" context, and applying this notion to This "subscribe and re-render only if last rendered update index doesn't match new update index" can be used generically across libraries that make use of this sort of context pattern, and lets us use much looser coupling of the behavior here. |
IMO it would make sense to have this as the default behavior for |
They should go together. I'm actually a bit more concerned now about the case of how something like this works at all in the context of not using batched updates. I feel like potentially having dozens of non-batched |
Though I do think it would be semver-breaking to make this the default (and to change |
React Router can use |
Would this be any different from an event emitter? |
The only difference is on the subscriber side. You'd attach an update index to each update, then process the event only if the received update index doesn't match the most recently rendered update index. That way the subscription ends up being a no-op and everything goes through the standard re-render when intervening SCU clauses are not causing issues. |
See #470 (comment) and #470 (comment). This would wire up to |
Thanks for taking time! I’ll try to make it work. Who would know that |
In general for this sort of container pattern, the context updates when the container re-renders, right? Or at least that's how it'd work without intervening SCUs? I'm thinking an HoC like: function createContextProvider(name, contextType = React.PropTypes.any) {
return class ContextProvider extends React.Component {
static propTypes = {
children: React.PropTypes.node.isRequired,
};
static contextTypes = {
[name]: contextType,
};
static childContextTypes = {
[name]: contextType,
};
constructor(props, context) {
super(props, context);
this.eventIndex = 0;
this.listeners = [];
}
getChildContext() {
return {
[name]: {
...this.context[name],
subscribe: this.subscribe,
eventIndex: this.eventIndex,
},
};
}
componentWillReceiveProps() {
++this.eventIndex;
}
componentDidUpdate() {
this.listeners.forEach(listener => listener(this.eventIndex));
}
subscribe = (listener) => {
// No need to immediately call listener here.
this.listeners.push(listener);
return () => {
this.listeners = this.listeners.filter(item => item !== listener);
};
};
render() {
return this.props.children;
}
};
} Then in const RouterContextProvider = createContextProvider('router', routerShape);
// in render
return (
<RouterContextProvider>
{element}
</RouterContextProvider>
); Does this look generic enough? These two HoCs seem usable almost anywhere. |
I think the plan is to move to the |
Fixes #470, thereby unbreaking every project that uses React Redux with React Router.
This follows the approach described in #3340 (comment), but I’m happy for this to go into 3.0 rather than 2.x. I tested this on a few apps, and it works well in my testing. I’m happy to add as many additional unit tests as you like if this is something you are ready to consider to take in. I believe it’s going to take a huge pain point away from using these together, and I’m super excited about the possibility of this shipping.
Technical details as comments inline.