Skip to content
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
13 changes: 13 additions & 0 deletions packages/react-dom-bindings/src/client/ReactDOMComponentTree.js
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,7 @@ const internalEventHandlesSetKey = '__reactHandles$' + randomKey;
const internalRootNodeResourcesKey = '__reactResources$' + randomKey;
const internalHoistableMarker = '__reactMarker$' + randomKey;
const internalScrollTimer = '__reactScroll$' + randomKey;
const internalLoadPendingKey = '__reactLoad$' + randomKey;

type InstanceUnion =
| Instance
Expand Down Expand Up @@ -386,6 +387,18 @@ export function clearScrollEndTimer(node: EventTarget): void {
(node: any)[internalScrollTimer] = undefined;
}

export function markNodeAsPendingLoad(node: Node): void {
(node: any)[internalLoadPendingKey] = true;
}

export function clearPendingLoadOnNode(node: Node): void {
(node: any)[internalLoadPendingKey] = undefined;
}

export function isNodePendingLoad(node: Node): boolean {
return (node: any)[internalLoadPendingKey] === true;
}

export function isOwnedInstance(node: Node): boolean {
if (enableInternalInstanceMap) {
return !!(
Expand Down
62 changes: 40 additions & 22 deletions packages/react-dom-bindings/src/client/ReactFiberConfigDOM.js
Original file line number Diff line number Diff line change
Expand Up @@ -56,6 +56,9 @@ import {
getResourcesFromRoot,
isMarkedHoistable,
markNodeAsHoistable,
markNodeAsPendingLoad,
clearPendingLoadOnNode,
isNodePendingLoad,
isOwnedInstance,
} from './ReactDOMComponentTree';
import {
Expand Down Expand Up @@ -5005,11 +5008,18 @@ function preload(href: string, as: string, options?: ?PreloadImplOptions) {
as === 'script' &&
ownerDocument.querySelector(getScriptSelectorFromKey(key))
) {
// We already have a stylesheet for this key. We don't need to preload it.
// We already have a script for this key. We don't need to preload it.
return;
}
const instance = ownerDocument.createElement('link');
setInitialProperties(instance, 'link', preloadProps);
if (as === 'style') {
// Stash a loading state on the preload link. it will clean itself up once settled
markNodeAsPendingLoad(instance);
instance.onload = instance.onerror = () => {
clearPendingLoadOnNode(instance);
};
}
markNodeAsHoistable(instance);
(ownerDocument.head: any).appendChild(instance);
}
Expand Down Expand Up @@ -5357,19 +5367,16 @@ export function getResource(
resource.instance = instance;
resource.state.loading = Loaded | Inserted;
}
}

if (!preloadPropsMap.has(key)) {
const preloadProps = preloadPropsFromStylesheet(qualifiedProps);
preloadPropsMap.set(key, preloadProps);
if (!instance) {
preloadStylesheet(
ownerDocument,
key,
preloadProps,
resource.state,
);
} else {
// We don't have an instance we need to preload it instead
// $FlowFixMe[incompatible-type] -- the key we use here can only match non module preloads
let preloadProps: void | PreloadProps = preloadPropsMap.get(key);
if (!preloadProps) {
preloadProps = preloadPropsFromStylesheet(qualifiedProps);
preloadPropsMap.set(key, preloadProps);
}

preloadStylesheet(ownerDocument, key, preloadProps, resource.state);
}
}
if (currentProps && currentResource === null) {
Expand Down Expand Up @@ -5540,22 +5547,33 @@ function preloadStylesheet(
preloadProps: PreloadProps,
state: StylesheetState,
) {
const preloadEl = ownerDocument.querySelector(
let instance = ownerDocument.querySelector(
getPreloadStylesheetSelectorFromKey(key),
);
if (preloadEl) {
// If we find a preload already it was SSR'd and we won't have an actual
// loading state to track. For now we will just assume it is loaded
state.loading = Loaded;
if (instance) {
if (!isNodePendingLoad(instance)) {
// If we find a preload already it was SSR'd and we won't have an actual
// loading state to track. For now we will just assume it is loaded
state.loading = Loaded;
return;
} else {
// fall through and attach loading listeners
}
} else {
const instance = ownerDocument.createElement('link');
state.preload = instance;
instance.addEventListener('load', () => (state.loading |= Loaded));
instance.addEventListener('error', () => (state.loading |= Errored));
instance = ownerDocument.createElement('link');
markNodeAsPendingLoad(instance);
instance.onload = instance.onerror = clearPendingLoadOnNode.bind(
null,
instance,
);
setInitialProperties(instance, 'link', preloadProps);
markNodeAsHoistable(instance);
(ownerDocument.head: any).appendChild(instance);
}
// $FlowFixMe: [incompatible-type] -- if instance is an Element it will also be an HTMLLinkElement
state.preload = instance;
instance.addEventListener('load', () => (state.loading |= Loaded));
instance.addEventListener('error', () => (state.loading |= Errored));
}

function preloadPropsFromStylesheet(
Expand Down
104 changes: 104 additions & 0 deletions packages/react-dom/src/__tests__/ReactDOMFloat-test.js
Original file line number Diff line number Diff line change
Expand Up @@ -3674,6 +3674,110 @@ body {
);
});

it('does not suspend a transition on a stylesheet whose preload has already loaded', async () => {
const root = ReactDOMClient.createRoot(document);
root.render(
<html>
<body>
<Suspense fallback="loading...">initial</Suspense>
</body>
</html>,
);
await waitForAll([]);

ReactDOM.preload('route.css', {as: 'style'});
expect(getMeaningfulChildren(document.head)).toEqual(
<link rel="preload" href="route.css" as="style" />,
);
expect(getMeaningfulChildren(document.body)).toEqual('initial');

loadPreloads(['route.css']);
assertLog(['load preload: route.css']);

React.startTransition(() => {
root.render(
<html>
<body>
<Suspense fallback="loading...">
<link rel="stylesheet" href="route.css" precedence="default" />
next
</Suspense>
</body>
</html>,
);
});
await waitForAll([]);

expect(getMeaningfulChildren(document.head)).toEqual([
<link rel="stylesheet" href="route.css" data-precedence="default" />,
<link rel="preload" href="route.css" as="style" />,
]);
expect(getMeaningfulChildren(document.body)).toEqual('next');

loadStylesheets(['route.css']);
assertLog(['load stylesheet: route.css']);
expect(getMeaningfulChildren(document.head)).toEqual([
<link rel="stylesheet" href="route.css" data-precedence="default" />,
<link rel="preload" href="route.css" as="style" />,
]);
expect(getMeaningfulChildren(document.body)).toEqual('next');
});

it('suspends a transition on a stylesheet whose preload has not loaded yet', async () => {
const root = ReactDOMClient.createRoot(document);
root.render(
<html>
<body>
<Suspense fallback="loading...">initial</Suspense>
</body>
</html>,
);
await waitForAll([]);

ReactDOM.preload('route.css', {as: 'style'});
expect(getMeaningfulChildren(document.head)).toEqual(
<link rel="preload" href="route.css" as="style" />,
);
expect(getMeaningfulChildren(document.body)).toEqual('initial');

React.startTransition(() => {
root.render(
<html>
<body>
<Suspense fallback="loading...">
<link rel="stylesheet" href="route.css" precedence="default" />
next
</Suspense>
</body>
</html>,
);
});
await waitForAll([]);

expect(getMeaningfulChildren(document.head)).toEqual(
<link rel="preload" href="route.css" as="style" />,
);
expect(getMeaningfulChildren(document.body)).toEqual('initial');

loadPreloads(['route.css']);
assertLog(['load preload: route.css']);
await waitForAll([]);
expect(getMeaningfulChildren(document.head)).toEqual([
<link rel="stylesheet" href="route.css" data-precedence="default" />,
<link rel="preload" href="route.css" as="style" />,
]);
expect(getMeaningfulChildren(document.body)).toEqual('initial');

loadStylesheets(['route.css']);
assertLog(['load stylesheet: route.css']);
await waitForAll([]);
expect(getMeaningfulChildren(document.head)).toEqual([
<link rel="stylesheet" href="route.css" data-precedence="default" />,
<link rel="preload" href="route.css" as="style" />,
]);
expect(getMeaningfulChildren(document.body)).toEqual('next');
});

it('can suspend commits on more than one root for the same resource at the same time', async () => {
document.body.innerHTML = '';
const container1 = document.createElement('div');
Expand Down
Loading