From 6396b664118442f3c2eae7bf13732fcb27bda98f Mon Sep 17 00:00:00 2001
From: Josh Story
Date: Thu, 9 Feb 2023 22:59:29 -0800
Subject: [PATCH] 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 .
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 tag because the queues for hoistable elements won't
flush until after we have flushed the preamble (``).
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
## `` as a Resource analagous to ``
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 `');
-export function writeInitialResources(
+function flushResourceInPreamble(this: Destination, resource: T) {
+ if ((resource.state & (Flushed | Blocked)) === NoState) {
+ const chunks = resource.chunks;
+ for (let i = 0; i < chunks.length; i++) {
+ writeChunk(this, chunks[i]);
+ }
+ resource.state |= FlushedInPreamble;
+ }
+}
+
+function flushResourceLate(this: Destination, resource: T) {
+ if ((resource.state & Flushed) === NoState) {
+ const chunks = resource.chunks;
+ for (let i = 0; i < chunks.length; i++) {
+ writeChunk(this, chunks[i]);
+ }
+ resource.state |= FlushedLate;
+ }
+}
+
+let didFlush = false;
+
+function flushUnblockedStyle(
+ this: Destination,
+ resource: StyleResource,
+ key: mixed,
+ set: Set,
+) {
+ const chunks = resource.chunks;
+ if (resource.state & Flushed) {
+ // In theory this should never happen because we clear from the
+ // Set on flush but to ensure correct semantics we don't emit
+ // anything if we are in this state.
+ set.delete(resource);
+ } else if (resource.state & Blocked) {
+ // We can't flush but we can preload. We will do this in a second pass
+ } else {
+ didFlush = true;
+ // We can emit this style or stylesheet as is.
+
+ if (resource.type === 'stylesheet') {
+ // We still need to encode stylesheet chunks
+ // because unlike most Hoistables and Resources we do not eagerly encode
+ // them during render. This is because if we flush late we have to send a
+ // different encoding and we don't want to encode multiple times
+ pushLinkImpl(chunks, resource.props);
+ }
+ for (let i = 0; i < chunks.length; i++) {
+ writeChunk(this, chunks[i]);
+ }
+ resource.state |= FlushedInPreamble;
+ set.delete(resource);
+ }
+}
+
+function flushUnblockedStyles(
+ this: Destination,
+ set: Set,
+ precedence: string,
+) {
+ didFlush = false;
+ set.forEach(flushUnblockedStyle, this);
+ if (!didFlush) {
+ // if we did not flush anything for this precedence slot we emit
+ // an empty tag to ensure the
+ // precedence remains in the correct order
+ writeChunk(this, precedencePlaceholderStart);
+ writeChunk(this, stringToChunk(escapeTextForBrowser(precedence)));
+ writeChunk(this, precedencePlaceholderEnd);
+ }
+}
+
+function preloadBlockedStyle(this: Destination, resource: StyleResource) {
+ // The only Resources that should remain are Blocked resources
+ if (__DEV__) {
+ if ((resource.state & Blocked) === NoState) {
+ console.error(
+ 'React encountered a Stylesheet Resource that was not Blocked when it was expected to be. This is a bug in React.',
+ );
+ } else if (resource.state & PreloadFlushed) {
+ console.error(
+ 'React encountered a Stylesheet Resource that already flushed a Preload when it was not expected to. This is a bug in React.',
+ );
+ }
+ }
+ if (resource.type === 'style') {
+ //
,
).toErrorDev([
- 'Cannot render
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ ,
+ ).pipe(writable);
+ });
+
+ expect(getMeaningfulChildren(document)).toEqual(
+
+
+
+
+
+
+
+
+ ,
+ );
+
+ await act(() => {
+ resolveText('block');
+ });
+
+ expect(getMeaningfulChildren(document)).toEqual(
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ ,
+ );
+
+ await act(() => {
+ resolveText('block2');
+ });
+
+ expect(getMeaningfulChildren(document)).toEqual(
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ ,
+ );
+
+ await act(() => {
+ resolveText('block again');
+ });
+
+ expect(getMeaningfulChildren(document)).toEqual(
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ ,
+ );
+
+ ReactDOMClient.hydrateRoot(
+ document,
+
+
+
+
+
+
+
+
+
+ ,
+ );
+ expect(Scheduler).toFlushWithoutYielding();
+
+ expect(getMeaningfulChildren(document)).toEqual(
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ ,
+ );
+ });
+
+ // @gate enableFloat
+ it('client renders a boundary if a style Resource dependency fails to load', async () => {
+ function App() {
+ return (
+
+
+
+
+
+
+
+ Hello
+
+
+
+
+ );
+ }
+ await actIntoEmptyDocument(() => {
+ const {pipe} = renderToPipeableStream();
+ pipe(writable);
+ });
+
+ await act(() => {
+ resolveText('unblock');
+ });
+
+ expect(getMeaningfulChildren(document)).toEqual(
+
+
+
+
+
+
+ loading...
+
+
+
+ ,
+ );
+
+ await act(() => {
+ const barLink = document.querySelector(
+ 'link[rel="stylesheet"][href="bar"]',
+ );
+ const event = document.createEvent('Events');
+ event.initEvent('error', true, true);
+ barLink.dispatchEvent(event);
+ });
+
+ const boundaryTemplateInstance = document.getElementById('B:0');
+ const suspenseInstance = boundaryTemplateInstance.previousSibling;
+
+ expect(suspenseInstance.data).toEqual('$!');
+ expect(boundaryTemplateInstance.dataset.dgst).toBe(
+ 'Resource failed to load',
+ );
+
+ expect(getMeaningfulChildren(document)).toEqual(
+
+
+
+
+
+
+ loading...
+
+
+
+ ,
+ );
+
+ const errors = [];
+ ReactDOMClient.hydrateRoot(document, , {
+ onRecoverableError(err, errInfo) {
+ errors.push(err.message);
+ errors.push(err.digest);
+ },
+ });
+ expect(Scheduler).toFlushWithoutYielding();
+ expect(getMeaningfulChildren(document)).toEqual(
+
+
+
+
+
+
+
+
+ Hello
+
+ ,
+ );
+ expect(errors).toEqual([
+ 'The server could not finish this Suspense boundary, likely due to an error during server rendering. Switched to client rendering.',
+ 'Resource failed to load',
+ ]);
+ });
+
+ // @gate enableFloat
+ it('treats stylesheet links with a precedence as a resource', async () => {
+ await actIntoEmptyDocument(() => {
+ const {pipe} = renderToPipeableStream(
+
+
+
+
+ Hello
+
+ ,
+ );
+ pipe(writable);
+ });
+ expect(getMeaningfulChildren(document)).toEqual(
+
+
+
+
+ Hello
+ ,
+ );
+
+ ReactDOMClient.hydrateRoot(
+ document,
+
+
+ Hello
+ ,
+ );
+ expect(Scheduler).toFlushWithoutYielding();
+ expect(getMeaningfulChildren(document)).toEqual(
+
+
+
+
+ Hello
+ ,
+ );
+ });
+
+ // @gate enableFloat
+ it('inserts text separators following text when followed by an element that is converted to a resource and thus removed from the html inline', async () => {
+ // If you render many of these as siblings the values get emitted as a single text with no separator sometimes
+ // because the link gets elided as a resource
+ function AsyncTextWithResource({text, href, precedence}) {
+ const value = readText(text);
+ return (
+ <>
+ {value}
+
+ >
+ );
+ }
+
+ await actIntoEmptyDocument(() => {
+ const {pipe} = renderToPipeableStream(
+
+
+
+
+
+
+
+ ,
+ );
+ pipe(writable);
+ resolveText('foo');
+ resolveText('bar');
+ resolveText('baz');
+ });
+
+ expect(getMeaningfulChildren(document)).toEqual(
+
+
+
+
+
+
+
+ {'foo'}
+ {'bar'}
+ {'baz'}
+
+ ,
+ );
+ });
+
+ // @gate enableFloat
+ it('hoists late stylesheets the correct precedence', async () => {
+ function PresetPrecedence() {
+ ReactDOM.preinit('preset', {as: 'style', precedence: 'preset'});
+ }
+ await actIntoEmptyDocument(() => {
+ const {pipe} = renderToPipeableStream(
+
+
+
+
+ ,
+ );
+
+ // This will enqueue a stylesheet resource in a deep blocked boundary (loading baz...).
+ await act(() => {
+ resolveText('baz');
+ });
+ expect(getMeaningfulChildren(document)).toEqual(
+
+
+
loading...
+
+ ,
+ );
+
+ // This will enqueue a stylesheet resource in the intermediate blocked boundary (loading bar...).
+ await act(() => {
+ resolveText('bar');
+ });
+ expect(getMeaningfulChildren(document)).toEqual(
+
+
+
loading...
+
+ ,
+ );
+
+ // This will complete a segment in the top level boundary that is still blocked on another segment.
+ // It will flush the completed segment however the inner boundaries should not emit their style dependencies
+ // because they are not going to be revealed yet. instead their dependencies are hoisted to the blocked
+ // boundary (top level).
+ await act(() => {
+ resolveText('foo');
+ });
+ expect(getMeaningfulChildren(document)).toEqual(
+
+
+
loading...
+
+
+
+
+ ,
+ );
+
+ // This resolves the last blocked segment on the top level boundary so we see all dependencies of the
+ // nested boundaries emitted at this level
+ await act(() => {
+ resolveText('qux');
+ });
+ expect(getMeaningfulChildren(document)).toEqual(
+
+
+
+
+
+
+
+
+
,
);
+ }).toErrorDev([
+ 'React does not recognize the `nonStandardAttr` prop on a DOM element.' +
+ ' If you intentionally want it to appear in the DOM as a custom attribute,' +
+ ' spell it as lowercase `nonstandardattr` instead. If you accidentally passed it from a' +
+ ' parent component, remove it from the DOM element.',
+ 'Invalid values for props `shouldnotincludefunctions`, `norsymbols` on tag. Either remove them from' +
+ ' the element, or pass a string or number value to keep them in the DOM. For' +
+ ' details, see https://reactjs.org/link/attribute-behavior',
+ ]);
+
+ // Now we flush the stylesheet with the boundary
+ await act(() => {
+ resolveText('unblock');
});
- // @gate enableFloat
- it('inserts a preload resource when called in a layout effect', async () => {
- function App() {
- React.useLayoutEffect(() => {
- ReactDOM.preload('foo', {as: 'style'});
- }, []);
- return 'foobar';
- }
- const root = ReactDOMClient.createRoot(container);
- root.render();
- expect(Scheduler).toFlushWithoutYielding();
+ expect(getMeaningfulChildren(document)).toEqual(
+
+
+
+
+
+
+
+
+
+
+
+
+ ,
+ );
- // @gate enableFloat
- it('inserts a preload resource when called in module scope if a root has already been created', async () => {
- // The requirement that a root be created has to do with bootstrapping the dispatcher.
- // We are intentionally avoiding setting it to the default via import due to cycles and
- // we are trying to avoid doing a mutable initailation in module scope.
- ReactDOM.preload('foo', {as: 'style'});
- ReactDOMClient.createRoot(container);
- ReactDOM.preload('bar', {as: 'style'});
- // We need to use global.document because preload falls back
- // to the window.document global when no other documents have been used
- // The way the JSDOM runtim is created for these tests the local document
- // global does not point to the global.document
- expect(getMeaningfulChildren(global.document)).toEqual(
+ await act(() => {
+ resolveText('A');
+ });
+ await act(() => {
+ document.querySelectorAll('link[rel="stylesheet"]').forEach(l => {
+ const event = document.createEvent('Events');
+ event.initEvent('load', true, true);
+ l.dispatchEvent(event);
+ });
+ });
+ expect(getMeaningfulChildren(document)).toEqual(
+
+
+
+
+
+
+
,
);
});
// @gate enableFloat
- it('inserts a preload resource when called in module scope', async () => {
- // The requirement that a root be created has to do with bootstrapping the dispatcher.
- // We are intentionally avoiding setting it to the default via import due to cycles and
- // we are trying to avoid doing a mutable initailation in module scope.
- ReactDOM.preinit('foo', {as: 'style'});
- ReactDOMClient.hydrateRoot(container, null);
- ReactDOM.preinit('bar', {as: 'style'});
- // We need to use global.document because preload falls back
- // to the window.document global when no other documents have been used
- // The way the JSDOM runtim is created for these tests the local document
- // global does not point to the global.document
- expect(getMeaningfulChildren(global.document)).toEqual(
-
-
-
-
-
+
+
+ ,
+ );
+ });
+ }).toErrorDev([
+ 'Warning: ReactDOM.preload(): The options provided conflict with another call to `ReactDOM.preload("foo", { as: "font", ...})`. React will always use the options it first encounters when preloading a resource for a given `href` and `as` type, and any later options will be ignored if different. Try updating all calls to `ReactDOM.preload()` with the same `href` and `as` type to use the same options, or eliminate one of the calls.\n "integrity" option value: "some hash", missing from original options\n "crossOrigin" option value: "anonymous", original option value: "use-credentials"',
+ 'Warning: ReactDOM.preload(): The options provided conflict with another call to `ReactDOM.preload("foo", { as: "font", ...})`. React will always use the options it first encounters when preloading a resource for a given `href` and `as` type, and any later options will be ignored if different. Try updating all calls to `ReactDOM.preload()` with the same `href` and `as` type to use the same options, or eliminate one of the calls.\n "crossOrigin" missing from options, original option value: "use-credentials"',
+ ]);
+ });
+
+ // @gate enableFloat
+ it('warns if you pass incompatible options to two `ReactDOM.preload(...)` when an implicit preload already exists with the same href', async () => {
+ function Component() {
+ ReactDOM.preload('foo', {
+ as: 'style',
+ crossOrigin: 'use-credentials',
+ });
+ }
+
+ await expect(async () => {
+ await actIntoEmptyDocument(() => {
+ renderToPipeableStream(
+
+
+
+
+
+ ,
+ );
+ });
+ }).toErrorDev([
+ 'ReactDOM.preload(): For `href` "foo", The options provided conflict with props on a matching element. When the preload options disagree with the underlying resource it usually means the browser will not be able to use the preload when the resource is fetched, negating any benefit the preload would provide. React will preload the resource using props derived from the resource instead and ignore the options provided to the `ReactDOM.preload()` call. In general, preloading is useful when you expect to render a resource soon but have not yet done so. In this case since the underlying resource was already rendered the preload call may be extraneous. Try removing the call, otherwise try adjusting both the props on the and the options passed to `ReactDOM.preload()` to agree.\n "integrity" missing from options, underlying prop value: "some hash"\n "media" missing from options, underlying prop value: "print"\n "crossOrigin" option value: "use-credentials", missing from underlying props',
+ ]);
});
});
- describe('ReactDOM.preinit as script', () => {
+ describe('ReactDOM.preinit(href, { as: ... })', () => {
// @gate enableFloat
- it('can preinit a script', async () => {
- function App({srcs}) {
- srcs.forEach(src => ReactDOM.preinit(src, {as: 'script'}));
+ it('creates a stylesheet resource when ReactDOM.preinit(..., {as: "style" }) is called', async () => {
+ function App() {
+ ReactDOM.preinit('foo', {as: 'style'});
+ return (
+
+
+
+
+
+
+
+
+
+ );
+ }
+
+ function Component() {
+ ReactDOM.preinit('bar', {as: 'style'});
+ return
hello
;
+ }
+
+ await actIntoEmptyDocument(() => {
+ const {pipe} = renderToPipeableStream();
+ pipe(writable);
+ });
+ expect(getMeaningfulChildren(document)).toEqual(
+
+
+
+
+ loading...
+ ,
+ );
+
+ await act(() => {
+ resolveText('bar');
+ });
+ // The reason we do not see the "bar" stylesheet here is that ReactDOM.preinit is not about
+ // encoding a resource dependency but is a hint that a resource will be used in the near future.
+ // If we call preinit on the server after the shell has flushed the best we can do is emit a preload
+ // because any flushing suspense boundaries are not actually dependent on that resource and we don't
+ // want to delay reveal based on when that resource loads.
+ expect(getMeaningfulChildren(document)).toEqual(
+
+
+
+
+
+
,
- ]);
- });
- });
-
- describe('head resources', () => {
- // @gate enableFloat
- it('supports preconnects, prefetc-dns, and arbitrary other link types', async () => {
- await actIntoEmptyDocument(() => {
- const {pipe} = renderToPipeableStream(
+ it('creates a script resource when ReactDOM.preinit(..., {as: "script" }) is called', async () => {
+ function App() {
+ ReactDOM.preinit('foo', {as: 'script'});
+ return (
-
-
hello world
-
-
-
-
-
-
-
-
-
-
-
-
- ,
- );
- pipe(writable);
- });
- // "preconnect" and "dns-prefetch" get hoisted to the front.
- // All other generic links (not styles, or typed preloads)
- // get emitted after styles and other higher priority Resources
- // Sizes and Media are part of generic link keys
- expect(getMeaningfulChildren(document)).toEqual(
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
- ,
- );
- });
-
- // @gate enableFloat
- it('can rendering title tags anywhere in the tree', async () => {
- await actIntoEmptyDocument(() => {
- const {pipe} = renderToPipeableStream(
- <>
- before
- <>
-
-
- in head
-
-
-
- during
- hello world
-
-
-
- >
- after
- >,
- );
- pipe(writable);
- });
-
- expect(getMeaningfulChildren(document)).toEqual(
-
-
- before
- in head
- during
- after
-
-
-
hello world
-
- ,
- );
-
- ReactDOMClient.hydrateRoot(
- document,
- <>
- before
- <>
-
-
- in head
-
-
-
- during
- hello world
-
-
-
- >
- after
- >,
- );
- expect(Scheduler).toFlushWithoutYielding();
- expect(getMeaningfulChildren(document)).toEqual(
-
-
- before
- in head
- during
- after
-
-
-
hello world
-
- ,
- );
- });
-
- // @gate enableFloat
- it('prepends new titles on the client so newer ones override older ones, including orphaned server rendered titles', async () => {
- await actIntoEmptyDocument(() => {
- const {pipe} = renderToPipeableStream(
-
-
- server
-
-
-
-
- ,
- );
-
- ReactDOMClient.hydrateRoot(
- document,
-
- html
-
- head
-
-
- body
-
hello world
-
- ,
- );
- expect(Scheduler).toFlushWithoutYielding();
- expect(getMeaningfulChildren(document)).toEqual(
-
-
- body
- head
- html
- server
-
-
-
hello world
-
- ,
- );
- });
-
- // @gate enableFloat
- it('keys titles on text children and only removes them when no more instances refer to that title', async () => {
- const root = ReactDOMClient.createRoot(container);
- root.render(
-
-
-
- >,
- {
- onRecoverableError(err) {
- errors.push(err.message);
- },
- },
- );
- try {
- expect(() => {
- expect(Scheduler).toFlushWithoutYielding();
- }).toErrorDev(
- [
- 'Warning: Text content did not match. Server: "server" Client: "client"',
- 'Warning: An error occurred during hydration. The server HTML was replaced with client content in <#document>.',
- ],
- {withoutStack: 1},
- );
- } catch (e) {
- // When gates are false this test fails on a DOMException if you don't clear the scheduler after catching.
- // When gates are true this branch should not be hit
- expect(Scheduler).toFlushWithoutYielding();
- throw e;
}
+
+ function Component() {
+ ReactDOM.preinit('bar', {as: 'script'});
+ return
+
,
);
});
+
+ // @gate enableFloat
+ it('creates a preload resource when ReactDOM.preinit(..., {as: "script" }) is called outside of render on the client', async () => {
+ function App() {
+ React.useEffect(() => {
+ ReactDOM.preinit('foo', {as: 'script'});
+ }, []);
+ return (
+
+ foo
+
+ );
+ }
+
+ const root = ReactDOMClient.createRoot(document);
+ root.render();
+ expect(Scheduler).toFlushWithoutYielding();
+ expect(getMeaningfulChildren(document)).toEqual(
+
+
+
+
+ foo
+ ,
+ );
+ });
+
+ // @gate enableFloat
+ it('warns if you do not pass in a valid href argument or options argument', async () => {
+ function App() {
+ ReactDOM.preinit();
+ ReactDOM.preinit('');
+ ReactDOM.preinit('foo', null);
+ ReactDOM.preinit('foo', {});
+ ReactDOM.preinit('foo', {as: 'foo'});
+ return
foo
;
+ }
+
+ await expect(async () => {
+ await act(() => {
+ renderToPipeableStream().pipe(writable);
+ });
+ }).toErrorDev([
+ 'ReactDOM.preinit(): Expected the `href` argument (first) to be a non-empty string but encountered `undefined` instead',
+ 'ReactDOM.preinit(): Expected the `href` argument (first) to be a non-empty string but encountered an empty string instead',
+ 'ReactDOM.preinit(): Expected the `options` argument (second) to be an object with an `as` property describing the type of resource to be preinitialized but encountered `null` instead',
+ 'ReactDOM.preinit(): Expected the `as` property in the `options` argument (second) to contain a valid value describing the type of resource to be preinitialized but encountered `undefined` instead. Valid values for `as` are "style" and "script".',
+ 'ReactDOM.preinit(): Expected the `as` property in the `options` argument (second) to contain a valid value describing the type of resource to be preinitialized but encountered "foo" instead. Valid values for `as` are "style" and "script".',
+ ]);
+ });
+
+ // @gate enableFloat
+ it('warns if you pass options to `ReactDOM.preinit(..., { as: "style", ... })` incompatible with props from an existing ', async () => {
+ function Component() {
+ ReactDOM.preinit('foo', {
+ as: 'style',
+ integrity: 'some hash',
+ crossOrigin: 'use-credentials',
+ });
+ }
+
+ await expect(async () => {
+ await actIntoEmptyDocument(() => {
+ renderToPipeableStream(
+
+
+
+
+
+ ,
+ );
+ });
+ }).toErrorDev([
+ 'ReactDOM.preinit(): For `href` "foo", the options provided conflict with props found on a that was already rendered. React will always use the props or options it first encounters for a hoistable stylesheet for a given `href` and any later props or options will be ignored if different. Generally, ReactDOM.preinit() is useful when you are not yet rendering a stylesheet but you anticipate it will be used soon. In this case the stylesheet was already rendered so preinitializing it does not provide any additional benefit. To resolve, try making the props and options agree between the and the `ReactDOM.preinit()` call or remove the `ReactDOM.preinit()` call.\n "precedence" missing from options, prop value: "foo"\n "integrity" option value: "some hash", missing from props\n "crossOrigin" option value: "use-credentials", prop value: "anonymous"',
+ ]);
+ });
+
+ // @gate enableFloat
+ it('warns if you pass incompatible options to two `ReactDOM.preinit(..., { as: "style", ... })` with the same href', async () => {
+ function Component() {
+ ReactDOM.preinit('foo', {
+ as: 'style',
+ precedence: 'foo',
+ crossOrigin: 'use-credentials',
+ });
+ ReactDOM.preinit('foo', {
+ as: 'style',
+ integrity: 'some hash',
+ crossOrigin: 'anonymous',
+ });
+ }
+
+ await expect(async () => {
+ await actIntoEmptyDocument(() => {
+ renderToPipeableStream(
+
+
+
+
+ ,
+ );
+ });
+ }).toErrorDev([
+ 'ReactDOM.preinit(): For `href` "foo", the options provided conflict with another call to `ReactDOM.preinit("foo", { as: "style", ... })`. React will always use the options it first encounters when preinitializing a hoistable stylesheet for a given `href` and any later options will be ignored if different. Try updating all calls to `ReactDOM.preinit()` for a given `href` to use the same options, or only call `ReactDOM.preinit()` once per `href`.\n "precedence" missing from options, original option value: "foo"\n "integrity" option value: "some hash", missing from original options\n "crossOrigin" option value: "anonymous", original option value: "use-credentials"',
+ ]);
+ });
+
+ // @gate enableFloat
+ it('warns if you pass options to `ReactDOM.preinit(..., { as: "script", ... })` incompatible with props from an existing ', async () => {
+ function Component() {
+ ReactDOM.preinit('foo', {
+ as: 'script',
+ integrity: 'some hash',
+ crossOrigin: 'use-credentials',
+ });
+ }
+
+ await expect(async () => {
+ await actIntoEmptyDocument(() => {
+ renderToPipeableStream(
+
+
+
+
+
+ ,
+ );
+ });
+ }).toErrorDev([
+ 'ReactDOM.preinit(): For `href` "foo", the options provided conflict with props found on a that was already rendered. React will always use the props or options it first encounters for a hoistable script for a given `href` and any later props or options will be ignored if different. Generally, ReactDOM.preinit() is useful when you are not yet rendering a script but you anticipate it will be used soon and want to go beyond preloading it and have it execute early. In this case the script was already rendered so preinitializing it does not provide any additional benefit. To resolve, try making the props and options agree between the and the `ReactDOM.preinit()` call or remove the `ReactDOM.preinit()` call.\n "integrity" option value: "some hash", missing from props\n "crossOrigin" option value: "use-credentials", prop value: "anonymous"',
+ ]);
+ });
+
+ // @gate enableFloat
+ it('warns if you pass incompatible options to two `ReactDOM.preinit(..., { as: "script", ... })` with the same href', async () => {
+ function Component() {
+ ReactDOM.preinit('foo', {
+ as: 'script',
+ crossOrigin: 'use-credentials',
+ });
+ ReactDOM.preinit('foo', {
+ as: 'script',
+ integrity: 'some hash',
+ crossOrigin: 'anonymous',
+ });
+ }
+
+ await expect(async () => {
+ await actIntoEmptyDocument(() => {
+ renderToPipeableStream(
+
+
+
+
+ ,
+ );
+ });
+ }).toErrorDev([
+ 'ReactDOM.preinit(): For `href` "foo", the options provided conflict with another call to `ReactDOM.preinit("foo", { as: "script", ... })`. React will always use the options it first encounters when preinitializing a hoistable script for a given `href` and any later options will be ignored if different. Try updating all calls to `ReactDOM.preinit()` for a given `href` to use the same options, or only call `ReactDOM.preinit()` once per `href`.\n "integrity" option value: "some hash", missing from original options\n "crossOrigin" option value: "anonymous", original option value: "use-credentials"',
+ ]);
+ });
});
- describe('style resources', () => {
+ describe('Stylesheet Resources', () => {
// @gate enableFloat
- it('treats link rel stylesheet elements as a style resource when it includes a precedence when server rendering', async () => {
+ it('treats link rel stylesheet elements as a stylesheet resource when it includes a precedence when server rendering', async () => {
await actIntoEmptyDocument(() => {
const {pipe} = renderToPipeableStream(
@@ -2178,7 +2891,7 @@ describe('ReactDOMFloat', () => {
});
// @gate enableFloat
- it('treats link rel stylesheet elements as a style resource when it includes a precedence when client rendering', async () => {
+ it('treats link rel stylesheet elements as a stylesheet resource when it includes a precedence when client rendering', async () => {
const root = ReactDOMClient.createRoot(document);
root.render(
@@ -2204,7 +2917,7 @@ describe('ReactDOMFloat', () => {
});
// @gate enableFloat
- it('treats link rel stylesheet elements as a style resource when it includes a precedence when hydrating', async () => {
+ it('treats link rel stylesheet elements as a stylesheet resource when it includes a precedence when hydrating', async () => {
await actIntoEmptyDocument(() => {
const {pipe} = renderToPipeableStream(
@@ -2270,7 +2983,7 @@ describe('ReactDOMFloat', () => {
});
// @gate enableFloat
- it('hoists style resources to the correct precedence', async () => {
+ it('hoists stylesheet resources to the correct precedence', async () => {
await actIntoEmptyDocument(() => {
const {pipe} = renderToPipeableStream(
@@ -2518,2836 +3231,159 @@ describe('ReactDOMFloat', () => {
,
);
});
- });
-
- describe('script resources', () => {
// @gate enableFloat
- it('treats async scripts without onLoad or onError as Resources', async () => {
- await actIntoEmptyDocument(() => {
- const {pipe} = renderToPipeableStream(
-
-
-
-
+ ,
+ );
});
- describe('ReactDOM.preinit as style', () => {
+ describe('ReactDOM.preload(href, { as: ... })', () => {
// @gate enableFloat
- it('creates a style Resource when called during server rendering before first flush', async () => {
- function Component() {
- ReactDOM.preinit('foo', {as: 'style'});
- return 'foo';
- }
- await actIntoEmptyDocument(() => {
- const {pipe} = renderToPipeableStream(
+ it('creates a preload resource when called', async () => {
+ function App() {
+ ReactDOM.preload('foo', {as: 'style'});
+ return (
-
-
-
- ,
- );
+ it('warns if you do not pass in a valid href argument or options argument', async () => {
+ function App() {
+ ReactDOM.preload();
+ ReactDOM.preload('');
+ ReactDOM.preload('foo', null);
+ ReactDOM.preload('foo', {});
+ ReactDOM.preload('foo', {as: 'foo'});
+ return
foo
;
+ }
+
+ await expect(async () => {
+ await act(() => {
+ renderToPipeableStream().pipe(writable);
+ });
+ }).toErrorDev([
+ 'ReactDOM.preload(): Expected the `href` argument (first) to be a non-empty string but encountered `undefined` instead.',
+ 'ReactDOM.preload(): Expected the `href` argument (first) to be a non-empty string but encountered an empty string instead.',
+ 'ReactDOM.preload(): Expected the `options` argument (second) to be an object with an `as` property describing the type of resource to be preloaded but encountered `null` instead.',
+ 'ReactDOM.preload(): Expected the `as` property in the `options` argument (second) to contain a string value describing the type of resource to be preloaded but encountered `undefined` instead. Values that are valid in for the `as` attribute of a `` tag are valid here.',
+ ]);
+ });
+
+ // @gate enableFloat
+ it('warns if you pass incompatible options to two `ReactDOM.preload(...)` with the same href', async () => {
+ function Component() {
+ ReactDOM.preload('foo', {
+ as: 'font',
+ crossOrigin: 'use-credentials',
+ });
+ ReactDOM.preload('foo', {
+ as: 'font',
+ integrity: 'some hash',
+ crossOrigin: 'anonymous',
+ });
+ ReactDOM.preload('foo', {
+ as: 'font',
+ extra: 'ignored',
+ });
+ }
+
+ await expect(async () => {
+ await actIntoEmptyDocument(() => {
+ renderToPipeableStream(
+
+