Skip to content

Commit 85cdbf8

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 5e9d061 commit 85cdbf8

13 files changed

+495
-81
lines changed

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

Lines changed: 326 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
@@ -110,6 +110,7 @@ import {
110110
preparePortalMount,
111111
prepareScopeUpdate,
112112
maySuspendCommit,
113+
mayResourceSuspendCommit,
113114
preloadInstance,
114115
} from './ReactFiberHostConfig';
115116
import {
@@ -521,7 +522,17 @@ function preloadInstanceAndSuspendIfNeeded(
521522
renderLanes: Lanes,
522523
) {
523524
// Ask the renderer if this instance should suspend the commit.
524-
if (!maySuspendCommit(type, props)) {
525+
if (workInProgress.memoizedState !== null) {
526+
if (!mayResourceSuspendCommit(workInProgress.memoizedState)) {
527+
// If this flag was set previously, we can remove it. The flag represents
528+
// whether this particular set of props might ever need to suspend. The
529+
// safest thing to do is for shouldSuspendCommit to always return true, but
530+
// if the renderer is reasonably confident that the underlying resource
531+
// won't be evicted, it can return false as a performance optimization.
532+
workInProgress.flags &= ~SuspenseyCommit;
533+
return;
534+
}
535+
} else if (!maySuspendCommit(type, props)) {
525536
// If this flag was set previously, we can remove it. The flag represents
526537
// whether this particular set of props might ever need to suspend. The
527538
// 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;

packages/react-reconciler/src/ReactFiberWorkLoop.js

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -209,7 +209,7 @@ import {
209209
invokePassiveEffectMountInDEV,
210210
invokeLayoutEffectUnmountInDEV,
211211
invokePassiveEffectUnmountInDEV,
212-
recursivelyAccumulateSuspenseyCommit,
212+
accumulateSuspenseyCommit,
213213
} from './ReactFiberCommitWork';
214214
import {enqueueUpdate} from './ReactFiberClassUpdateQueue';
215215
import {resetContextDependencies} from './ReactFiberNewContext';
@@ -1444,7 +1444,7 @@ function commitRootWhenReady(
14441444
// the suspensey resources. The renderer is responsible for accumulating
14451445
// all the load events. This all happens in a single synchronous
14461446
// transaction, so it track state in its own module scope.
1447-
recursivelyAccumulateSuspenseyCommit(finishedWork);
1447+
accumulateSuspenseyCommit(finishedWork);
14481448
// At the end, ask the renderer if it's ready to commit, or if we should
14491449
// suspend. If it's not ready, it will return a callback to subscribe to
14501450
// a ready event.

packages/react-reconciler/src/forks/ReactFiberHostConfig.custom.js

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -220,6 +220,9 @@ export const unmountHoistable = $$$hostConfig.unmountHoistable;
220220
export const createHoistableInstance = $$$hostConfig.createHoistableInstance;
221221
export const prepareToCommitHoistables =
222222
$$$hostConfig.prepareToCommitHoistables;
223+
export const mayResourceSuspendCommit = $$$hostConfig.mayResourceSuspendCommit;
224+
export const preloadResource = $$$hostConfig.preloadResource;
225+
export const suspendResource = $$$hostConfig.suspendResource;
223226

224227
// -------------------
225228
// Singletons

scripts/error-codes/codes.json

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -459,5 +459,6 @@
459459
"471": "BigInt (%s) is not yet supported as an argument to a Server Function.",
460460
"472": "Type %s is not supported as an argument to a Server Function.",
461461
"473": "React doesn't accept base64 encoded file uploads because we don't except form data passed from a browser to ever encode data that way. If that's the wrong assumption, we can easily fix it.",
462-
"474": "Suspense Exception: This is not a real error, and should not leak into userspace. If you're seeing this, it's likely a bug in React."
462+
"474": "Suspense Exception: This is not a real error, and should not leak into userspace. If you're seeing this, it's likely a bug in React.",
463+
"475": "Internal React Error: suspendedState null when it was expected to exists. Please report this as a React bug."
463464
}

0 commit comments

Comments
 (0)