-
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
Hydration of previously rendered server markup #9580
Conversation
ab81299
to
693e65c
Compare
// TODO: Possibly defer this until the commit phase where all the events | ||
// get attached. | ||
updateFiberProps(instance, props); | ||
setInitialProperties(instance, type, props, rootContainerInstance); |
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.
Will this be able to solve reviving input values (#7745) (whether now or after 16) or is solving that in ReactDOM core a non-goal?
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's a non-goal. We might start warning about server-rendering controlled components.
We could potentially always fire a change event after reviving with the new value.
However, maybe it's better to model it as uncontrolled first and then switch to controlled as you're reviving with the knowledge that you'll have to pick up the current value and do something useful with it.
The first solution is certainly more convenient, but is that a good user experience? It seems to me that most use cases for this would suffer in some way unless you designed both experiences.
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.
Although I guess doing both wouldn't hurt.
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 don’t want to conflate this issue too deeply with this tangent knowing it’s a non-goal so let me know if it makes sense to have a tracking issue for thoughts/best practices (whether that turns into implementation or documentation in the form section)
We could potentially always fire a change event after reviving with the new value.
It seems to me that most use cases for this would suffer in some way unless you designed both experiences.
I’m curious what use cases would suffer or hurt the user experience with this? I’ve experienced this scenario from browser prefilling forms from autocomplete setting and resetting the value during a refresh.
In both scenarios having it modeled as an onmount setState
would’ve been sufficient to hydrate the host environment values back to React component state.
f73058d
to
5d13239
Compare
* renders a controlled checkbox with client render on top of good server markup | ||
* renders a controlled select with client render on top of good server markup | ||
* should not blow away user-entered text on successful reconnect to an uncontrolled input | ||
* should not blow away user-entered text on successful reconnect to a controlled input | ||
* should not blow away user-entered text on successful reconnect to an uncontrolled checkbox | ||
* should not blow away user-entered text on successful reconnect to a controlled checkbox | ||
* should not blow away user-selected value on successful reconnect to an uncontrolled select | ||
* should not blow away user-selected value on successful reconnect to an controlled select |
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 tests fail because currently I'm going through the setInitialProperties
path which tries to set all the properties. This doesn't affect text inputs because there we use setAttribute('value', ...)
but ideally we should always set properties so that this gets properly reset to what we think it is. But we should be explicit about we think the value should be. Either the current value or the defaultValue. I'd like to follow up on that in the next separate PR.
I think this is ready to land. I'll fix the mentioned issues about controlled checkboxes/select later. After the server-side lands I might change the heuristic that determines what to clear in head/body as well as start skipping comments from being considered for hydration. Since I can then change the output. Additionally, there are a few async related issues that I'll leave for later:
|
Rebased on top #9710 |
@@ -433,6 +433,12 @@ function createFiberFromElementType( | |||
|
|||
exports.createFiberFromElementType = createFiberFromElementType; | |||
|
|||
exports.createFiberFromHostInstanceForDeletion = function(): Fiber { | |||
const fiber = createFiber(HostComponent, null, NoContext); | |||
fiber.type = 'DELETED'; |
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.
Any reason we don't pass the node to this function? Need to assign it to stateNode
anyway.
Otherwise I don't understand the value of this function versus calling createFiber
directly.
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.
createFiber
is not exposed at all. Makes it easier to add or change the default inputs here.
stateNode
is not passed when other HostComponent
Fibers are created so I figured we'd be consistent there.
The warnings need to check a valid container but that should happen in unstable_renderSubtreeIntoContainer too so might as well move it in.
Hydrates a server-rendered DOM tree by traversing it and connecting up the nodes if they match. Attributes are not diffed. They're assumed to be accurate on matching nodes.
Because the current server renderer includes the meta data, it remains in a revived tree.
During initial mount, we should not track Placement
TODO make these warnings instead.
When we don't match the first hydration node, we'll do an insertion. Currently we keep the next hydratable sibling so that we know where to pick up once we're done with the insertion. Unfortunately this makes the nodes inside the insertion think that there's a node to hydrate. I used to check for the direct parent but that doesn't work on nested host components. We need to instead keep track of that we're in an hydration context but we're not currently hydrating. Once we pop passed the inserted node can we resume hydrating.
isMounted checks whether a component is inside a Placement. During hydration we ideally don't do any Placements if hydration matches the tree. To work around this I use the Placement flag on the root, which isn't used for anything else. But only temporarily while we're hydrating. Then reset it before committing.
'ReactDOMServer.renderToString() for server rendering.', | ||
); | ||
if (ReactDOMFeatureFlags.useFiber) { | ||
ReactDOM.render(<Component text="Hello world" />, testDocument); |
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 intentional? Do we need to rename the test? It says "should throw".
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'm not sure those cross-browser quirks are still in modern browsers. I didn't find any issues (that aren't also issues for SSR).
I'm not sure how restrictive we want to be?
|
||
ReactDOM.render(<Component />, testDocument); | ||
|
||
expect(testDocument.body.innerHTML).toBe('Hello world'); | ||
|
||
// Reactive update | ||
expect(function() { | ||
if (ReactDOMFeatureFlags.useFiber) { | ||
// This works but is probably a bad idea. |
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.
Don't suppose you remember what you meant by this comment, @sebmarkbage?
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.
See UNMOUNT_INVARIANT_MESSAGE at the top of the file. Browsers don't like when you unmount the html
host node, or at least some didn't when we added that invariant to Stack.
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.
hey Sophie 👋
Thanks! I misread Seb's comment as "this works but isn't a good way to test" rather than "this works but we don't encourage it in production"
I follow now
This strategy keeps track of any available DOM nodes ("host instances") that we can reuse. They're stored in
ReactFiberHydrationContext
as we're reconciling and then picked off if they match. Any remaining non-matching nodes are deleted, others are inserted.This is using the normal reconciliation model so it should be possible to run in async mode so that reviving doesn't block the main thread. Currently, there are some properties that are set eagerly and events gets wired up early so that will break in async mode. I'll leave that as a todo.
This detects whether to revive or clear the container based on whether the child has a
data-reactid
attribute. We might want to change this API to be explicit.