Skip to content

Commit da0b480

Browse files
committed
[Fiber] render boundary in fallback if it contains a new stylesheet during sync update
When we implemented Suspensey CSS we had a heuristic that if the update was sync we would ignore the loading states of any new stylesheets and just do the commit. But for a stylesheet capability to be useful it needs to reliably prevent FOUC and since the stylesheet api is opt-in through precedence we don't have to maintain backaward compat (old stylesheets do not block commit but then nobody really renders them because of FOUC anyway) This update modifies the logic to put a boundary back into fallback if a sync update would lead to a stylesheet commiting before it loaded.
1 parent 4508873 commit da0b480

File tree

3 files changed

+152
-30
lines changed

3 files changed

+152
-30
lines changed

Diff for: packages/react-dom-bindings/src/client/ReactFiberConfigDOM.js

+15-4
Original file line numberDiff line numberDiff line change
@@ -3158,16 +3158,27 @@ export function preloadInstance(type: Type, props: Props): boolean {
31583158
return true;
31593159
}
31603160

3161-
export function preloadResource(resource: Resource): boolean {
3161+
export function preloadResource(resource: Resource): {
3162+
ready: boolean,
3163+
required: boolean,
3164+
} {
3165+
let ready = true;
3166+
let required = false;
31623167
if (
31633168
resource.type === 'stylesheet' &&
31643169
(resource.state.loading & Settled) === NotLoaded
31653170
) {
31663171
// we have not finished loading the underlying stylesheet yet.
3167-
return false;
3172+
ready = false;
3173+
required = true;
31683174
}
3169-
// Return true to indicate it's already loaded
3170-
return true;
3175+
3176+
// It is important that we return this in tail position to ensure
3177+
// closure will elide the object when building the bundle
3178+
return {
3179+
ready,
3180+
required,
3181+
};
31713182
}
31723183

31733184
type SuspendedState = {

Diff for: packages/react-dom/src/__tests__/ReactDOMFloat-test.js

+125-17
Original file line numberDiff line numberDiff line change
@@ -2946,25 +2946,8 @@ body {
29462946
<link rel="preload" as="style" href="bar" />,
29472947
]);
29482948

2949-
// Try just this and crash all of Jest
29502949
errorStylesheets(['bar']);
29512950

2952-
// // Try this and it fails the test when it shouldn't
2953-
// await act(() => {
2954-
// errorStylesheets(['bar']);
2955-
// });
2956-
2957-
// // Try this there is nothing throwing here which is not really surprising since
2958-
// // the error is bubbling up through some kind of unhandled promise rejection thingy but
2959-
// // still I thought it was worth confirming
2960-
// try {
2961-
// await act(() => {
2962-
// errorStylesheets(['bar']);
2963-
// });
2964-
// } catch (e) {
2965-
// console.log(e);
2966-
// }
2967-
29682951
loadStylesheets(['foo']);
29692952
assertLog(['load stylesheet: foo', 'error stylesheet: bar']);
29702953

@@ -3197,6 +3180,131 @@ body {
31973180
);
31983181
});
31993182

3183+
it('will put a Suspense boundary into fallback if it contains a stylesheet not loaded during a sync update', async () => {
3184+
function App({children}) {
3185+
return (
3186+
<html>
3187+
<body>{children}</body>
3188+
</html>
3189+
);
3190+
}
3191+
const root = ReactDOMClient.createRoot(document);
3192+
3193+
await clientAct(() => {
3194+
root.render(<App />);
3195+
});
3196+
await waitForAll([]);
3197+
3198+
await clientAct(() => {
3199+
root.render(
3200+
<App>
3201+
<Suspense fallback="loading...">
3202+
<div>
3203+
hello
3204+
<link rel="stylesheet" href="foo" precedence="default" />
3205+
</div>
3206+
</Suspense>
3207+
</App>,
3208+
);
3209+
});
3210+
await waitForAll([]);
3211+
3212+
// Although the commit suspended, a preload was inserted.
3213+
expect(getMeaningfulChildren(document)).toEqual(
3214+
<html>
3215+
<head>
3216+
<link rel="preload" href="foo" as="style" />
3217+
</head>
3218+
<body>loading...</body>
3219+
</html>,
3220+
);
3221+
3222+
loadPreloads(['foo']);
3223+
assertLog(['load preload: foo']);
3224+
expect(getMeaningfulChildren(document)).toEqual(
3225+
<html>
3226+
<head>
3227+
<link rel="stylesheet" href="foo" data-precedence="default" />
3228+
<link rel="preload" href="foo" as="style" />
3229+
</head>
3230+
<body>loading...</body>
3231+
</html>,
3232+
);
3233+
3234+
loadStylesheets(['foo']);
3235+
assertLog(['load stylesheet: foo']);
3236+
expect(getMeaningfulChildren(document)).toEqual(
3237+
<html>
3238+
<head>
3239+
<link rel="stylesheet" href="foo" data-precedence="default" />
3240+
<link rel="preload" href="foo" as="style" />
3241+
</head>
3242+
<body>
3243+
<div>hello</div>
3244+
</body>
3245+
</html>,
3246+
);
3247+
3248+
await clientAct(() => {
3249+
root.render(
3250+
<App>
3251+
<Suspense fallback="loading...">
3252+
<div>
3253+
hello
3254+
<link rel="stylesheet" href="foo" precedence="default" />
3255+
<link rel="stylesheet" href="bar" precedence="default" />
3256+
</div>
3257+
</Suspense>
3258+
</App>,
3259+
);
3260+
});
3261+
await waitForAll([]);
3262+
expect(getMeaningfulChildren(document)).toEqual(
3263+
<html>
3264+
<head>
3265+
<link rel="stylesheet" href="foo" data-precedence="default" />
3266+
<link rel="preload" href="foo" as="style" />
3267+
<link rel="preload" href="bar" as="style" />
3268+
</head>
3269+
<body>
3270+
<div style="display: none;">hello</div>loading...
3271+
</body>
3272+
</html>,
3273+
);
3274+
3275+
loadPreloads(['bar']);
3276+
assertLog(['load preload: bar']);
3277+
expect(getMeaningfulChildren(document)).toEqual(
3278+
<html>
3279+
<head>
3280+
<link rel="stylesheet" href="foo" data-precedence="default" />
3281+
<link rel="stylesheet" href="bar" data-precedence="default" />
3282+
<link rel="preload" href="foo" as="style" />
3283+
<link rel="preload" href="bar" as="style" />
3284+
</head>
3285+
<body>
3286+
<div style="display: none;">hello</div>loading...
3287+
</body>
3288+
</html>,
3289+
);
3290+
3291+
loadStylesheets(['bar']);
3292+
assertLog(['load stylesheet: bar']);
3293+
expect(getMeaningfulChildren(document)).toEqual(
3294+
<html>
3295+
<head>
3296+
<link rel="stylesheet" href="foo" data-precedence="default" />
3297+
<link rel="stylesheet" href="bar" data-precedence="default" />
3298+
<link rel="preload" href="foo" as="style" />
3299+
<link rel="preload" href="bar" as="style" />
3300+
</head>
3301+
<body>
3302+
<div style="">hello</div>
3303+
</body>
3304+
</html>,
3305+
);
3306+
});
3307+
32003308
it('can suspend commits on more than one root for the same resource at the same time', async () => {
32013309
document.body.innerHTML = '';
32023310
const container1 = document.createElement('div');

Diff for: packages/react-reconciler/src/ReactFiberCompleteWork.js

+12-9
Original file line numberDiff line numberDiff line change
@@ -588,15 +588,18 @@ function preloadResourceAndSuspendIfNeeded(
588588

589589
workInProgress.flags |= MaySuspendCommit;
590590

591-
const rootRenderLanes = getWorkInProgressRootRenderLanes();
592-
if (!includesOnlyNonUrgentLanes(rootRenderLanes)) {
593-
// This is an urgent render. Don't suspend or show a fallback.
594-
} else {
595-
const isReady = preloadResource(resource);
596-
if (!isReady) {
597-
if (shouldRemainOnPreviousScreen()) {
598-
workInProgress.flags |= ShouldSuspendCommit;
599-
} else {
591+
const {ready, required} = preloadResource(resource);
592+
if (!ready) {
593+
if (shouldRemainOnPreviousScreen()) {
594+
// We can stay on the current screen until the resource is ready
595+
// so we mark this fiber as suspending the commit
596+
workInProgress.flags |= ShouldSuspendCommit;
597+
} else {
598+
const rootRenderLanes = getWorkInProgressRootRenderLanes();
599+
if (!includesOnlyNonUrgentLanes(rootRenderLanes) && required) {
600+
// We can't stay on the current screen so we suspend
601+
// but this resource is required to show the boundary content
602+
// so we suspend the boundary
600603
suspendCommit();
601604
}
602605
}

0 commit comments

Comments
 (0)