[Fiber] Push class context providers even if they crash#8627
[Fiber] Push class context providers even if they crash#8627gaearon merged 7 commits intofacebook:masterfrom
Conversation
Previously we used to push them only after the instance was available. This caused issues in cases an error is thrown during componentWillMount(). In that case we never got to pushing the provider in the begin phase, but in complete phase the provider check returned true since the instance existed by that point. As a result we got mismatching context pops. We solve the issue by making the context check independent of whether the instance actually exists. Instead we're checking the type itself. This lets us push class context early. However there's another problem: we might not know the context value. If the instance is not yet created, we can't call getChildContext on it. To fix this, we are introducing a way to replace current value on the stack, and a way to read the previous value. This also helps remove some branching and split the memoized from invalidated code paths.
Also rename another test to have a shorter name.
All uses of push() and pop() are guarded by it anyway. This makes it more similar to how we use host context. There is only one other place where isContextProvider() is used and that's legacy code needed for renderSubtree().
|
I found one additional bug there but I can't figure out how to fix it. When an error boundary itself is a context provider, It's not blocking for this PR though. |
| const unmaskedContext = getUnmaskedContext(); | ||
| const context = {}; | ||
| const hasOwnContext = isContextProvider(workInProgress); | ||
| // If it is a context provider then use the previous context instead. |
There was a problem hiding this comment.
nit: This comment makes sense to me, with the context I have, but might not be sufficient explanation for someone seeing this part of the code for the first time. Maybe we should elaborate about why we use the previous one. Maybe even just a pointer to see the comments in pushContextProvider?
| // Instance might be null, if the fiber errored during construction | ||
| fiber.stateNode && | ||
| typeof fiber.stateNode.getChildContext === 'function' | ||
| fiber.type.childContextTypes != null |
| exports.invalidateContextProvider = function(workInProgress : Fiber) : void { | ||
| const instance = workInProgress.stateNode; | ||
| if (instance == null) { | ||
| throw new Error('Expected to have an instance by this point.'); |
There was a problem hiding this comment.
Curious why you're using throw here instead of invariant? (How do we determine when to do one vs the other?)
There was a problem hiding this comment.
We should always use invariants but I didn't bother because we're going to do a separate pass to replace them all anyway. (It's in the umbrella.)
| if (index === 0) { | ||
| return null; | ||
| } | ||
| return valueStack[index - 1]; |
There was a problem hiding this comment.
Isn't previous actually the value at valueStack[index]?
Current is in cursor.current and previous is the top item in the stack (at index).
There was a problem hiding this comment.
PS I was already thinking about doing this, but I believe I'll submit a follow-up PR that adds some unit tests for ReactFiberStack itself.
There was a problem hiding this comment.
Ah, I think you're right. I wonder why tests didn't catch this.
There was a problem hiding this comment.
No problem. I'll add some tests for this specific type of thing. 👍
| } | ||
| return null; | ||
| } | ||
| if (index === 0) { |
There was a problem hiding this comment.
This check is unnecessary. We only care about < 0 which is already checked for above.
There was a problem hiding this comment.
Rather than "unnecessary" I should have said "bad" because it would prevent the default value from being returned as previous if one was set. (eg like the default for didPerformWorkStackCursor)
|
Addressed. |
|
Nice clean up Dan! |
|
My only concern is whether c57cf3e is bad from optimization point of view. I.e. maybe it prevents inlining or something. What do you think? |
|
Hmm maybe off by one wasn't a bug after all.. |
Unfortunately this is not something I'm that knowledgable about yet. I should defer to @sebmarkbage.
You mean |
|
Then there must be another off-by-one there that cancels it out since the test started failing after I fixed it (it goes into an infinite loop and shows wrong context in some tests). |
|
Hm. I'll finish up these |
|
Tests added for |
|
Hmm, I just realized |
|
This was a little confusing to me. While we're at it: why doesn't |
The stack doesn't track its allocated cursors so it has no way to reset current values. ( Once the stack has been reset, any cursor values should be ignored as they are no longer safe. |
|
I totally misunderstood how stack works. |
|
FYI in
|
|
My approach worked by accident. By addressing |
|
Ha! That's funny 😁 Yeah I had a few moments of off-by-one confusion like that when working on the initial stack branch. |
|
I totally should have spotted that. Sorry. This type of stack-management stuff is still a bit foreign to my thinking. |
|
Do I understand correctly that order of |
|
Yes. That is correct. Mirror order. |
|
It might make sense to track cursor objects together with fibers in DEV for better checks. |
bvaughn
left a comment
There was a problem hiding this comment.
We need an alternate approach to getPrevious. Sorry for not spotting this initially.
|
I don't think replace can work on anything but innermost level, right? We can't replace two things one after another. We can only pop twice and push twice. |
|
You can The part about this that doesn't work is getting the previous value. Stack currently doesn't know or care about what's on it or in what order, so the previous value is not guaranteed to be for the same cursor. We'd have to track previous on the cursor itself if we wanted to be able to return it. Alternately we could mirror pop-then-repush but this assumes that nothing else has pushed to the stack in the meanwhile. |
Actually even this would be problematic, because once we pop- we'd have the same problem, trying to figure what the previous was. I think the only safe thing to do here is to unwind the stack a little, then push them back on the stack once we've read them. |
The previous algorithm was flawed and worked by accident, as shown by the failing tests after an off-by-one was fixed. The implementation of getPrevious() was incorrect because the context stack currently has no notion of a previous value per cursor. Instead, we are caching the previous value directly in the ReactFiberContext in a local variable. Additionally, we are using push() and pop() instead of adding a new replace() method.
|
OK I think that fixes it. |
|
I'll land since it fixes bugs, happy to follow up on perf improvements if @sebmarkbage sees any. |
Previously we used to push them only after the instance was available. This caused issues in cases an error is thrown during
componentWillMount().In that case we never got to pushing the provider in the begin phase, but in complete phase the provider check returned
truesince the instance existed by that point. As a result we got mismatching context pops.We solve the issue by making the context check independent of whether the instance actually exists. Instead we're checking the type itself.
This lets us push class context early. However there's another problem: we might not know the context value. If the instance is not yet created, we can't call
getChildContexton it.To fix this, we are introducing a way to replace current value on the stack, and a way to read the previous value. This also helps remove some branching and split the memoized from invalidated code paths.
I also added a test from #8604 to verify this also solves the problem described in that PR.