Skip to content

Commit

Permalink
support retaining styles across remounts
Browse files Browse the repository at this point in the history
  • Loading branch information
gnoff committed Sep 24, 2022
1 parent 5506565 commit ad9fb36
Show file tree
Hide file tree
Showing 5 changed files with 142 additions and 17 deletions.
99 changes: 98 additions & 1 deletion packages/react-dom/src/__tests__/ReactDOMFloat-test.js
Original file line number Diff line number Diff line change
Expand Up @@ -395,7 +395,7 @@ describe('ReactDOMFloat', () => {
});
expect(getVisibleChildren(document)).toEqual(
<html>
<head></head>
<head />
<body>
foo
<link rel="preload" as="style" href="foo" />
Expand Down Expand Up @@ -701,6 +701,103 @@ describe('ReactDOMFloat', () => {
</html>,
);
});

// @gate enableFloat
it('retains styles even when a new html, head, and/body mount', async () => {
await actIntoEmptyDocument(() => {
const {pipe} = ReactDOMFizzServer.renderToPipeableStream(
<html>
<head />
<body>
<link rel="stylesheet" href="foo" precedence="foo" />
<link rel="stylesheet" href="bar" precedence="bar" />
server
</body>
</html>,
);
pipe(writable);
});
const errors = [];
ReactDOMClient.hydrateRoot(
document,
<html>
<head>
<link rel="stylesheet" href="qux" precedence="qux" />
<link rel="stylesheet" href="foo" precedence="foo" />
</head>
<body>client</body>
</html>,
{
onRecoverableError(error) {
errors.push(error.message);
},
},
);
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},
);
expect(getVisibleChildren(document)).toEqual(
<html>
<head>
<link rel="stylesheet" href="qux" data-rprec="qux" />
<link rel="stylesheet" href="foo" data-rprec="foo" />
</head>
<body>client</body>
</html>,
);
});

// @gate enableFloat
it('retains styles in head through head remounts', async () => {
const root = ReactDOMClient.createRoot(document);
root.render(
<html>
<head key={1} />
<body>
<link rel="stylesheet" href="foo" precedence="foo" />
<link rel="stylesheet" href="bar" precedence="bar" />
hello
</body>
</html>,
);
expect(Scheduler).toFlushWithoutYielding();
expect(getVisibleChildren(document)).toEqual(
<html>
<head>
<link rel="stylesheet" href="foo" data-rprec="foo" />
<link rel="stylesheet" href="bar" data-rprec="bar" />
</head>
<body>hello</body>
</html>,
);

root.render(
<html>
<head key={2} />
<body>
<link rel="stylesheet" href="foo" precedence="foo" />
<link rel="stylesheet" href="bar" precedence="bar" />
hello
</body>
</html>,
);
expect(Scheduler).toFlushWithoutYielding();
expect(getVisibleChildren(document)).toEqual(
<html>
<head>
<link rel="stylesheet" href="foo" data-rprec="foo" />
<link rel="stylesheet" href="bar" data-rprec="bar" />
</head>
<body>hello</body>
</html>,
);
});
});

// @gate enableFloat
Expand Down
26 changes: 24 additions & 2 deletions packages/react-dom/src/client/ReactDOMFloatClient.js
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,6 @@ import {pushDispatcher, popDispatcher} from '../shared/ReactDOMDispatcher';
import {
validatePreloadResourceDifference,
validateStyleResourceDifference,
validateStyleAndHintProps,
validateLinkPropsForStyleResource,
validateLinkPropsForPreloadResource,
validatePreloadArguments,
Expand Down Expand Up @@ -80,6 +79,7 @@ type StyleResourceLoadingState = Promise<mixed> & {s?: 'l' | 'e'};
// we encounter during render. If this is null and we are dispatching preloads and
// other calls on the ReactDOM module we look for the window global and get the document from there
let currentDocument: ?Document = null;

// It is valid to preload even when we aren't actively rendering. For cases where Float functions are
// called when there is no rendering we track the last used document. It is not safe to insert
// arbitrary resources into the lastCurrentDocument b/c it may not actually be the document
Expand All @@ -88,8 +88,16 @@ let currentDocument: ?Document = null;
// preloads
let lastCurrentDocument: ?Document = null;

// When the document Node that hosts style resources is removed from the tree and another one created
// the style Resources end up in a detatched state. We need to be able to restore them to the newly
// inserted hosts (html, head, or body, preferring head). However to simplify the logic we attempt
// restoration anytime a new Resource host mounts but we only want to restore once per commit. This
// boolean is used to flag that a restore should happen or be ignored and resets on each render
let stylesRestorable = true;

export function prepareToRender(ownerDocument: Document) {
currentDocument = lastCurrentDocument = ownerDocument;
stylesRestorable = true;
pushDispatcher(Dispatcher);
}

Expand Down Expand Up @@ -537,7 +545,7 @@ function immediatelyPreloadStyleResource(resource: StyleResource) {
// we wait and call this later it is possible a preload will already exist for this href
if (resource.instance === null && resource.hint === null) {
const {href, props} = resource;
const preloadProps = preloadPropsFromStyleProps(resource.props);
const preloadProps = preloadPropsFromStyleProps(props);
resource.hint = createPreloadResource(
resource.ownerDocument,
href,
Expand Down Expand Up @@ -718,6 +726,20 @@ function insertStyleInstance(
}
}

export function restoreAllStylesResources() {
if (stylesRestorable) {
stylesRestorable = false;
const iter = styleResources.values();
let resource;
while ((resource = iter.next().value)) {
const {instance, count, ownerDocument, precedence} = resource;
if (count && instance && !ownerDocument.contains(instance)) {
insertStyleInstance(instance, precedence, ownerDocument);
}
}
}
}

function insertPreloadInstance(
instance: Instance,
ownerDocument: Document,
Expand Down
28 changes: 16 additions & 12 deletions packages/react-dom/src/client/ReactDOMHostConfig.js
Original file line number Diff line number Diff line change
Expand Up @@ -83,6 +83,7 @@ import {
prepareToRender as prepareToRenderImpl,
cleanupAfterRender as cleanupAfterRenderImpl,
isHostResourceType,
restoreAllStylesResources,
} from './ReactDOMFloatClient';

export type Type = string;
Expand Down Expand Up @@ -311,6 +312,14 @@ export function finalizeInitialChildren(
return !!props.autoFocus;
case 'img':
return true;
case 'html':
case 'head':
case 'body': {
if (enableFloat) {
return true;
}
}
// eslint-disable-next-line-no-fallthrough
default:
return false;
}
Expand Down Expand Up @@ -449,6 +458,13 @@ export function commitMount(
}
return;
}
case 'html':
case 'head':
case 'body': {
if (enableFloat) {
restoreAllStylesResources();
}
}
}
}

Expand Down Expand Up @@ -763,18 +779,6 @@ export function getSuspenseInstanceFallbackErrorDetails(
digest,
};
}

// let value = {message: undefined, hash: undefined};
// const nextSibling = instance.nextSibling;
// if (nextSibling) {
// const dataset = ((nextSibling: any): HTMLTemplateElement).dataset;
// value.message = dataset.msg;
// value.hash = dataset.hash;
// if (__DEV__) {
// value.stack = dataset.stack;
// }
// }
// return value;
}

export function registerSuspenseInstanceRetry(
Expand Down
3 changes: 2 additions & 1 deletion packages/react-reconciler/src/ReactFiberBeginWork.new.js
Original file line number Diff line number Diff line change
Expand Up @@ -163,6 +163,7 @@ import {
getSuspenseInstanceFallbackErrorDetails,
registerSuspenseInstanceRetry,
supportsHydration,
supportsResources,
isPrimaryRenderer,
getResource,
} from './ReactFiberHostConfig';
Expand Down Expand Up @@ -4003,7 +4004,7 @@ function beginWork(
case HostRoot:
return updateHostRoot(current, workInProgress, renderLanes);
case HostResource:
if (enableFloat) {
if (enableFloat && supportsResources) {
return updateHostResource(current, workInProgress, renderLanes);
}
// eslint-disable-next-line no-fallthrough
Expand Down
3 changes: 2 additions & 1 deletion packages/react-reconciler/src/ReactFiberBeginWork.old.js
Original file line number Diff line number Diff line change
Expand Up @@ -163,6 +163,7 @@ import {
getSuspenseInstanceFallbackErrorDetails,
registerSuspenseInstanceRetry,
supportsHydration,
supportsResources,
isPrimaryRenderer,
getResource,
} from './ReactFiberHostConfig';
Expand Down Expand Up @@ -4003,7 +4004,7 @@ function beginWork(
case HostRoot:
return updateHostRoot(current, workInProgress, renderLanes);
case HostResource:
if (enableFloat) {
if (enableFloat && supportsResources) {
return updateHostResource(current, workInProgress, renderLanes);
}
// eslint-disable-next-line no-fallthrough
Expand Down

0 comments on commit ad9fb36

Please sign in to comment.