-
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
Renderers can queue commit effects for initial mount #8646
Conversation
c443eed
to
4d1d0c4
Compare
@@ -371,6 +379,8 @@ module.exports = function<T, P, I, TI, C, CX>(config : HostConfig<T, P, I, TI, C | |||
// In the second pass we'll perform all life-cycles and ref callbacks. | |||
// Life-cycles happen as a separate pass so that all placements, updates, | |||
// and deletions in the entire tree have already been invoked. | |||
// This pass also manages focus since components must be in the DOM first, | |||
// And this does not happen untilt he entire tree is mounted. |
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.
Small typo!
untilt he
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.
Thanks!
I am not 100% this works but shouldn't you be able to use prepareForCommit/resetAfterCommit to do this? |
4d1d0c4
to
0c1595d
Compare
IMO we should not have the reconciler managing focusing state. It should be up to the renderer to do that if we can't do that with currently exposed methods from the reconciler we should introduce new API methods such as shouldFocusHostComponent(type, props) and focusHostComponent(instance). |
Hey @edvinerikson 😄 Yeah, I believe this could also be done in |
case 'button': | ||
case 'input': | ||
case 'select': | ||
case 'textarea': |
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 types are specific to DOM renderer so they shouldn’t be in the reconciler.
case 'input': | ||
case 'select': | ||
case 'textarea': | ||
if (newProps.autoFocus) { |
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.
As is this particular prop name.
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.
Right 👍 Sorry. This wasn't quite ready for review yet. I probably shouldn't have opened a PR yet. I just like to track my thoughts-in-progress via PR. Once it's ready I'll assign a reviewer.
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.
Yeah no worries. A convention I've been using is to tag PR with [WIP]
and maybe mention in the description what isn't ready yet.
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 call! I've updated the description to be more clear. 😁
I thought everyone else was on holiday today!
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 sort of am but having a lazy evening. 😛
|
||
// Auto-focus host components once they have been mounted. | ||
if (nextEffect.effectTag & Focus) { | ||
focusNode(nextEffect.stateNode); |
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.
Similarly this is a DOM-specific utility function and shouldn't get called in non-DOM environments.
Very helpful initial feedback by @edvinerikson. (Thanks!) After looking at So I've updated this PR to add 2 new
|
265f7e7
to
b5ed656
Compare
I believe this PR is ready for review now. 😁 I don't want to assign it to @gaearon since you're on holiday, but if you are bored and want to take a look I'd value your feedback. 😉 Else maybe @acdlite can pick it up tomorrow? I'm out for the evening to catch a movie with my parents. Will check back in a few hours! |
Another thought: What if we store these DOM nodes in the rootHostContainer when they are created / updated and use that in resetAfterCommit? Should be possible if we send the rootHostContainer to resetAfterCommit. Something like: rootHostContainer._focusedElement.focus() in resetAfterCommit. Or maybe this will cause memory leaks if we don't remove instances if work gets aborted. |
Don't have time to look at this closely until tomorrow, but it'd be best to solve this without a new effect type. @sebmarkbage's idea was to reuse the |
This can hold until tomorrow @acdlite. I'm done for the night in Virginia anyway. 😄 Maybe we can chat when you get in tomorrow. I don't initially see how we can re-use the I don't know what Sebastian may have had in mind about this. All I had to go off of was the TODO comment in |
@edvinerikson Yes, that's what I was worried about- cleaning up after cases where we get interrupted before completion, etc. Seemed safer this way. |
Even "focus" is a pretty specific concept that likely does not apply to all renderers. Having some way for components in the host tree to opt into a commit-time effect for initial mount makes sense to me. |
@bvaughn As Ben said, what we need is a "commit-time effect for initial mount" — like I don't think adding another method to the host config is necessary. We already have |
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.
- Remove
Focus
side-effect. TheUpdate
effect should be sufficient. - Instead of a new host config method, have
finalizeInitialChildren
return a boolean to indicate if a host component needs anUpdate
effect on initial mount.
Great point. Thanks for the suggestions Andrew and Ben. I'll implement them sometime this morning and get back. 😄 |
Couple of snags with this:
My thoughts are that we should add a new renderer method (eg We also need to wait to call this method until mounting has finished. I don't feel strongly about the best way to handle this, although I lean toward using I'd love to know your thoughts on the above. 😄 Update: I expect some discussion and changes will be required but I'm going to go ahead and push what I suggested above for discussion purposes (see 09229ef). |
d2dd297
to
1d23b4e
Compare
// aka when there is no current/alternate. | ||
if ( | ||
!current && | ||
nextEffect.effectTag & Update |
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.
Need an additional check that the tag is HostComponent
.
@@ -285,12 +289,23 @@ module.exports = function<T, P, I, TI, C, CX>(config : HostConfig<T, P, I, TI, C | |||
} | |||
} | |||
|
|||
function commitAllLifeCycles() { | |||
function commitAllLifeCyclesAndApplyInitialEffects() { |
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.
Let's keep the previous name. What you're calling an "initial effect" could also be used to describe componentDidMount
. I think calling this a "lifecycle" is sufficient.
const props = nextEffect.memoizedProps; | ||
const instance : I = nextEffect.stateNode; | ||
const rootContainerInstance = getRootHostContainer(); | ||
commitInitialEffects(instance, type, props, rootContainerInstance, nextEffect); |
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.
How about commitInitialMount
?
That's fine
Conceptually what we're adding is |
) : void { | ||
if (shouldAutoFocusHostComponent(type, newProps)) { | ||
(domElement : any).focus(); | ||
} |
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.
Could you explain why we need this extra method? Could we not unify this with commitUpdate
? We can determine if it's the initial mount by checking if oldProps
is null
. That's how we detect the initial mount elsewhere.
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 fact the code to handle this is already in master
: https://github.com/facebook/react/blob/master/src/renderers/dom/fiber/ReactDOMFiberComponent.js#L609 You deleted it in a previous commit but I think if you add it back, it will work now that the host component is scheduled to update on initial mount.
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 attempted to explain my reasoning in my previous comment. Some of it is subjective.
- That change would introduce asymmetry between
prepareUpdate
andcommitUpdate
.commitUpdate
would be called for newly-mounted components as well as pre-existing ones butprepareUpdate
would only be called for existing ones. (Technically we could callprepareUpdate
during initial mount as well but it seems unnecessary.) commitUpdate
would be called during different times, depending on whether the component was initially mounting (in which case it would be called duringcommitAllLifeCycles
) or whether it was an updated to a pre-existing component (in which case it would be called duringcommitWork
).- Adding checks to handle possible null values for
oldProps
for all renderers just to support a specific on-mount use case for one renderer felt (subjectively) wrong.
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.
Makes sense. Let's go ahead and call that method commitMount
then. Initial is redundant.
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 fact the code to handle this is already in
master
. You deleted it in a previous commit but I think if you add it back, it will work now that the host component is scheduled to update on initial mount.
The previous code was in setInitialProperties
which is only called by finalizeInitialChildren
which happens too early (during complete not commit).
If you are referring to moving it into updateDOMProperties
, then correct me if I'm wrong, but doing that would cause us to call domElement.focus()
more than just on initial mount- which is not the way autoFocus
should work.
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.
Yeah you're right, I misunderstood when setInitialProperties
was called.
Also since the original commits have essentially been reverted, would you mind either rebasing or opening up a new PR? |
There's a lot of useful discussion on this PR (so opening a new PR seems overkill). I'm leaving the commits in place to make it easier for anyone interested in doing an incremental review. I rebase and squash things at the end of a review, before merging. This is part of my normal workflow. |
Yeah I just mean before it's merged. |
Oh, gotcha. Yup! Squash before merge, for sure. |
// aka when there is no current/alternate. | ||
if ( | ||
!current && | ||
nextEffect.tag === HostComponent && |
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.
Hmm how about we move this to commitLifeCycles
? Avoids an extra check. https://github.com/facebook/react/blob/master/src/renderers/shared/fiber/ReactFiberCommitWork.js#L434
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 dig it.
5c95952
to
5f4e681
Compare
return false; | ||
}, | ||
|
||
focusHostComponent(instance : Instance) : void { |
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.
Oops
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.
Shoot. Thanks!
Delete that one leftover method and this is good to merge. We can continue bikeshedding on the name and come back to it later. |
The finalizeInitialChildren HostConfig method now utilizes a boolean return type. Renderers can return true to indicate that custom effects should be processed at commit-time once host components have been mounted. This type of work is marked using the existing Update flag. A new HostConfig method, commitMount, has been added as well for performing this type of work. This change set is in support of the autoFocus prop.
5f4e681
to
a101313
Compare
// aka when there is no current/alternate. | ||
if ( | ||
!current && | ||
finishedWork.effectTag & Update |
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.
Oh shoot, missed this. Don't need to check for Update effect because it was already checked by the time commitLifeCycles
is called.
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 that's the case, why does the ClassComponent
case check for it:
if (finishedWork.effectTag & Update) { |
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.
Because it also gets called if there's a Callback
effect, which host components don't have. I guess it's a bit weird either way, so I'm fine with keeping this as-is.
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.
Ah, right. That makes sense. Ok!
Huge thanks to @acdlite and others for the above review. Learned a lot! |
@@ -50,10 +50,11 @@ export type HostConfig<T, P, I, TI, C, CX> = { | |||
|
|||
createInstance(type : T, props : P, rootContainerInstance : C, hostContext : CX, internalInstanceHandle : OpaqueNode) : I, | |||
appendInitialChild(parentInstance : I, child : I | TI) : void, | |||
finalizeInitialChildren(parentInstance : I, type : T, props : P, rootContainerInstance : C) : void, | |||
finalizeInitialChildren(parentInstance : I, type : T, props : P, rootContainerInstance : C) : 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.
We could consider returning an constant enum number tag here. Like other tags. It's not less efficient as long as it gets inlined and the constants make it a bit more readable. It's also more extensible if we want to add more tags here.
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.
True! What other sorts of work do you think finalizeInitialChildren
might want to schedule? (I'd prefer to think of at least one other likely use case before adding a constant.)
case 'input': | ||
case 'select': | ||
case 'textarea': | ||
return !!(props : any).autoFocus; |
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 expand the Props
type to include this as optional instead of relying on any
here.
internalInstanceHandle : Object, | ||
) : void { | ||
if (shouldAutoFocusHostComponent(type, newProps)) { | ||
(domElement : any).focus(); |
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 can cast this to something more specific that has the focus method on it.
Such as ((domElement : any) : HTMLInputElement)
. That way we know what it should be if we properly typed this.
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 no longer using the focusNode
helper which means that this will start throwing in IE8 for hidden elements which is a breaking change. Do we really want 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.
Aye, the PR summary noted that:
IE8 compatibility note
Note that this PR also drops use of the
focusNode
helper method since it was in place for IE8 and IE8 support was officially dropped in January 2016.
Seemed wrong to keep an abstraction around solely for compat with a browser version we no longer support.
rootContainerInstance : Container, | ||
internalInstanceHandle : Object, | ||
) : void { | ||
if (shouldAutoFocusHostComponent(type, newProps)) { |
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.
Is this extra shouldAutoFocusHostComponent
really necessary? Won't this only be scheduled if the first one is true? At least after #8685.
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 check is still necessary. commitMount
will be called for host components with refs even if finalizeInitialChildren
returns false
. (The "preserves focus" test in ReactDOM-test
covers this case indirectly.)
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 #8685 added a special tag for refs so it shouldn't be necessary for it to call commitMount
in that case anymore, right?
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.
Hm, I think you're right, but there's this:
react/src/renderers/shared/fiber/ReactFiberCompleteWork.js
Lines 240 to 243 in 3bc5595
if (workInProgress.ref) { | |
// If there is a ref on a host node we need to schedule a callback | |
markUpdate(workInProgress); | |
} |
I think this is actually not intentional. I'll follow up on 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.
^ @acdlite
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.
Yeah that was an oversight on my part
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.
Follow-up on #8779
Auto-focus was previously managed by
ReactDOMFiberComponent.setInitialProperties
(which gets called during the complete phase). This did not work because host components are not yet present in the DOM until the entire tree has been mounted (which happens during the commit phase).This PR resolves that by making a few changes:
HostConfig
finalizeInitialChildren
method now returns a boolean. Renderers should return true to indicate that custom effects should be processed during commit (once host components have been mounted). egReactDOMFiber
uses this to queue auto-focus.HostConfig
method,commitMount
, has been added for performing this type of work. (See this discussion thread for why a new method was added instead of repurposingcommitUpdate
.) Existing renderers have been updated with noops.Tests
I was able to easily reproduce the broken focus behavior in a browser and verify the fix. However it was a bit awkward to catch in a unit test because PhantomJS does not require DOM elements to be mounted in order to track their focused state. I was able to add a test that caught the broken behavior and verifies the fix, but it's kind of a hack.
IE8 compatibility note
Note that this PR also drops use of the
focusNode
helper method since it was in place for IE8 and IE8 support was officially dropped in January 2016.