Skip to content

Commit 6396b66

Browse files
authored
Model Float on Hoistables semantics (#26106)
## Hoistables In the original implementation of Float, all hoisted elements were treated like Resources. They had deduplication semantics and hydrated based on a key. This made certain kinds of hoists very challenging such as sequences of meta tags for `og:image:...` metadata. The reason is each tag along is not dedupable based on only it's intrinsic properties. two identical tags may need to be included and hoisted together with preceding meta tags that describe a semantic object with a linear set of html nodes. It was clear that the concept of Browser Resources (stylesheets / scripts / preloads) did not extend universally to all hositable tags (title, meta, other links, etc...) Additionally while Resources benefit from deduping they suffer an inability to update because while we may have multiple rendered elements that refer to a single Resource it isn't unambiguous which element owns the props on the underlying resource. We could try merging props, but that is still really hard to reason about for authors. Instead we restrict Resource semantics to freezing the props at the time the Resource is first constructed and warn if you attempt to render the same Resource with different props via another rendered element or by updating an existing element for that Resource. This lack of updating restriction is however way more extreme than necessary for instances that get hoisted but otherwise do not dedupe; where there is a well defined DOM instance for each rendered element. We should be able to update props on these instances. Hoistable is a generalization of what Float tries to model for hoisting. Instead of assuming every hoistable element is a Resource we now have two distinct categories, hoistable elements and hoistable resources. As one might guess the former has semantics that match regular Host Components except the placement of the node is usually in the <head>. The latter continues to behave how the original implementation of HostResource behaved with the first iteration of Float ### Hoistable Element On the server hoistable elements render just like regular tags except the output is stored in special queues that can be emitted in the stream earlier than they otherwise would be if rendered in place. This also allow for instance the ability to render a hoistable before even rendering the <html> tag because the queues for hoistable elements won't flush until after we have flushed the preamble (`<DOCTYPE html><html><head>`). On the client, hoistable elements largely operate like HostComponents. The most notable difference is in the hydration strategy. If we are hydrating and encounter a hoistable element we will look for all tags in the document that could potentially be a match and we check whether the attributes match the props for this particular instance. We also do this in the commit phase rather than the render phase. The reason hydration can be done for HostComponents in render is the instance will be removed from the document if hydration fails so mutating it in render is safe. For hoistables the nodes are not in a hydration boundary (Root or SuspenseBoundary at time of writing) and thus if hydration fails and we may have an instance marked as bound to some Fiber when that Fiber never commits. Moving the hydration matching to commit ensures we will always succeed in pairing the hoisted DOM instance with a Fiber that has committed. ### Hoistable Resource On the server and client the semantics of Resources are largely the same they just don't apply to title, meta, and most link tags anymore. Resources hoist and dedupe via an `href` key and are ref counted. In a future update we will add a garbage collector so we can clean up Resources that no longer have any references ## `<style>` support In earlier implementations there was no support for <style> tags. This PR adds support for treating `<style href="..." precedence="...">...</style>` as a Resource analagous to `<link rel="stylesheet" href="..." precedence="..." />` It may seem odd at first to require an href to get Resource semantics for a style tag. The rationale is that these are for inlining of actual external stylesheets as an optimization and for URI like scoping of inline styles for css-in-js libraries. The href indicates that the key space for `<style>` and `<link rel="stylesheet" />` Resources is shared. and the precedence is there to allow for interleaving of both kinds of Style resources. This is an advanced feature that we do not expect most app developers to use directly but will be quite handy for various styling libraries and for folks who want to inline as much as possible once Fizz supports this feature. ## refactor notes * HostResource Fiber type is renamed HostHoistable to reflect the generalization of the concept * The Resource object representation is modified to reduce hidden class checks and to use less memory overall * The thing that distinguishes a resource from an element is whether the Fiber has a memoizedState. If it does, it will use resource semantics, otherwise element semantics * The time complexity of matching hositable elements for hydration should be improved
1 parent ef9f6e7 commit 6396b66

File tree

45 files changed

+7195
-8300
lines changed

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

45 files changed

+7195
-8300
lines changed

packages/react-devtools-shared/src/backend/renderer.js

Lines changed: 9 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -223,7 +223,7 @@ export function getInternalReactConstants(version: string): {
223223
HostComponent: 5,
224224
HostPortal: 4,
225225
HostRoot: 3,
226-
HostResource: 26, // In reality, 18.2+. But doesn't hurt to include it here
226+
HostHoistable: 26, // In reality, 18.2+. But doesn't hurt to include it here
227227
HostSingleton: 27, // Same as above
228228
HostText: 6,
229229
IncompleteClassComponent: 17,
@@ -257,7 +257,7 @@ export function getInternalReactConstants(version: string): {
257257
HostComponent: 5,
258258
HostPortal: 4,
259259
HostRoot: 3,
260-
HostResource: -1, // Doesn't exist yet
260+
HostHoistable: -1, // Doesn't exist yet
261261
HostSingleton: -1, // Doesn't exist yet
262262
HostText: 6,
263263
IncompleteClassComponent: 17,
@@ -290,7 +290,7 @@ export function getInternalReactConstants(version: string): {
290290
HostComponent: 5,
291291
HostPortal: 4,
292292
HostRoot: 3,
293-
HostResource: -1, // Doesn't exist yet
293+
HostHoistable: -1, // Doesn't exist yet
294294
HostSingleton: -1, // Doesn't exist yet
295295
HostText: 6,
296296
IncompleteClassComponent: 17,
@@ -323,7 +323,7 @@ export function getInternalReactConstants(version: string): {
323323
HostComponent: 7,
324324
HostPortal: 6,
325325
HostRoot: 5,
326-
HostResource: -1, // Doesn't exist yet
326+
HostHoistable: -1, // Doesn't exist yet
327327
HostSingleton: -1, // Doesn't exist yet
328328
HostText: 8,
329329
IncompleteClassComponent: -1, // Doesn't exist yet
@@ -356,7 +356,7 @@ export function getInternalReactConstants(version: string): {
356356
HostComponent: 5,
357357
HostPortal: 4,
358358
HostRoot: 3,
359-
HostResource: -1, // Doesn't exist yet
359+
HostHoistable: -1, // Doesn't exist yet
360360
HostSingleton: -1, // Doesn't exist yet
361361
HostText: 6,
362362
IncompleteClassComponent: -1, // Doesn't exist yet
@@ -397,7 +397,7 @@ export function getInternalReactConstants(version: string): {
397397
IndeterminateComponent,
398398
ForwardRef,
399399
HostRoot,
400-
HostResource,
400+
HostHoistable,
401401
HostSingleton,
402402
HostComponent,
403403
HostPortal,
@@ -465,7 +465,7 @@ export function getInternalReactConstants(version: string): {
465465
return null;
466466
case HostComponent:
467467
case HostSingleton:
468-
case HostResource:
468+
case HostHoistable:
469469
return type;
470470
case HostPortal:
471471
case HostText:
@@ -591,7 +591,7 @@ export function attach(
591591
Fragment,
592592
FunctionComponent,
593593
HostRoot,
594-
HostResource,
594+
HostHoistable,
595595
HostSingleton,
596596
HostPortal,
597597
HostComponent,
@@ -1032,7 +1032,7 @@ export function attach(
10321032
case HostRoot:
10331033
return ElementTypeRoot;
10341034
case HostComponent:
1035-
case HostResource:
1035+
case HostHoistable:
10361036
case HostSingleton:
10371037
return ElementTypeHostComponent;
10381038
case HostPortal:

packages/react-devtools-shared/src/backend/types.js

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -40,7 +40,7 @@ export type WorkTagMap = {
4040
HostComponent: WorkTag,
4141
HostPortal: WorkTag,
4242
HostRoot: WorkTag,
43-
HostResource: WorkTag,
43+
HostHoistable: WorkTag,
4444
HostSingleton: WorkTag,
4545
HostText: WorkTag,
4646
IncompleteClassComponent: WorkTag,

packages/react-devtools-shared/src/devtools/views/Components/InspectedElementStateTree.js

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -36,7 +36,7 @@ export default function InspectedElementStateTree({
3636
}: Props): React.Node {
3737
const {state, type} = inspectedElement;
3838

39-
// HostSingleton and HostResource may have state that we don't want to expose to users
39+
// HostSingleton and HostHoistable may have state that we don't want to expose to users
4040
const isHostComponent = type === ElementTypeHostComponent;
4141

4242
const entries = state != null ? Object.entries(state) : null;

packages/react-dom-bindings/src/client/ReactDOMComponentTree.js

Lines changed: 7 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@
77
* @flow
88
*/
99

10-
import type {FloatRoot, RootResources} from './ReactDOMFloatClient';
10+
import type {HoistableRoot, RootResources} from './ReactDOMFloatClient';
1111
import type {Fiber} from 'react-reconciler/src/ReactInternalTypes';
1212
import type {ReactScopeInstance} from 'shared/ReactTypes';
1313
import type {
@@ -24,7 +24,7 @@ import type {
2424

2525
import {
2626
HostComponent,
27-
HostResource,
27+
HostHoistable,
2828
HostSingleton,
2929
HostText,
3030
HostRoot,
@@ -178,7 +178,7 @@ export function getInstanceFromNode(node: Node): Fiber | null {
178178
tag === HostComponent ||
179179
tag === HostText ||
180180
tag === SuspenseComponent ||
181-
(enableFloat ? tag === HostResource : false) ||
181+
(enableFloat ? tag === HostHoistable : false) ||
182182
(enableHostSingletons ? tag === HostSingleton : false) ||
183183
tag === HostRoot
184184
) {
@@ -198,7 +198,7 @@ export function getNodeFromInstance(inst: Fiber): Instance | TextInstance {
198198
const tag = inst.tag;
199199
if (
200200
tag === HostComponent ||
201-
(enableFloat ? tag === HostResource : false) ||
201+
(enableFloat ? tag === HostHoistable : false) ||
202202
(enableHostSingletons ? tag === HostSingleton : false) ||
203203
tag === HostText
204204
) {
@@ -277,14 +277,12 @@ export function doesTargetHaveEventHandle(
277277
return eventHandles.has(eventHandle);
278278
}
279279

280-
export function getResourcesFromRoot(root: FloatRoot): RootResources {
280+
export function getResourcesFromRoot(root: HoistableRoot): RootResources {
281281
let resources = (root: any)[internalRootNodeResourcesKey];
282282
if (!resources) {
283283
resources = (root: any)[internalRootNodeResourcesKey] = {
284-
styles: new Map(),
285-
scripts: new Map(),
286-
head: new Map(),
287-
lastStructuredMeta: new Map(),
284+
hoistableStyles: new Map(),
285+
hoistableScripts: new Map(),
288286
};
289287
}
290288
return resources;

0 commit comments

Comments
 (0)