-
Notifications
You must be signed in to change notification settings - Fork 47k
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
Context improvements #10334
Context improvements #10334
Conversation
Wow, cool find.
Could you clarify what case this is testing that wasn’t tested before? In other words, what is special about these new cases that was missing in old ones? I’m trying to understand in which cases the bug would occur but it doesn’t jump out to me. (I’m cool with adding more tests. Just trying to understand what we missed earlier.)
What difference is it? Can products (internally or externally) be relying on it? |
Thanks 😄
Yes. I rebuilt the react/react-dom bundles locally with this change and confirmed that it resolved the repro case @edvinerikson provided.
The specific test case added that failed previously is "updates descendants with multiple context-providing ancestors with new context values". The others I added to ensure we had extra coverage and that my changes didn't cause a regression.
It's reproducible using the public API in test "should update descendants with new context values if setState() is called in the middle of the tree". Specifically the last assertion that verifies the updated output is "count:1, name:not brian". This is true for the noop and fiber renderers but for the stack renderer count remains 0. It's hard to say whether products could be relying on this behavior, but my intuition says no. The previous behavior is unintuitive and (in my opinion) broken. |
@@ -277,6 +277,11 @@ module.exports = function<T, P, I, TI, PI, C, CX, PL>( | |||
markRef(current, workInProgress); | |||
|
|||
if (!shouldUpdate) { | |||
// Context providers should defer to sCU for rendering | |||
if (hasContext) { | |||
invalidateContextProvider(workInProgress, false); |
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.
There is also a sCU on functional components but since that's a new thing I guess we could ignore that.
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.
But as long as it's in the codebase, I think we should keep the implementations in sync.
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.
That means that we have to push/pop every functional component though, right? That seems like we're severely punishing perf of functional components for this edge case.
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 guess I misunderstood @bvaughn's original description of this change as a bugfix, or improvement to a more correct behavior. Subsequent comments suggest otherwise.
Now that I think about it, this only affects context providers, right? So I guess functional components aren't affected?
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 difference is whether functional components can terminate rendering. They can't with this PR if the context provider rerendered above them. Basically sCU on a functional component becomes useless in many cases.
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.
Actually I'm not sure what happens... They don't get priority and pending props, so maybe things don't rerender?
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.
This change shouldn't be relevant for functional components, since it's specific to context-providers. Functional components can only consume context, since there's no underlying instance. Right?
One thing to watch out for is whether we make some common case that previously bailed out slower. For example technically we could make context even more "correct" by always traversing deep on every update but this would kill perf. We need to see if the fix that doesn't match Stack behavior also doesn't make some cases pathological. |
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 think this makes sense to me.
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 think we should either remove shouldComponentUpdate
from functional components, or implement this change there as well. https://github.com/facebook/react/pull/10334/files#r130508835
Never mind, I don't think functional components are relevant here. #10334 (comment)
push(didPerformWorkStackCursor, true, workInProgress); | ||
if (didChange) { | ||
pop(contextStackCursor, workInProgress); | ||
// $FlowFixMe - We know that this is always an object when didChange is true. |
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.
Suggested refactor:
if (didChange) {
const mergedContext = processChildContext(workInProgress, previousContext, true);
instance.__reactInternalMemoizedMergedChildContext = mergedContext;
pop(didPerformWorkStackCursor, workInProgress);
pop(contextStackCursor, workInProgress);
push(contextStackCursor, mergedContext, workInProgress);
} else {
pop(didPerformWorkStackCursor, workInProgress);
}
One fewer branch and prevents Flow from complaining.
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 like this suggestion. Check out the most recent commit. It cool?
To clarify, the issue initially reported by @edvinerikson was a regression in 16. |
Lots of discussion on this issue but it seems like @sebmarkbage approves and @acdlite retracted his initial concern, so I'm going to move forward with this! Let's follow up if needed. |
That was a behavior regression. I think @gaearon is talking about a performance regression. I don't know if we have any tests to confirm whether this leads us to overrender more than necessary. |
I remember adding some when I was working on memoization logic. So we’re probably good. |
React maintains context using a stack structure (
ReactFiberStack
). Each time a context-providing component is encountered during the begin-work phase, it is pushed onto the stack. We push early to ensure stack size consistency/predictability. However, at the time we initially push, the context-providing instance may not yet exist. In this case we add a placeholder context object and a default value for whether we performed work at this level and assume that we will update both later.Previously we initialized the "did perform work at this level" value to false. However that approach can cause problems for context-providing components, making it possible for a component to block its own updates by failing this check in
ReactFiberClassComponent
. This was initially reported by @edvinerikson via Twitter.I believe a more correct approach to initializing the "did perform work at this level" bit for a context-providing component is for the component to default to the same value as its parent. We can later update the value using the
invalidateContextProvider
helper to match whatshouldComponentUpdate
returned.I have added some new test cases to the
ReactIncremental-test
to cover the previous regression case. These new tests may overlap somewhat with the existing ones but I thought it better to be slightly verbose in this case. (I'm open to feedback on this though!)PS While investigating this issue I also noticed a difference in behavior between stack and fiber DOM renderers. I believe in this case that fiber is behaving correctly and the noop renderer is consistent with fiber's behavior, so I didn't bother adding additional stack-specific tests.