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

[Float][Fiber] Enable Float methods to be called outside of render #26557

Merged
merged 1 commit into from
Apr 20, 2023
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
8 changes: 0 additions & 8 deletions packages/react-art/src/ReactFiberConfigART.js
Original file line number Diff line number Diff line change
Expand Up @@ -479,11 +479,3 @@ export function suspendInstance(type, props) {}
export function waitForCommitToBeReady() {
return null;
}
// eslint-disable-next-line no-undef
export function prepareRendererToRender(container: Container): void {
// noop
}

export function resetRendererAfterRender(): void {
// noop
}
138 changes: 28 additions & 110 deletions packages/react-dom-bindings/src/client/ReactFiberConfigDOM.js
Original file line number Diff line number Diff line change
Expand Up @@ -25,8 +25,6 @@ import {ConcurrentMode, NoMode} from 'react-reconciler/src/ReactTypeOfMode';

import hasOwnProperty from 'shared/hasOwnProperty';
import {checkAttributeStringCoercion} from 'shared/CheckStringCoercion';
import ReactDOMSharedInternals from 'shared/ReactDOMSharedInternals.js';
const {Dispatcher} = ReactDOMSharedInternals;

import {
precacheFiberNode,
Expand Down Expand Up @@ -1936,31 +1934,6 @@ export function prepareToCommitHoistables() {
tagCaches = 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
// that the resource is meant to apply too (for example stylesheets or scripts). This is only
// appropriate for resources that don't really have a strict tie to the document itself for example
// preloads
let lastCurrentDocument: ?Document = null;
let previousDispatcher = null;
export function prepareRendererToRender(rootContainer: Container) {
if (enableFloat) {
const rootNode = getHoistableRoot(rootContainer);
lastCurrentDocument = getDocumentFromRoot(rootNode);

previousDispatcher = Dispatcher.current;
Dispatcher.current = ReactDOMClientDispatcher;
}
}

export function resetRendererAfterRender() {
if (enableFloat) {
Dispatcher.current = previousDispatcher;
previousDispatcher = null;
}
}

// global collections of Resources
const preloadPropsMap: Map<string, PreloadProps> = new Map();
const preconnectsSet: Set<string> = new Set();
Expand All @@ -1982,25 +1955,6 @@ function getCurrentResourceRoot(): null | HoistableRoot {
return currentContainer ? getHoistableRoot(currentContainer) : null;
}

// Preloads are somewhat special. Even if we don't have the Document
// used by the root that is rendering a component trying to insert a preload
// we can still seed the file cache by doing the preload on any document we have
// access to. We prefer the currentDocument if it exists, we also prefer the
// lastCurrentDocument if that exists. As a fallback we will use the window.document
// if available.
function getDocumentForPreloads(): ?Document {
const root = getCurrentResourceRoot();
if (root) {
return root.ownerDocument || root;
} else {
try {
return lastCurrentDocument || window.document;
} catch (error) {
return null;
}
}
}

function getDocumentFromRoot(root: HoistableRoot): Document {
return root.ownerDocument || root;
}
Expand All @@ -2015,13 +1969,23 @@ export const ReactDOMClientDispatcher = {
preinit,
};

// We expect this to get inlined. It is a function mostly to communicate the special nature of
// how we resolve the HoistableRoot for ReactDOM.pre*() methods. Because we support calling
// these methods outside of render there is no way to know which Document or ShadowRoot is 'scoped'
// and so we have to fall back to something universal. Currently we just refer to the global document.
// This is notable because nowhere else in ReactDOM do we actually reference the global document or window
// because we may be rendering inside an iframe.
function getDocumentForImperativeFloatMethods(): Document {
return document;
}

function preconnectAs(
rel: 'preconnect' | 'dns-prefetch',
crossOrigin: null | '' | 'use-credentials',
href: string,
) {
const ownerDocument = getDocumentForPreloads();
if (typeof href === 'string' && href && ownerDocument) {
const ownerDocument = getDocumentForImperativeFloatMethods();
if (typeof href === 'string' && href) {
const limitedEscapedHref =
escapeSelectorAttributeValueInsideDoubleQuotes(href);
let key = `link[rel="${rel}"][href="${limitedEscapedHref}"]`;
Expand All @@ -2043,6 +2007,9 @@ function preconnectAs(
}

function prefetchDNS(href: string, options?: mixed) {
if (!enableFloat) {
return;
}
if (__DEV__) {
if (typeof href !== 'string' || !href) {
console.error(
Expand Down Expand Up @@ -2105,10 +2072,13 @@ type PreloadOptions = {
type?: string,
};
function preload(href: string, options: PreloadOptions) {
if (!enableFloat) {
return;
}
if (__DEV__) {
validatePreloadArguments(href, options);
}
const ownerDocument = getDocumentForPreloads();
const ownerDocument = getDocumentForImperativeFloatMethods();
if (
typeof href === 'string' &&
href &&
Expand Down Expand Up @@ -2166,61 +2136,25 @@ type PreinitOptions = {
integrity?: string,
};
function preinit(href: string, options: PreinitOptions) {
if (!enableFloat) {
return;
}
if (__DEV__) {
validatePreinitArguments(href, options);
}
const ownerDocument = getDocumentForImperativeFloatMethods();

if (
typeof href === 'string' &&
href &&
typeof options === 'object' &&
options !== null
) {
const resourceRoot = getCurrentResourceRoot();
const as = options.as;
if (!resourceRoot) {
if (as === 'style' || as === 'script') {
// We are going to emit a preload as a best effort fallback since this preinit
// was called outside of a render. Given the passive nature of this fallback
// we do not warn in dev when props disagree if there happens to already be a
// matching preload with this href
const preloadDocument = getDocumentForPreloads();
if (preloadDocument) {
const limitedEscapedHref =
escapeSelectorAttributeValueInsideDoubleQuotes(href);
const preloadKey = `link[rel="preload"][as="${as}"][href="${limitedEscapedHref}"]`;
let key = preloadKey;
switch (as) {
case 'style':
key = getStyleKey(href);
break;
case 'script':
key = getScriptKey(href);
break;
}
if (!preloadPropsMap.has(key)) {
const preloadProps = preloadPropsFromPreinitOptions(
href,
as,
options,
);
preloadPropsMap.set(key, preloadProps);

if (null === preloadDocument.querySelector(preloadKey)) {
const instance = preloadDocument.createElement('link');
setInitialProperties(instance, 'link', preloadProps);
markNodeAsHoistable(instance);
(preloadDocument.head: any).appendChild(instance);
}
}
}
}
return;
}

switch (as) {
case 'style': {
const styles = getResourcesFromRoot(resourceRoot).hoistableStyles;
const styles = getResourcesFromRoot(ownerDocument).hoistableStyles;

const key = getStyleKey(href);
const precedence = options.precedence || 'default';
Expand All @@ -2239,7 +2173,7 @@ function preinit(href: string, options: PreinitOptions) {
};

// Attempt to hydrate instance from DOM
let instance: null | Instance = resourceRoot.querySelector(
let instance: null | Instance = ownerDocument.querySelector(
getStylesheetSelectorFromKey(key),
);
if (instance) {
Expand All @@ -2255,7 +2189,6 @@ function preinit(href: string, options: PreinitOptions) {
if (preloadProps) {
adoptPreloadPropsForStylesheet(stylesheetProps, preloadProps);
}
const ownerDocument = getDocumentFromRoot(resourceRoot);
const link = (instance = ownerDocument.createElement('link'));
markNodeAsHoistable(link);
setInitialProperties(link, 'link', stylesheetProps);
Expand All @@ -2272,7 +2205,7 @@ function preinit(href: string, options: PreinitOptions) {
});

state.loading |= Inserted;
insertStylesheet(instance, precedence, resourceRoot);
insertStylesheet(instance, precedence, ownerDocument);
}

// Construct a Resource and cache it
Expand All @@ -2287,7 +2220,7 @@ function preinit(href: string, options: PreinitOptions) {
}
case 'script': {
const src = href;
const scripts = getResourcesFromRoot(resourceRoot).hoistableScripts;
const scripts = getResourcesFromRoot(ownerDocument).hoistableScripts;

const key = getScriptKey(src);

Expand All @@ -2300,7 +2233,7 @@ function preinit(href: string, options: PreinitOptions) {
}

// Attempt to hydrate instance from DOM
let instance: null | Instance = resourceRoot.querySelector(
let instance: null | Instance = ownerDocument.querySelector(
getScriptSelectorFromKey(key),
);
if (!instance) {
Expand All @@ -2311,7 +2244,6 @@ function preinit(href: string, options: PreinitOptions) {
if (preloadProps) {
adoptPreloadPropsForScript(scriptProps, preloadProps);
}
const ownerDocument = getDocumentFromRoot(resourceRoot);
instance = ownerDocument.createElement('script');
markNodeAsHoistable(instance);
setInitialProperties(instance, 'link', scriptProps);
Expand All @@ -2332,20 +2264,6 @@ function preinit(href: string, options: PreinitOptions) {
}
}

function preloadPropsFromPreinitOptions(
href: string,
as: ResourceType,
options: PreinitOptions,
): PreloadProps {
return {
href,
rel: 'preload',
as,
crossOrigin: as === 'font' ? '' : options.crossOrigin,
integrity: options.integrity,
};
}

function stylesheetPropsFromPreinitOptions(
href: string,
precedence: string,
Expand Down
50 changes: 47 additions & 3 deletions packages/react-dom/src/__tests__/ReactDOMFloat-test.js
Original file line number Diff line number Diff line change
Expand Up @@ -3833,7 +3833,7 @@ body {
});

// @gate enableFloat
it('creates a preload resource when ReactDOM.preinit(..., {as: "style" }) is called outside of render on the client', async () => {
it('creates a stylesheet resource in the ownerDocument when ReactDOM.preinit(..., {as: "style" }) is called outside of render on the client', async () => {
function App() {
React.useEffect(() => {
ReactDOM.preinit('foo', {as: 'style'});
Expand All @@ -3851,11 +3851,55 @@ body {
expect(getMeaningfulChildren(document)).toEqual(
<html>
<head>
<link rel="preload" href="foo" as="style" />
<link rel="stylesheet" href="foo" data-precedence="default" />
</head>
<body>foo</body>
</html>,
);
});

// @gate enableFloat
it('creates a stylesheet resource in the ownerDocument when ReactDOM.preinit(..., {as: "style" }) is called outside of render on the client', async () => {
// This is testing behavior, but it shows that it is not a good idea to preinit inside a shadowRoot. The point is we are asserting a behavior
// you would want to avoid in a real app.
const shadow = document.body.attachShadow({mode: 'open'});
function ShadowComponent() {
ReactDOM.preinit('bar', {as: 'style'});
return null;
}
function App() {
React.useEffect(() => {
ReactDOM.preinit('foo', {as: 'style'});
}, []);
return (
<html>
<body>
foo
{ReactDOM.createPortal(
<div>
<ShadowComponent />
shadow
</div>,
shadow,
)}
</body>
</html>
);
}

const root = ReactDOMClient.createRoot(document);
root.render(<App />);
await waitForAll([]);
expect(getMeaningfulChildren(document)).toEqual(
<html>
<head>
<link rel="stylesheet" href="bar" data-precedence="default" />
<link rel="stylesheet" href="foo" data-precedence="default" />
</head>
<body>foo</body>
</html>,
);
expect(getMeaningfulChildren(shadow)).toEqual(<div>shadow</div>);
});

// @gate enableFloat
Expand Down Expand Up @@ -3955,7 +3999,7 @@ body {
expect(getMeaningfulChildren(document)).toEqual(
<html>
<head>
<link rel="preload" href="foo" as="script" />
<script async="" src="foo" />
</head>
<body>foo</body>
</html>,
Expand Down
10 changes: 2 additions & 8 deletions packages/react-dom/src/client/ReactDOMRoot.js
Original file line number Diff line number Diff line change
Expand Up @@ -247,11 +247,8 @@ export function createRoot(
transitionCallbacks,
);
markContainerAsRoot(root.current, container);
Dispatcher.current = ReactDOMClientDispatcher;

if (enableFloat) {
// Set the default dispatcher to the client dispatcher
Dispatcher.current = ReactDOMClientDispatcher;
}
const rootContainerElement: Document | Element | DocumentFragment =
container.nodeType === COMMENT_NODE
? (container.parentNode: any)
Expand Down Expand Up @@ -339,10 +336,7 @@ export function hydrateRoot(
transitionCallbacks,
);
markContainerAsRoot(root.current, container);
if (enableFloat) {
// Set the default dispatcher to the client dispatcher
Dispatcher.current = ReactDOMClientDispatcher;
}
Dispatcher.current = ReactDOMClientDispatcher;
// This can't be a comment node since hydration doesn't work on comment nodes anyway.
listenToAllSupportedEvents(container);

Expand Down
8 changes: 0 additions & 8 deletions packages/react-native-renderer/src/ReactFiberConfigFabric.js
Original file line number Diff line number Diff line change
Expand Up @@ -489,11 +489,3 @@ export function suspendInstance(type: Type, props: Props): void {}
export function waitForCommitToBeReady(): null {
return null;
}

export function prepareRendererToRender(container: Container): void {
// noop
}

export function resetRendererAfterRender() {
// noop
}
8 changes: 0 additions & 8 deletions packages/react-native-renderer/src/ReactFiberConfigNative.js
Original file line number Diff line number Diff line change
Expand Up @@ -542,11 +542,3 @@ export function suspendInstance(type: Type, props: Props): void {}
export function waitForCommitToBeReady(): null {
return null;
}

export function prepareRendererToRender(container: Container): void {
// noop
}

export function resetRendererAfterRender(): void {
// noop
}
Loading