-
Notifications
You must be signed in to change notification settings - Fork 46.8k
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
Added ReactFiberStack shared by ReactFiberContext and ReactFiberHostContext #8611
Conversation
6941810
to
badc65a
Compare
valueStack[index] = emptyObject; | ||
|
||
if (__DEV__) { | ||
fiberStack[index] = emptyObject; |
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.
Why emptyObject instead of null on both of these? We could use ES2015 .fill()
here if available.
Another thing we could do if we're worried about leakage is to store the max index of the previous run and then only clear the delta between the previous two runs. That way if we end up refilling the stack every time we rerender we don't have to fill it twice.
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.
My initial thought was to go with null, but I noticed that ReactFiberContext
returned emptyObject
in a few cases so I followed that pattern. I'll change it to null
. Probably best to have the emptyObject
fallback happening in ReactFiberContext
rather than ReactFiberStack
anyway.
|
||
import type { Fiber } from 'ReactFiber'; | ||
|
||
export type StackCursor = {| |
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.
We should make this generic type StackCursor<T> = {| current: T |}
.
You will have to make some unsound things when you pop here but then everywhere else is fine.
|
||
let index = -1; | ||
|
||
exports.createCursor = function() : StackCursor { |
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.
If you add an initial default value, Flow will let you make this generic.
function<T>(defaultValue : T) : StackCursor<T> {
When you can invoke it with createCursor((null : ?string))
to get a typed version of the stack cursor.
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.
Passing a default value feels a bit awkward since (at least for now) none of the cursors have meaningful defaults. But the generics benefit is nice! So sure! 😄
In ReactFiberScheduler we call unwindContext followed by unwindHostContext. The point of these is to reset the context of both to where the error boundary is. However, this will break with your change because the popping needs to be interleaved not sequential. I.e. you need to pop both (or all three I guess) until we reach the desired error boundary. I think we need to have an EDIT: Basically what unwindHostContext is already doing. Just break that out into a separate module and have it unwind the ReactFiberContext too. |
The functional component branch is a bit special since it has sCU in it. The class component branch actually already covers this in a bit convoluted way. I will next replicate this check in each other branch.
Regarding |
Another TODO item for me: In In order to avoid over-popping, I need to add another cursor to track the fibers we push for. And to do that, I think we'll need to address the other TODO to pass the host container Fiber down. I have a meeting shortly but I'll work on both of these afterward. |
For some reason I thought that unwindHostContext wasn't in the scheduler. Ignore the comment about separate module. |
I was thinking that maybe we should tag the host components with a different tag if they don't need to change context. That way we know when we complete that we don't need to pop it. Similarly, the next time we update and beginWork, we can pick the fast path of skipping checking for context changes. |
Where would this tagging be done? Seems inappropriate for If you'd like to chat about ideas here ping me on Messenger. 😄 |
The thing calling |
63c8cc5
to
720035b
Compare
|
||
function getRootHostContainer() : C { | ||
if (rootInstance == null) { | ||
if (rootInstanceStackCursor.current == null) { |
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.
Minor nit: could you read .current
once? I don't know if compiler is smart enough to know it hasn't changed.
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.
Absolutely.
4a39d39
to
2bea981
Compare
The issue I'm noticing is that adding static childContextTypes = {
foo: React.PropTypes.number
};
getChildContext() {
return {foo: 42};
} to some of the "broken" components in It's likely the same issue as #8604 but worth noting it doesn't only happen for |
There's a kind of obvious design flaw with this single-stack-with-shared-cursors approach that I managed to overlook until a half hour ago.
Irrespective of mismatched pushes/pops, this breaks a lot of things. I'm reevaluating the best way to proceed at the moment. I think Sebastian was a big fan of a single stack but I'm not currently able to visualize a way for it to work at the moment. |
The thing that is suppose to go on the stack is the previous value of the cursor. Not the value you push.
|
Ahhh, that makes sense. Thanks @sebmarkbage. |
|
||
index++; | ||
|
||
valueStack[index] = value; |
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.
index++;
valueStack[index] = cursor.current;
cursor.current = value;
The mental model is: var current = null;
function work(foo) {
// Begin Work
var prevOnTheStack = current;
current = getContext(foo);
try {
processChildren(foo);
// Complete Work
completeSomething();
} finally {
// Complete Work and Unwind Context
current = prevOnTheStack;
}
} |
As of my most recent push, all tests that were previously passing (in |
…ontext ReactFiberStack is the new underlying stack used by ReactFiberContext and ReactFiberHostContext. The goal is to simplify the 2 context managers and to add more errors/dev-warnings when we pop unexpected. This changeset currently causes a lot of tests to fail (as we are currently popping too many times and/or in the wrong order). I'm pushing this branch now to share with Sebastian as he is working on a related cleanup pass at beginWork.
CI is failing for some reason. Try running |
2c684f0
to
e7a591e
Compare
Yeah, I noticed that. Running locally I see a lot of garbage in the console like
...but then it ultimately finishes with
|
I am able to reproduce the error locally by manually running the gulp task // This works
export type StackCursor<T> = { current: T };
// This causes the error even though it's a valid Flow type
export type StackCursor<T> = {| current: T |}; For now I will relax that type so that Gulp works. I assume I should also be committing the updated |
CI gets further now but ultimately fails at a different stage- |
This was causing the 'react:extract-errors' Gulp taks to fail.
58a7d56
to
ea6530d
Compare
Had an informative chat with Ben about this. The failing Grunt task was caused by the fact that I've added new invariants/warnings in this PR and updated my local error codes JSON. This in turn caused our This further confused me for a while because Grunt failed for me locally on master as well. That turned out to be caused by a caching issue which was fixed with PR #8622. In general we have a test flakiness problem that will bite us when we next update the error codes (before the next release). That flakiness issue will need to be addressed in a subsequent PR. I don't mind taking that task on once these context PRs have been wrapped up. 😄 For now, CI passes again for this branch. Yay. |
const contextStack : Array<Object> = []; | ||
const didPerformWorkStack : Array<boolean> = []; | ||
let contextStackCursor : StackCursor<?Object> = createCursor((null: ?Object)); | ||
let didPerformWorkStackCursor : StackCursor<?boolean> = createCursor((null: ?boolean)); |
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.
If you set the default here to (false : boolean)
you can get rid of the nullable type.
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 thought about doing that, but then I'd lose the ability to differentiate between "empty" and default-state. Which isn't really important in this case but... slightly more compelling is that it would also mean the cursor would have asymmetric behavior- false
by default, and then null
after the stack gets reset (since we don't track and fall back to the default value). It seemed worth doing the cast to avoid that, but I don't feel strongly about it.
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.
It won't have asymmetric behavior if you fix the reset below. :)
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.
You are correct of course! 😁 That turnery was a holdover from when I was pushing/popping incorrectly. D'oh.
@@ -62,7 +66,7 @@ exports.getMaskedContext = function(workInProgress : Fiber) { | |||
}; | |||
|
|||
exports.hasContextChanged = function() : boolean { | |||
return index > -1 && didPerformWorkStack[index]; | |||
return Boolean(didPerformWorkStackCursor.current); |
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.
By avoiding the nullable type above, you don't need coerce this. You can just return didPerformWorkStackCursor.current
.
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.
Ok. :) I'll go ahead and make that change then. I don't feel strongly about it.
}; | ||
|
||
exports.reset = function<T>( | ||
cursor : StackCursor<T>, |
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.
Passing a cursor here seems like an odd API since it's invalid to only reset one cursor. You have to reset all at once. It also causes the unsound assignment below.
Currently this only works because index is already at -1
at the second call to reset so the loop is unnecessary.
I'd recommend just dropping the cursor
argument and reset them separately if you think it's necessary to reset them.
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.
Sure. It's a little weird either way I guess. I'll drop the cursor from reset
and just manually clear the current
pointer :)
|
||
exports.push = function<T>( | ||
cursor : StackCursor<T>, | ||
value : any, |
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 can be value : T
for a bit stronger guarantees that ensures that we don't push unexpected values.
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.
Good catch.
|
||
cursor.current = index > 0 | ||
? valueStack[index] | ||
: (null : any); |
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 is unsound since it doesn't reset to the defaultValue. However, I don't think this should ever be possible since the first value that goes into the value stack is the defaultValue. So you shouldn't be able to pop further than 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.
This should just be:
cursor.current = valueStack[index];
Otherwise you're never reading index 0.
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.
Good suggestion.
52f7d00
to
2f2a440
Compare
}; | ||
|
||
exports.resetContext = function() : void { | ||
index = -1; | ||
reset(); |
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.
We now have two calls to reset in a row because the only thing that calls this is:
So we could just call reset();
at the top of that function instead.
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.
Seems slightly weird to bypass the contexts and talk directly to the stack they use, no?
But sure. I can change 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.
Awesome! Let's go!
Thanks for the review Sebastian!! |
Overview
ReactFiberStack
is the new underlying stack construct used byReactFiberContext
andReactFiberHostContext
. The goal is to simplify the 2 context managers and to add earlier and better dev-warnings when we pop unexpectedly.This change set causes a lot of tests to fail as we are currently popping too many times and/or in the wrong order. I'm pushing this branch now to share with Sebastian as he is working on a related cleanup in
beginWork
.Work needing to be done for this branch
Fiber | null
type inReactFiberStack
by making sure that we always pass aFiber
down; currently this is not the case forpushHostContainer
andpopHostContainer
.