Skip to content

Commit

Permalink
[Fizz/Float] Float for stylesheet resources
Browse files Browse the repository at this point in the history
This change implements Float for a minimal use case of hoisting stylesheet resources to the head and ensuring the flush in the appropriate spot in the stream. Subsequent commits will add support for client stylesheet hoisting and Flight resources.

While there is some additional buildout of Float capabilities in general the public APIs have all been removed. The intent with this first implementation is to opt in <link rel="stylesheet"> use the more useful semantics of resources
  • Loading branch information
gnoff committed Sep 14, 2022
1 parent c91a1e0 commit 2119dff
Show file tree
Hide file tree
Showing 51 changed files with 5,702 additions and 669 deletions.
1 change: 1 addition & 0 deletions packages/react-art/src/ReactARTHostConfig.js
Original file line number Diff line number Diff line change
Expand Up @@ -243,6 +243,7 @@ export * from 'react-reconciler/src/ReactFiberHostConfigWithNoHydration';
export * from 'react-reconciler/src/ReactFiberHostConfigWithNoScopes';
export * from 'react-reconciler/src/ReactFiberHostConfigWithNoTestSelectors';
export * from 'react-reconciler/src/ReactFiberHostConfigWithNoMicrotasks';
export * from 'react-reconciler/src/ReactFiberHostConfigWithNoResources';

export function appendInitialChild(parentInstance, child) {
if (typeof child === 'string') {
Expand Down
2 changes: 2 additions & 0 deletions packages/react-dom/index.experimental.js
Original file line number Diff line number Diff line change
Expand Up @@ -22,3 +22,5 @@ export {
unstable_runWithPriority, // DO NOT USE: Temporarily exposed to migrate off of Scheduler.runWithPriority.
version,
} from './src/client/ReactDOM';

export {preinit, preload} from './src/shared/ReactDOMFloat';
122 changes: 8 additions & 114 deletions packages/react-dom/src/__tests__/ReactDOMFizzServer-test.js
Original file line number Diff line number Diff line change
Expand Up @@ -329,6 +329,7 @@ describe('ReactDOMFizzServer', () => {
);
pipe(writable);
});

expect(getVisibleChildren(container)).toEqual(
<div>
<div>Loading...</div>
Expand Down Expand Up @@ -4391,7 +4392,7 @@ describe('ReactDOMFizzServer', () => {
});

// @gate enableFloat
it('recognizes stylesheet links as attributes during hydration', async () => {
it('recognizes stylesheet links as resources during hydration', async () => {
await actIntoEmptyDocument(() => {
const {pipe} = ReactDOMFizzServer.renderToPipeableStream(
<>
Expand Down Expand Up @@ -4469,15 +4470,13 @@ describe('ReactDOMFizzServer', () => {
);
try {
expect(Scheduler).toFlushWithoutYielding();
// The reason data-bar is not found on the link is props are only used to generate the resource instance
// if it does not already exist but in this case it was left behind in the document. In the future
// changing props on resources will warn in dev
expect(getVisibleChildren(document)).toEqual(
<html>
<head>
<link
rel="stylesheet"
href="foo"
data-rprec="default"
data-bar="bar"
/>
<link rel="stylesheet" href="foo" data-rprec="default" />
</head>
<body>a body</body>
</html>,
Expand All @@ -4498,7 +4497,7 @@ describe('ReactDOMFizzServer', () => {

// Temporarily this test is expected to fail everywhere. When we have resource hoisting
// it should start to pass and we can adjust the gate accordingly
// @gate false && enableFloat
// @gate experimental && enableFloat
it('should insert missing resources during hydration', async () => {
await actIntoEmptyDocument(() => {
const {pipe} = ReactDOMFizzServer.renderToPipeableStream(
Expand All @@ -4525,7 +4524,7 @@ describe('ReactDOMFizzServer', () => {
expect(getVisibleChildren(document)).toEqual(
<html>
<head>
<link rel="stylesheet" href="foo" precedence="foo" />
<link rel="stylesheet" href="foo" data-rprec="foo" />
</head>
<body>foo</body>
</html>,
Expand All @@ -4546,111 +4545,6 @@ describe('ReactDOMFizzServer', () => {
}
});

// @gate experimental && enableFloat
it('fail hydration if a suitable resource cannot be found in the DOM for a given location (href)', async () => {
gate(flags => {
if (!(__EXPERIMENTAL__ && flags.enableFloat)) {
throw new Error('bailing out of test');
}
});
await actIntoEmptyDocument(() => {
const {pipe} = ReactDOMFizzServer.renderToPipeableStream(
<html>
<head />
<body>a body</body>
</html>,
);
pipe(writable);
});

const errors = [];
ReactDOMClient.hydrateRoot(
document,
<html>
<head>
<link rel="stylesheet" href="foo" precedence="low" />
</head>
<body>a body</body>
</html>,
{
onRecoverableError(err, errInfo) {
errors.push(err.message);
},
},
);
expect(() => {
expect(Scheduler).toFlushWithoutYielding();
}).toErrorDev(
[
'Warning: A matching Hydratable Resource was not found in the DOM for <link rel="stylesheet" href="foo">',
'Warning: An error occurred during hydration. The server HTML was replaced with client content in <#document>.',
],
{withoutStack: 1},
);
expect(errors).toEqual([
'Hydration failed because the initial UI does not match what was rendered on the server.',
'Hydration failed because the initial UI does not match what was rendered on the server.',
'There was an error while hydrating. Because the error happened outside of a Suspense boundary, the entire root will switch to client rendering.',
]);
});

// @gate experimental && enableFloat
it('should error in dev when rendering more than one resource for a given location (href)', async () => {
gate(flags => {
if (!(__EXPERIMENTAL__ && flags.enableFloat)) {
throw new Error('bailing out of test');
}
});
await actIntoEmptyDocument(() => {
const {pipe} = ReactDOMFizzServer.renderToPipeableStream(
<>
<link rel="stylesheet" href="foo" precedence="low" />
<link rel="stylesheet" href="foo" precedence="high" />
<html>
<head />
<body>a body</body>
</html>
</>,
);
pipe(writable);
});
expect(getVisibleChildren(document)).toEqual(
<html>
<head>
<link rel="stylesheet" href="foo" data-rprec="low" />
<link rel="stylesheet" href="foo" data-rprec="high" />
</head>
<body>a body</body>
</html>,
);

const errors = [];
ReactDOMClient.hydrateRoot(
document,
<>
<html>
<head>
<link rel="stylesheet" href="foo" precedence="low" />
<link rel="stylesheet" href="foo" precedence="high" />
</head>
<body>a body</body>
</html>
</>,
{
onRecoverableError(err, errInfo) {
errors.push(err.message);
},
},
);
expect(() => {
expect(Scheduler).toFlushWithoutYielding();
}).toErrorDev([
'Warning: Stylesheet resources need a unique representation in the DOM while hydrating and more than one matching DOM Node was found. To fix, ensure you are only rendering one stylesheet link with an href attribute of "foo"',
'Warning: Stylesheet resources need a unique representation in the DOM while hydrating and more than one matching DOM Node was found. To fix, ensure you are only rendering one stylesheet link with an href attribute of "foo"',
]);
expect(errors).toEqual([]);
});

describe('text separators', () => {
// To force performWork to start before resolving AsyncText but before piping we need to wait until
// after scheduleWork which currently uses setImmediate to delay performWork
Expand Down
Loading

0 comments on commit 2119dff

Please sign in to comment.