Skip to content

Commit d1e3e84

Browse files
committed
Implement suspensey css for float
Implements waitForCommitToBeReady for resources. currently it is only opted into when a special prop is passed. This will be removed in the next commit when I update all the tests that now require different mechanics to simulate resource loading. The general approach is to track how many things we are waiting on and when we hit zero proceed with the commit. For Float CSS in particular we wait for all stylesheet preloads before inserting any uninserted stylesheets. When all the stylesheets have loaded we continue the commit as usual.
1 parent 34fa5e8 commit d1e3e84

13 files changed

+486
-81
lines changed

packages/react-dom-bindings/src/client/ReactDOMHostConfig.js

Lines changed: 317 additions & 36 deletions
Large diffs are not rendered by default.

packages/react-dom-bindings/src/server/fizz-instruction-set/ReactDOMFizzInstructionSetExternalRuntime.js

Lines changed: 3 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -6,8 +6,6 @@ import {
66
clientRenderBoundary,
77
completeBoundary,
88
completeSegment,
9-
LOADED,
10-
ERRORED,
119
} from './ReactDOMFizzInstructionSetShared';
1210

1311
export {clientRenderBoundary, completeBoundary, completeSegment};
@@ -46,10 +44,6 @@ export function completeBoundaryWithStyles(
4644
const dependencies = [];
4745
let href, precedence, attr, loadingState, resourceEl, media;
4846

49-
function setStatus(s) {
50-
this['s'] = s;
51-
}
52-
5347
// Sheets Mode
5448
let sheetMode = true;
5549
while (true) {
@@ -84,14 +78,10 @@ export function completeBoundaryWithStyles(
8478
while ((attr = stylesheetDescriptor[j++])) {
8579
resourceEl.setAttribute(attr, stylesheetDescriptor[j++]);
8680
}
87-
loadingState = resourceEl['_p'] = new Promise((re, rj) => {
88-
resourceEl.onload = re;
89-
resourceEl.onerror = rj;
81+
loadingState = resourceEl['_p'] = new Promise((resolve, reject) => {
82+
resourceEl.onload = resolve;
83+
resourceEl.onerror = reject;
9084
});
91-
loadingState.then(
92-
setStatus.bind(loadingState, LOADED),
93-
setStatus.bind(loadingState, ERRORED),
94-
);
9585
// Save this resource element so we can bailout if it is used again
9686
resourceMap.set(href, resourceEl);
9787
}

packages/react-dom-bindings/src/server/fizz-instruction-set/ReactDOMFizzInstructionSetInlineCodeStrings.js

Lines changed: 1 addition & 1 deletion
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

packages/react-dom-bindings/src/server/fizz-instruction-set/ReactDOMFizzInstructionSetInlineSource.js

Lines changed: 3 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -8,8 +8,6 @@ import {
88
clientRenderBoundary,
99
completeBoundary,
1010
completeSegment,
11-
LOADED,
12-
ERRORED,
1311
} from './ReactDOMFizzInstructionSetShared';
1412

1513
export {clientRenderBoundary, completeBoundary, completeSegment};
@@ -49,10 +47,6 @@ export function completeBoundaryWithStyles(
4947
const dependencies = [];
5048
let href, precedence, attr, loadingState, resourceEl, media;
5149

52-
function setStatus(s) {
53-
this['s'] = s;
54-
}
55-
5650
// Sheets Mode
5751
let sheetMode = true;
5852
while (true) {
@@ -87,14 +81,10 @@ export function completeBoundaryWithStyles(
8781
while ((attr = stylesheetDescriptor[j++])) {
8882
resourceEl.setAttribute(attr, stylesheetDescriptor[j++]);
8983
}
90-
loadingState = resourceEl['_p'] = new Promise((re, rj) => {
91-
resourceEl.onload = re;
92-
resourceEl.onerror = rj;
84+
loadingState = resourceEl['_p'] = new Promise((resolve, reject) => {
85+
resourceEl.onload = resolve;
86+
resourceEl.onerror = reject;
9387
});
94-
loadingState.then(
95-
setStatus.bind(loadingState, LOADED),
96-
setStatus.bind(loadingState, ERRORED),
97-
);
9888
// Save this resource element so we can bailout if it is used again
9989
resourceMap.set(href, resourceEl);
10090
}

packages/react-dom-bindings/src/server/fizz-instruction-set/ReactDOMFizzInstructionSetShared.js

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -8,8 +8,6 @@ export const SUSPENSE_START_DATA = '$';
88
export const SUSPENSE_END_DATA = '/$';
99
export const SUSPENSE_PENDING_START_DATA = '$?';
1010
export const SUSPENSE_FALLBACK_START_DATA = '$!';
11-
export const LOADED = 'l';
12-
export const ERRORED = 'e';
1311

1412
// TODO: Symbols that are referenced outside this module use dynamic accessor
1513
// notation instead of dot notation to prevent Closure's advanced compilation

packages/react-dom/src/__tests__/ReactDOMFloat-test.js

Lines changed: 59 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2676,6 +2676,65 @@ body {
26762676
);
26772677
});
26782678

2679+
it('can delay commit until css resources load', async () => {
2680+
const root = ReactDOMClient.createRoot(container);
2681+
expect(getMeaningfulChildren(container)).toBe(undefined);
2682+
React.startTransition(() => {
2683+
root.render(
2684+
<>
2685+
<link
2686+
rel="stylesheet"
2687+
href="foo"
2688+
precedence="default"
2689+
data-suspensey={true}
2690+
/>
2691+
<div>hello</div>
2692+
</>,
2693+
);
2694+
});
2695+
await waitForAll([]);
2696+
expect(getMeaningfulChildren(container)).toBe(undefined);
2697+
expect(getMeaningfulChildren(document.head)).toEqual(
2698+
<link rel="preload" as="style" href="foo" />,
2699+
);
2700+
2701+
const preload = document.querySelector('link[rel="preload"][as="style"]');
2702+
const loadEvent = document.createEvent('Events');
2703+
loadEvent.initEvent('load', true, true);
2704+
preload.dispatchEvent(loadEvent);
2705+
2706+
// We expect that the stylesheet is inserted now but the commit has not happened yet.
2707+
expect(getMeaningfulChildren(container)).toBe(undefined);
2708+
expect(getMeaningfulChildren(document.head)).toEqual([
2709+
<link
2710+
rel="stylesheet"
2711+
href="foo"
2712+
data-precedence="default"
2713+
data-suspensey="true"
2714+
/>,
2715+
<link rel="preload" as="style" href="foo" />,
2716+
]);
2717+
2718+
const stylesheet = document.querySelector(
2719+
'link[rel="stylesheet"][data-precedence]',
2720+
);
2721+
const loadEvent2 = document.createEvent('Events');
2722+
loadEvent2.initEvent('load', true, true);
2723+
stylesheet.dispatchEvent(loadEvent2);
2724+
2725+
// We expect that the commit finishes synchronously after the stylesheet loads.
2726+
expect(getMeaningfulChildren(container)).toEqual(<div>hello</div>);
2727+
expect(getMeaningfulChildren(document.head)).toEqual([
2728+
<link
2729+
rel="stylesheet"
2730+
href="foo"
2731+
data-precedence="default"
2732+
data-suspensey="true"
2733+
/>,
2734+
<link rel="preload" as="style" href="foo" />,
2735+
]);
2736+
});
2737+
26792738
describe('ReactDOM.prefetchDNS(href)', () => {
26802739
it('creates a dns-prefetch resource when called', async () => {
26812740
function App({url}) {

packages/react-noop-renderer/src/createReactNoop.js

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -578,6 +578,12 @@ function createReactNoop(reconciler: Function, useMutation: boolean) {
578578
return type === 'suspensey-thing' && typeof props.src === 'string';
579579
},
580580

581+
mayResourceSuspendCommit(resource: mixed): boolean {
582+
throw new Error(
583+
'Resources are not implemented for React Noop yet. This method should not be called',
584+
);
585+
},
586+
581587
preloadInstance(type: string, props: Props): boolean {
582588
if (type !== 'suspensey-thing' || typeof props.src !== 'string') {
583589
throw new Error('Attempted to preload unexpected instance: ' + type);
@@ -608,8 +614,21 @@ function createReactNoop(reconciler: Function, useMutation: boolean) {
608614
}
609615
},
610616

617+
preloadResource(resource: mixed): boolean {
618+
throw new Error(
619+
'Resources are not implemented for React Noop yet. This method should not be called',
620+
);
621+
},
622+
611623
startSuspendingCommit,
612624
suspendInstance,
625+
626+
suspendResource(resource: mixed): void {
627+
throw new Error(
628+
'Resources are not implemented for React Noop yet. This method should not be called',
629+
);
630+
},
631+
613632
waitForCommitToBeReady,
614633

615634
prepareRendererToRender() {},

packages/react-reconciler/src/ReactFiberCommitWork.js

Lines changed: 62 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -160,6 +160,7 @@ import {
160160
unmountHoistable,
161161
prepareToCommitHoistables,
162162
suspendInstance,
163+
suspendResource,
163164
} from './ReactFiberHostConfig';
164165
import {
165166
captureCommitPhaseError,
@@ -4064,23 +4065,72 @@ export function commitPassiveUnmountEffects(finishedWork: Fiber): void {
40644065
resetCurrentDebugFiberInDEV();
40654066
}
40664067

4067-
export function recursivelyAccumulateSuspenseyCommit(parentFiber: Fiber): void {
4068+
export function accumulateSuspenseyCommit(finishedWork: Fiber): void {
4069+
accumulateSuspenseyCommitOnFiber(finishedWork);
4070+
}
4071+
4072+
function recursivelyAccumulateSuspenseyCommit(parentFiber: Fiber): void {
40684073
if (parentFiber.subtreeFlags & SuspenseyCommit) {
40694074
let child = parentFiber.child;
40704075
while (child !== null) {
4071-
recursivelyAccumulateSuspenseyCommit(child);
4072-
switch (child.tag) {
4073-
case HostComponent:
4074-
case HostHoistable: {
4075-
if (child.flags & SuspenseyCommit) {
4076-
const type = child.type;
4077-
const props = child.memoizedProps;
4078-
suspendInstance(type, props);
4079-
}
4080-
break;
4076+
accumulateSuspenseyCommitOnFiber(child);
4077+
child = child.sibling;
4078+
}
4079+
}
4080+
}
4081+
4082+
function accumulateSuspenseyCommitOnFiber(fiber: Fiber) {
4083+
switch (fiber.tag) {
4084+
case HostHoistable: {
4085+
recursivelyAccumulateSuspenseyCommit(fiber);
4086+
if (fiber.flags & SuspenseyCommit) {
4087+
if (fiber.memoizedState !== null) {
4088+
suspendResource(
4089+
// This should always be set by visiting HostRoot first
4090+
(currentHoistableRoot: any),
4091+
fiber.memoizedState,
4092+
fiber.memoizedProps,
4093+
);
4094+
} else {
4095+
const type = fiber.type;
4096+
const props = fiber.memoizedProps;
4097+
suspendInstance(type, props);
40814098
}
40824099
}
4083-
child = child.sibling;
4100+
break;
4101+
}
4102+
case HostComponent: {
4103+
recursivelyAccumulateSuspenseyCommit(fiber);
4104+
if (fiber.flags & SuspenseyCommit) {
4105+
const type = fiber.type;
4106+
const props = fiber.memoizedProps;
4107+
suspendInstance(type, props);
4108+
}
4109+
break;
4110+
}
4111+
case HostRoot: {
4112+
if (enableFloat && supportsResources) {
4113+
const previousHoistableRoot = currentHoistableRoot;
4114+
currentHoistableRoot = getHoistableRoot(fiber.stateNode.containerInfo);
4115+
4116+
recursivelyAccumulateSuspenseyCommit(fiber);
4117+
currentHoistableRoot = previousHoistableRoot;
4118+
break;
4119+
}
4120+
}
4121+
// eslint-disable-next-line-no-fallthrough
4122+
case HostPortal: {
4123+
if (enableFloat && supportsResources) {
4124+
const previousHoistableRoot = currentHoistableRoot;
4125+
currentHoistableRoot = getHoistableRoot(fiber.stateNode.containerInfo);
4126+
recursivelyAccumulateSuspenseyCommit(fiber);
4127+
currentHoistableRoot = previousHoistableRoot;
4128+
break;
4129+
}
4130+
}
4131+
// eslint-disable-next-line-no-fallthrough
4132+
default: {
4133+
recursivelyAccumulateSuspenseyCommit(fiber);
40844134
}
40854135
}
40864136
}

packages/react-reconciler/src/ReactFiberCompleteWork.js

Lines changed: 12 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -112,6 +112,7 @@ import {
112112
preparePortalMount,
113113
prepareScopeUpdate,
114114
maySuspendCommit,
115+
mayResourceSuspendCommit,
115116
preloadInstance,
116117
} from './ReactFiberHostConfig';
117118
import {
@@ -523,7 +524,17 @@ function preloadInstanceAndSuspendIfNeeded(
523524
renderLanes: Lanes,
524525
) {
525526
// Ask the renderer if this instance should suspend the commit.
526-
if (!maySuspendCommit(type, props)) {
527+
if (workInProgress.memoizedState !== null) {
528+
if (!mayResourceSuspendCommit(workInProgress.memoizedState)) {
529+
// If this flag was set previously, we can remove it. The flag represents
530+
// whether this particular set of props might ever need to suspend. The
531+
// safest thing to do is for shouldSuspendCommit to always return true, but
532+
// if the renderer is reasonably confident that the underlying resource
533+
// won't be evicted, it can return false as a performance optimization.
534+
workInProgress.flags &= ~SuspenseyCommit;
535+
return;
536+
}
537+
} else if (!maySuspendCommit(type, props)) {
527538
// If this flag was set previously, we can remove it. The flag represents
528539
// whether this particular set of props might ever need to suspend. The
529540
// safest thing to do is for maySuspendCommit to always return true, but

packages/react-reconciler/src/ReactFiberHostConfigWithNoResources.js

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -32,3 +32,6 @@ export const mountHoistable = shim;
3232
export const unmountHoistable = shim;
3333
export const createHoistableInstance = shim;
3434
export const prepareToCommitHoistables = shim;
35+
export const mayResourceSuspendCommit = shim;
36+
export const preloadResource = shim;
37+
export const suspendResource = shim;

0 commit comments

Comments
 (0)