Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -4518,7 +4518,7 @@ export function writeCompletedRoot(
}
if (enableFizzBlockingRender) {
const preamble = renderState.preamble;
if (preamble.htmlChunks || preamble.headChunks) {
if (!isComplete && (preamble.htmlChunks || preamble.headChunks)) {
// If we rendered the whole document, then we emitted a rel="expect" that needs a
// matching target. Normally we use one of the bootstrap scripts for this but if
// there are none, then we need to emit a tag to complete the shell.
Expand Down
22 changes: 2 additions & 20 deletions packages/react-dom/src/__tests__/ReactDOMFizzServer-test.js
Original file line number Diff line number Diff line change
Expand Up @@ -3630,9 +3630,6 @@ describe('ReactDOMFizzServer', () => {
'</script><script async="" src="foo"></script>' +
(gate(flags => flags.shouldUseFizzExternalRuntime)
? '<script src="react-dom-bindings/src/server/ReactDOMServerExternalRuntime.js" async=""></script>'
: '') +
(gate(flags => flags.enableFizzBlockingRender)
? '<link rel="expect" href="#_R_" blocking="render">'
: ''),
);
});
Expand Down Expand Up @@ -4566,15 +4563,7 @@ describe('ReactDOMFizzServer', () => {

// the html should be as-is
expect(document.documentElement.innerHTML).toEqual(
'<head><script src="react-dom-bindings/src/server/ReactDOMServerExternalRuntime.js" async=""></script>' +
(gate(flags => flags.enableFizzBlockingRender)
? '<link rel="expect" href="#_R_" blocking="render">'
: '') +
'</head><body><p>hello world!</p>' +
(gate(flags => flags.enableFizzBlockingRender)
? '<template id="_R_"></template>'
: '') +
'</body>',
'<head><script src="react-dom-bindings/src/server/ReactDOMServerExternalRuntime.js" async=""></script></head><body><p>hello world!</p></body>',
);
});

Expand Down Expand Up @@ -6618,14 +6607,7 @@ describe('ReactDOMFizzServer', () => {
(gate(flags => flags.shouldUseFizzExternalRuntime)
? '<script src="react-dom-bindings/src/server/ReactDOMServerExternalRuntime.js" async=""></script>'
: '') +
(gate(flags => flags.enableFizzBlockingRender)
? '<link rel="expect" href="#_R_" blocking="render">'
: '') +
'</head><body><script>try { foo() } catch (e) {} ;</script>' +
(gate(flags => flags.enableFizzBlockingRender)
? '<template id="_R_"></template>'
: '') +
'</body></html>',
'</head><body><script>try { foo() } catch (e) {} ;</script></body></html>',
);
});

Expand Down
22 changes: 4 additions & 18 deletions packages/react-dom/src/__tests__/ReactDOMFizzServerBrowser-test.js
Original file line number Diff line number Diff line change
Expand Up @@ -71,15 +71,9 @@ describe('ReactDOMFizzServerBrowser', () => {
),
);
const result = await readResult(stream);
if (gate(flags => flags.enableFizzBlockingRender)) {
expect(result).toMatchInlineSnapshot(
`"<!DOCTYPE html><html><head><link rel="expect" href="#_R_" blocking="render"/></head><body>hello world<template id="_R_"></template></body></html>"`,
);
} else {
expect(result).toMatchInlineSnapshot(
`"<!DOCTYPE html><html><head></head><body>hello world</body></html>"`,
);
}
expect(result).toMatchInlineSnapshot(
`"<!DOCTYPE html><html><head></head><body>hello world</body></html>"`,
);
});

it('should emit bootstrap script src at the end', async () => {
Expand Down Expand Up @@ -522,15 +516,7 @@ describe('ReactDOMFizzServerBrowser', () => {

const result = await readResult(stream);
expect(result).toEqual(
'<!DOCTYPE html><html><head>' +
(gate(flags => flags.enableFizzBlockingRender)
? '<link rel="expect" href="#_R_" blocking="render"/>'
: '') +
'<title>foo</title></head><body>bar' +
(gate(flags => flags.enableFizzBlockingRender)
? '<template id="_R_"></template>'
: '') +
'</body></html>',
'<!DOCTYPE html><html><head><title>foo</title></head><body>bar</body></html>',
);
});

Expand Down
12 changes: 3 additions & 9 deletions packages/react-dom/src/__tests__/ReactDOMFizzServerEdge-test.js
Original file line number Diff line number Diff line change
Expand Up @@ -73,15 +73,9 @@ describe('ReactDOMFizzServerEdge', () => {
setTimeout(resolve, 1);
});

if (gate(flags => flags.enableFizzBlockingRender)) {
expect(result).toMatchInlineSnapshot(
`"<!DOCTYPE html><html><head><link rel="expect" href="#_R_" blocking="render"/></head><body><main>hello</main><template id="_R_"></template></body></html>"`,
);
} else {
expect(result).toMatchInlineSnapshot(
`"<!DOCTYPE html><html><head></head><body><main>hello</main></body></html>"`,
);
}
expect(result).toMatchInlineSnapshot(
`"<!DOCTYPE html><html><head></head><body><main>hello</main></body></html>"`,
);
});

it('recoverably errors and does not add rel="expect" for large shells', async () => {
Expand Down
12 changes: 3 additions & 9 deletions packages/react-dom/src/__tests__/ReactDOMFizzServerNode-test.js
Original file line number Diff line number Diff line change
Expand Up @@ -113,15 +113,9 @@ describe('ReactDOMFizzServerNode', () => {
pipe(writable);
});
// with Float, we emit empty heads if they are elided when rendering <html>
if (gate(flags => flags.enableFizzBlockingRender)) {
expect(output.result).toMatchInlineSnapshot(
`"<!DOCTYPE html><html><head><link rel="expect" href="#_R_" blocking="render"/></head><body>hello world<template id="_R_"></template></body></html>"`,
);
} else {
expect(output.result).toMatchInlineSnapshot(
`"<!DOCTYPE html><html><head></head><body>hello world</body></html>"`,
);
}
expect(output.result).toMatchInlineSnapshot(
`"<!DOCTYPE html><html><head></head><body>hello world</body></html>"`,
);
});

it('should emit bootstrap script src at the end', async () => {
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,160 @@
/**
* Copyright (c) Meta Platforms, Inc. and affiliates.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*
* @emails react-core
* @jest-environment node
*/

'use strict';

let Stream;
let React;
let ReactDOMFizzServer;
let Suspense;
let lazy;
let act;

describe('ReactDOMFizzServerNode Issue 34966', () => {
beforeEach(() => {
jest.resetModules();
React = require('react');
ReactDOMFizzServer = require('react-dom/server');
Stream = require('stream');
Suspense = React.Suspense;
lazy = React.lazy;
act = require('internal-test-utils').act;
});

function getTestWritable() {
const writable = new Stream.PassThrough();
writable.setEncoding('utf8');
const output = {result: '', error: undefined};
writable.on('data', chunk => {
output.result += chunk;
});
writable.on('error', error => {
output.error = error;
});
const completed = new Promise(resolve => {
writable.on('finish', () => {
resolve();
});
writable.on('error', () => {
resolve();
});
});
return {writable, completed, output};
}

it('should not inject streaming scripts in onAllReady with lazy components', async () => {
const Button = () =>
React.createElement(
'button',
{
type: 'button',
'data-test-button-value1': 'some-value-for-test',
'data-test-button-value2': 'some-value-for-test',
},
'Test',
);

const LazyButton = lazy(async () => ({default: Button}));

const App = () =>
React.createElement(
'html',
null,
React.createElement('head', null),
React.createElement(
'body',
null,
React.createElement(
Suspense,
{fallback: React.createElement('h1', null, 'Loading...')},
React.createElement(LazyButton),
),
),
);

const {writable, output, completed} = getTestWritable();

let allReadyCalled = false;

await act(async () => {
const {pipe} = ReactDOMFizzServer.renderToPipeableStream(
React.createElement(App),
{
onAllReady() {
allReadyCalled = true;
pipe(writable);
},
},
);
});

await completed;

expect(allReadyCalled).toBe(true);

expect(output.result).not.toContain('$RC');
expect(output.result).not.toContain('$RV');
expect(output.result).not.toContain('$RB');
expect(output.result).not.toContain('$RT');

expect(output.result).not.toContain('<div hidden');
expect(output.result).not.toContain('<template');

expect(output.result).toContain('<button');
expect(output.result).toContain('data-test-button-value1');
expect(output.result).toContain('Test</button>');

expect(output.result).not.toContain('Loading...');
});

it('should inject streaming scripts in onShellReady with lazy components', async () => {
const Button = () =>
React.createElement('button', {type: 'button'}, 'Test');

const LazyButton = lazy(async () => ({default: Button}));

const App = () =>
React.createElement(
'html',
null,
React.createElement('head', null),
React.createElement(
'body',
null,
React.createElement(
Suspense,
{fallback: React.createElement('h1', null, 'Loading...')},
React.createElement(LazyButton),
),
),
);

const {writable, output, completed} = getTestWritable();

let shellReadyCalled = false;

await act(async () => {
const {pipe} = ReactDOMFizzServer.renderToPipeableStream(
React.createElement(App),
{
onShellReady() {
shellReadyCalled = true;
pipe(writable);
},
},
);
});

await completed;

expect(shellReadyCalled).toBe(true);

});
});
8 changes: 8 additions & 0 deletions packages/react-server/src/ReactFizzServer.js
Original file line number Diff line number Diff line change
Expand Up @@ -5757,6 +5757,9 @@ function flushSegment(
// because it doesn't make sense to outline something if its parent is going to be
// blocked on something later in the stream anyway.
!flushingPartialBoundaries &&
// We don't outline when all pending tasks are complete (e.g., when using onAllReady)
// because there's no need for streaming instructions if everything is already done.
request.allPendingTasks > 0 &&
isEligibleForOutlining(request, boundary) &&
(flushedByteSize + boundary.byteSize > request.progressiveChunkSize ||
hasSuspenseyContent(boundary.contentState))
Expand Down Expand Up @@ -6043,6 +6046,11 @@ function flushCompletedQueues(
logRecoverableError(request, error, errorInfo, null);
}
}
// If all pending tasks are complete, we don't need blocking render instructions
// because everything is already done (e.g., when using onAllReady for SEO crawlers).
if (request.allPendingTasks === 0) {
skipBlockingShell = true;
}
flushPreamble(
request,
destination,
Expand Down