Skip to content
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

Implement Offscreen in Fizz #24988

Merged
merged 1 commit into from
Jul 26, 2022
Merged
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 @@ -17,6 +17,7 @@ let Scheduler;
let ReactFeatureFlags;
let Suspense;
let SuspenseList;
let Offscreen;
let act;
let IdleEventPriority;

Expand Down Expand Up @@ -106,6 +107,7 @@ describe('ReactDOMServerPartialHydration', () => {
ReactDOMServer = require('react-dom/server');
Scheduler = require('scheduler');
Suspense = React.Suspense;
Offscreen = React.unstable_Offscreen;
if (gate(flags => flags.enableSuspenseList)) {
SuspenseList = React.SuspenseList;
}
Expand Down Expand Up @@ -3283,6 +3285,103 @@ describe('ReactDOMServerPartialHydration', () => {
expect(ref.current.innerHTML).toBe('Hidden child');
});

// @gate enableOffscreen
it('a visible Offscreen component acts like a fragment', async () => {
const ref = React.createRef();

function App() {
return (
<Offscreen mode="visible">
<span ref={ref}>Child</span>
</Offscreen>
);
}

const finalHTML = ReactDOMServer.renderToString(<App />);
expect(Scheduler).toHaveYielded([]);

const container = document.createElement('div');
container.innerHTML = finalHTML;

// Visible Offscreen boundaries behave exactly like fragments: a
// pure indirection.
expect(container).toMatchInlineSnapshot(`
<div>
<span>
Child
</span>
</div>
`);

const span = container.getElementsByTagName('span')[0];

// The tree successfully hydrates
ReactDOMClient.hydrateRoot(container, <App />);
expect(Scheduler).toFlushAndYield([]);
expect(ref.current).toBe(span);
});

// @gate enableOffscreen
it('a hidden Offscreen component is skipped over during server rendering', async () => {
const visibleRef = React.createRef();

function HiddenChild() {
Scheduler.unstable_yieldValue('HiddenChild');
return <span>Hidden</span>;
}

function App() {
Scheduler.unstable_yieldValue('App');
return (
<>
<span ref={visibleRef}>Visible</span>
<Offscreen mode="hidden">
<HiddenChild />
</Offscreen>
</>
);
}

// During server rendering, the Child component should not be evaluated,
// because it's inside a hidden tree.
const finalHTML = ReactDOMServer.renderToString(<App />);
expect(Scheduler).toHaveYielded(['App']);

const container = document.createElement('div');
container.innerHTML = finalHTML;

// The hidden child is not part of the server rendered HTML
expect(container).toMatchInlineSnapshot(`
<div>
<span>
Visible
</span>
</div>
`);

const visibleSpan = container.getElementsByTagName('span')[0];

// The visible span successfully hydrates
ReactDOMClient.hydrateRoot(container, <App />);
expect(Scheduler).toFlushUntilNextPaint(['App']);
expect(visibleRef.current).toBe(visibleSpan);

// Subsequently, the hidden child is prerendered on the client
expect(Scheduler).toFlushUntilNextPaint(['HiddenChild']);
expect(container).toMatchInlineSnapshot(`
<div>
<span>
Visible
</span>
<span
style="display: none;"
>
Hidden
</span>
</div>
`);
});

function itHydratesWithoutMismatch(msg, App) {
it('hydrates without mismatch ' + msg, () => {
const container = document.createElement('div');
Expand Down
35 changes: 27 additions & 8 deletions packages/react-server/src/ReactFizzServer.js
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ import type {
ReactNodeList,
ReactContext,
ReactProviderType,
OffscreenMode,
} from 'shared/ReactTypes';
import type {LazyComponent as LazyComponentType} from 'react/src/ReactLazy';
import type {
Expand Down Expand Up @@ -107,6 +108,7 @@ import {
REACT_PROVIDER_TYPE,
REACT_CONTEXT_TYPE,
REACT_SCOPE_TYPE,
REACT_OFFSCREEN_TYPE,
} from 'shared/ReactSymbols';
import ReactSharedInternals from 'shared/ReactSharedInternals';
import {
Expand Down Expand Up @@ -1062,6 +1064,18 @@ function renderLazyComponent(
popComponentStackInDEV(task);
}

function renderOffscreen(request: Request, task: Task, props: Object): void {
const mode: ?OffscreenMode = (props.mode: any);
if (mode === 'hidden') {
// A hidden Offscreen boundary is not server rendered. Prerendering happens
// on the client.
} else {
// A visible Offscreen boundary is treated exactly like a fragment: a
// pure indirection.
renderNodeDestructive(request, task, props.children);
}
}

function renderElement(
request: Request,
task: Task,
Expand All @@ -1084,14 +1098,15 @@ function renderElement(
}

switch (type) {
// TODO: LegacyHidden acts the same as a fragment. This only works
// because we currently assume that every instance of LegacyHidden is
// accompanied by a host component wrapper. In the hidden mode, the host
// component is given a `hidden` attribute, which ensures that the
// initial HTML is not visible. To support the use of LegacyHidden as a
// true fragment, without an extra DOM node, we would have to hide the
// initial HTML in some other way.
// TODO: Add REACT_OFFSCREEN_TYPE here too with the same capability.
// LegacyHidden acts the same as a fragment. This only works because we
// currently assume that every instance of LegacyHidden is accompanied by a
// host component wrapper. In the hidden mode, the host component is given a
// `hidden` attribute, which ensures that the initial HTML is not visible.
// To support the use of LegacyHidden as a true fragment, without an extra
// DOM node, we would have to hide the initial HTML in some other way.
// TODO: Delete in LegacyHidden. It's an unstable API only used in the
// www build. As a migration step, we could add a special prop to Offscreen
// that simulates the old behavior (no hiding, no change to effects).
case REACT_LEGACY_HIDDEN_TYPE:
case REACT_DEBUG_TRACING_MODE_TYPE:
case REACT_STRICT_MODE_TYPE:
Expand All @@ -1100,6 +1115,10 @@ function renderElement(
renderNodeDestructive(request, task, props.children);
return;
}
case REACT_OFFSCREEN_TYPE: {
renderOffscreen(request, task, props);
return;
}
case REACT_SUSPENSE_LIST_TYPE: {
pushBuiltInComponentStackInDEV(task, 'SuspenseList');
// TODO: SuspenseList should control the boundaries.
Expand Down