From 3fcd91826c87f734d2f12eee9cf257ebf7cccb26 Mon Sep 17 00:00:00 2001 From: Andrew Clark Date: Sat, 18 Mar 2023 21:19:06 -0400 Subject: [PATCH] Check if suspensey instance resolves in immediate task MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit When rendering a suspensey resource that we haven't seen before, it may have loaded in the background while we were rendering. We should yield to the main thread to see if the load event fires in an immediate task. For example, if the resource for a link element has already loaded, its load event will fire in a task right after React yields to the main thread. Because the continuation task is not scheduled until right before React yields, the load event will ping React before it resumes. If this happens, we can resume rendering without showing a fallback. I don't think this matters much for images, because the `completed` property tells us whether the image has loaded, and we currently never block the main thread for more than 5ms at a time (for now — we might increase this in the future). It matters more for stylesheets because the only way to check if it has loaded is by listening for the load event. This is essentially the same trick that `use` does for userspace promises, but a bit simpler because we don't need to replay the host component's begin phase; the work-in-progress fiber already completed, so we can just continue onto the next sibling without any additional work. As part of this change, I split the `shouldSuspendCommit` host config method into separate `maySuspendCommit` and `preloadInstance` methods. Previously `shouldSuspendCommit` was used for both. This raised a question of whether we should preload resources during a synchronous render. My initial instinct was that we shouldn't, because we're going to synchronously block the main thread until the resource is inserted into the DOM, anyway. But I wonder if the browser is able to initiate the preload even while the main thread is blocked. It's probably a micro-optimization either way because most resources will be loaded during transitions, not urgent renders. --- packages/react-art/src/ReactARTHostConfig.js | 7 +- .../src/client/ReactDOMHostConfig.js | 7 +- .../src/ReactFabricHostConfig.js | 6 +- .../src/ReactNativeHostConfig.js | 7 +- .../src/createReactNoop.js | 81 +++++++++-------- .../src/ReactFiberCompleteWork.js | 63 +++++++++---- .../src/ReactFiberThenable.js | 7 +- .../src/ReactFiberWorkLoop.js | 88 +++++++++++++++++-- .../ReactFiberHostContext-test.internal.js | 5 +- .../ReactSuspenseyCommitPhase-test.js | 63 +++++++++++-- .../src/forks/ReactFiberHostConfig.custom.js | 3 +- .../src/ReactTestHostConfig.js | 7 +- scripts/error-codes/codes.json | 5 +- 13 files changed, 271 insertions(+), 78 deletions(-) diff --git a/packages/react-art/src/ReactARTHostConfig.js b/packages/react-art/src/ReactARTHostConfig.js index 754af14dcc342..97a6b421b9f0f 100644 --- a/packages/react-art/src/ReactARTHostConfig.js +++ b/packages/react-art/src/ReactARTHostConfig.js @@ -459,10 +459,15 @@ export function requestPostPaintCallback(callback: (time: number) => void) { // noop } -export function shouldSuspendCommit(type, props) { +export function maySuspendCommit(type, props) { return false; } +export function preloadInstance(type, props) { + // Return true to indicate it's already loaded + return true; +} + export function startSuspendingCommit() {} export function suspendInstance(type, props) {} diff --git a/packages/react-dom-bindings/src/client/ReactDOMHostConfig.js b/packages/react-dom-bindings/src/client/ReactDOMHostConfig.js index fdd4484f714e6..86ddecfbf9790 100644 --- a/packages/react-dom-bindings/src/client/ReactDOMHostConfig.js +++ b/packages/react-dom-bindings/src/client/ReactDOMHostConfig.js @@ -1609,10 +1609,15 @@ export function requestPostPaintCallback(callback: (time: number) => void) { }); } -export function shouldSuspendCommit(type: Type, props: Props): boolean { +export function maySuspendCommit(type: Type, props: Props): boolean { return false; } +export function preloadInstance(type: Type, props: Props): boolean { + // Return true to indicate it's already loaded + return true; +} + export function startSuspendingCommit(): void {} export function suspendInstance(type: Type, props: Props): void {} diff --git a/packages/react-native-renderer/src/ReactFabricHostConfig.js b/packages/react-native-renderer/src/ReactFabricHostConfig.js index 2c2d5c598b366..5a3fe71fa26c1 100644 --- a/packages/react-native-renderer/src/ReactFabricHostConfig.js +++ b/packages/react-native-renderer/src/ReactFabricHostConfig.js @@ -414,10 +414,14 @@ export function requestPostPaintCallback(callback: (time: number) => void) { // noop } -export function shouldSuspendCommit(type: Type, props: Props): boolean { +export function maySuspendCommit(type: Type, props: Props): boolean { return false; } +export function preloadInstance(type: Type, props: Props): boolean { + return true; +} + export function startSuspendingCommit(): void {} export function suspendInstance(type: Type, props: Props): void {} diff --git a/packages/react-native-renderer/src/ReactNativeHostConfig.js b/packages/react-native-renderer/src/ReactNativeHostConfig.js index f23f73b00de5f..aeed0c01c069c 100644 --- a/packages/react-native-renderer/src/ReactNativeHostConfig.js +++ b/packages/react-native-renderer/src/ReactNativeHostConfig.js @@ -522,10 +522,15 @@ export function requestPostPaintCallback(callback: (time: number) => void) { // noop } -export function shouldSuspendCommit(type: Type, props: Props): boolean { +export function maySuspendCommit(type: Type, props: Props): boolean { return false; } +export function preloadInstance(type: Type, props: Props): boolean { + // Return true to indicate it's already loaded + return true; +} + export function startSuspendingCommit(): void {} export function suspendInstance(type: Type, props: Props): void {} diff --git a/packages/react-noop-renderer/src/createReactNoop.js b/packages/react-noop-renderer/src/createReactNoop.js index c6856ac539ac9..15d5d40d05462 100644 --- a/packages/react-noop-renderer/src/createReactNoop.js +++ b/packages/react-noop-renderer/src/createReactNoop.js @@ -312,7 +312,9 @@ function createReactNoop(reconciler: Function, useMutation: boolean) { if (record === undefined) { throw new Error('Could not find record for key.'); } - if (record.status === 'pending') { + if (record.status === 'fulfilled') { + // Already loaded. + } else if (record.status === 'pending') { if (suspenseyCommitSubscription === null) { suspenseyCommitSubscription = { pendingCount: 1, @@ -321,20 +323,19 @@ function createReactNoop(reconciler: Function, useMutation: boolean) { } else { suspenseyCommitSubscription.pendingCount++; } + // Stash the subscription on the record. In `resolveSuspenseyThing`, + // we'll use this fire the commit once all the things have loaded. + if (record.subscriptions === null) { + record.subscriptions = []; + } + record.subscriptions.push(suspenseyCommitSubscription); } - // Stash the subscription on the record. In `resolveSuspenseyThing`, - // we'll use this fire the commit once all the things have loaded. - if (record.subscriptions === null) { - record.subscriptions = []; - } - record.subscriptions.push(suspenseyCommitSubscription); } else { throw new Error( 'Did not expect this host component to be visited when suspending ' + 'the commit. Did you check the SuspendCommit flag?', ); } - return suspenseyCommitSubscription; } function waitForCommitToBeReady(): @@ -569,38 +570,42 @@ function createReactNoop(reconciler: Function, useMutation: boolean) { callback(endTime); }, - shouldSuspendCommit(type: string, props: Props): boolean { - if (type === 'suspensey-thing' && typeof props.src === 'string') { - if (suspenseyThingCache === null) { - suspenseyThingCache = new Map(); - } - const record = suspenseyThingCache.get(props.src); - if (record === undefined) { - const newRecord: SuspenseyThingRecord = { - status: 'pending', - subscriptions: null, - }; - suspenseyThingCache.set(props.src, newRecord); - const onLoadStart = props.onLoadStart; - if (typeof onLoadStart === 'function') { - onLoadStart(); - } - return props.src; - } else { - if (record.status === 'pending') { - // The resource was already requested, but it hasn't finished - // loading yet. - return true; - } else { - // The resource has already loaded. If the renderer is confident that - // the resource will still be cached by the time the render commits, - // then it can return false, like we do here. - return false; - } + maySuspendCommit(type: string, props: Props): boolean { + // Asks whether it's possible for this combination of type and props + // to ever need to suspend. This is different from asking whether it's + // currently ready because even if it's ready now, it might get purged + // from the cache later. + return type === 'suspensey-thing' && typeof props.src === 'string'; + }, + + preloadInstance(type: string, props: Props): boolean { + if (type !== 'suspensey-thing' || typeof props.src !== 'string') { + throw new Error('Attempted to preload unexpected instance: ' + type); + } + + // In addition to preloading an instance, this method asks whether the + // instance is ready to be committed. If it's not, React may yield to the + // main thread and ask again. It's possible a load event will fire in + // between, in which case we can avoid showing a fallback. + if (suspenseyThingCache === null) { + suspenseyThingCache = new Map(); + } + const record = suspenseyThingCache.get(props.src); + if (record === undefined) { + const newRecord: SuspenseyThingRecord = { + status: 'pending', + subscriptions: null, + }; + suspenseyThingCache.set(props.src, newRecord); + const onLoadStart = props.onLoadStart; + if (typeof onLoadStart === 'function') { + onLoadStart(); } + return false; + } else { + // If this is false, React will trigger a fallback, if needed. + return record.status === 'fulfilled'; } - // Don't need to suspend. - return false; }, startSuspendingCommit, diff --git a/packages/react-reconciler/src/ReactFiberCompleteWork.js b/packages/react-reconciler/src/ReactFiberCompleteWork.js index b606632b8e806..73a9a455a617f 100644 --- a/packages/react-reconciler/src/ReactFiberCompleteWork.js +++ b/packages/react-reconciler/src/ReactFiberCompleteWork.js @@ -111,7 +111,8 @@ import { finalizeContainerChildren, preparePortalMount, prepareScopeUpdate, - shouldSuspendCommit, + maySuspendCommit, + preloadInstance, } from './ReactFiberHostConfig'; import { getRootHostContainer, @@ -434,8 +435,6 @@ function updateHostComponent( // Even better would be if children weren't special cased at all tho. const instance: Instance = workInProgress.stateNode; - suspendHostCommitIfNeeded(workInProgress, type, newProps, renderLanes); - const currentHostContext = getHostContext(); // TODO: Experiencing an error where oldProps is null. Suggests a host // component is hitting the resume path. Figure out why. Possibly @@ -495,8 +494,6 @@ function updateHostComponent( recyclableInstance, ); - suspendHostCommitIfNeeded(workInProgress, type, newProps, renderLanes); - if ( finalizeInitialChildren(newInstance, type, newProps, currentHostContext) ) { @@ -519,17 +516,17 @@ function updateHostComponent( // not created until the complete phase. For our existing use cases, host nodes // that suspend don't have children, so it doesn't matter. But that might not // always be true in the future. -function suspendHostCommitIfNeeded( +function preloadInstanceAndSuspendIfNeeded( workInProgress: Fiber, type: Type, props: Props, renderLanes: Lanes, ) { // Ask the renderer if this instance should suspend the commit. - if (!shouldSuspendCommit(type, props)) { + if (!maySuspendCommit(type, props)) { // If this flag was set previously, we can remove it. The flag represents // whether this particular set of props might ever need to suspend. The - // safest thing to do is for shouldSuspendCommit to always return true, but + // safest thing to do is for maySuspendCommit to always return true, but // if the renderer is reasonably confident that the underlying resource // won't be evicted, it can return false as a performance optimization. workInProgress.flags &= ~SuspenseyCommit; @@ -552,16 +549,24 @@ function suspendHostCommitIfNeeded( // TODO: We may decide to expose a way to force a fallback even during a // sync update. if (!includesOnlyNonUrgentLanes(renderLanes)) { - // This is an urgent render. Never suspend or trigger a fallback. + // This is an urgent render. Don't suspend or show a fallback. Also, + // there's no need to preload, because we're going to commit this + // synchronously anyway. + // TODO: Could there be benefit to preloading even during a synchronous + // render? The main thread will be blocked until the commit phase, but + // maybe the browser would be able to start loading off thread anyway? + // Likely a micro-optimization either way because typically new content + // is loaded during a transition, not an urgent render. } else { - // Need to decide whether to activate the nearest fallback or to continue - // rendering and suspend right before the commit phase. - if (shouldRemainOnPreviousScreen()) { - // It's OK to block the commit. Don't show a fallback. - } else { - // We shouldn't block the commit. Activate a fallback at the nearest - // Suspense boundary. - suspendCommit(); + // Preload the instance + const isReady = preloadInstance(type, props); + if (!isReady) { + if (shouldRemainOnPreviousScreen()) { + // It's OK to suspend. Continue rendering. + } else { + // Trigger a fallback rather than block the render. + suspendCommit(); + } } } } @@ -1054,6 +1059,17 @@ function completeWork( ); } bubbleProperties(workInProgress); + + // This must come at the very end of the complete phase, because it might + // throw to suspend, and if the resource immediately loads, the work loop + // will resume rendering as if the work-in-progress completed. So it must + // fully complete. + preloadInstanceAndSuspendIfNeeded( + workInProgress, + workInProgress.type, + workInProgress.pendingProps, + renderLanes, + ); return null; } } @@ -1192,14 +1208,23 @@ function completeWork( } } - suspendHostCommitIfNeeded(workInProgress, type, newProps, renderLanes); - if (workInProgress.ref !== null) { // If there is a ref on a host node we need to schedule a callback markRef(workInProgress); } } bubbleProperties(workInProgress); + + // This must come at the very end of the complete phase, because it might + // throw to suspend, and if the resource immediately loads, the work loop + // will resume rendering as if the work-in-progress completed. So it must + // fully complete. + preloadInstanceAndSuspendIfNeeded( + workInProgress, + type, + newProps, + renderLanes, + ); return null; } case HostText: { diff --git a/packages/react-reconciler/src/ReactFiberThenable.js b/packages/react-reconciler/src/ReactFiberThenable.js index 5b2501f33fe58..d759e692dd035 100644 --- a/packages/react-reconciler/src/ReactFiberThenable.js +++ b/packages/react-reconciler/src/ReactFiberThenable.js @@ -31,6 +31,11 @@ export const SuspenseException: mixed = new Error( "call the promise's `.catch` method and pass the result to `use`", ); +export const SuspenseyCommitException: mixed = new Error( + '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.", +); + // This is a noop thenable that we use to trigger a fallback in throwException. // TODO: It would be better to refactor throwException into multiple functions // so we can trigger a fallback directly without having to check the type. But @@ -151,7 +156,7 @@ export function suspendCommit(): void { // noopSuspenseyCommitThenable through to throwException. // TODO: Factor the thenable check out of throwException suspendedThenable = noopSuspenseyCommitThenable; - throw SuspenseException; + throw SuspenseyCommitException; } // This is used to track the actual thenable that suspended so it can be diff --git a/packages/react-reconciler/src/ReactFiberWorkLoop.js b/packages/react-reconciler/src/ReactFiberWorkLoop.js index 241c068bb35d1..6c3b01e6b15f2 100644 --- a/packages/react-reconciler/src/ReactFiberWorkLoop.js +++ b/packages/react-reconciler/src/ReactFiberWorkLoop.js @@ -86,6 +86,7 @@ import { resetRendererAfterRender, startSuspendingCommit, waitForCommitToBeReady, + preloadInstance, } from './ReactFiberHostConfig'; import { @@ -114,6 +115,9 @@ import { MemoComponent, SimpleMemoComponent, Profiler, + HostComponent, + HostHoistable, + HostSingleton, } from './ReactWorkTags'; import {ConcurrentRoot, LegacyRoot} from './ReactRootTags'; import type {Flags} from './ReactFiberFlags'; @@ -273,6 +277,7 @@ import { import {processTransitionCallbacks} from './ReactFiberTracingMarkerComponent'; import { SuspenseException, + SuspenseyCommitException, getSuspendedThenable, isThenableResolved, } from './ReactFiberThenable'; @@ -321,14 +326,16 @@ let workInProgress: Fiber | null = null; // The lanes we're rendering let workInProgressRootRenderLanes: Lanes = NoLanes; -opaque type SuspendedReason = 0 | 1 | 2 | 3 | 4 | 5 | 6; +opaque type SuspendedReason = 0 | 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8; const NotSuspended: SuspendedReason = 0; const SuspendedOnError: SuspendedReason = 1; const SuspendedOnData: SuspendedReason = 2; const SuspendedOnImmediate: SuspendedReason = 3; -const SuspendedOnDeprecatedThrowPromise: SuspendedReason = 4; -const SuspendedAndReadyToContinue: SuspendedReason = 5; -const SuspendedOnHydration: SuspendedReason = 6; +const SuspendedOnInstance: SuspendedReason = 4; +const SuspendedOnInstanceAndReadyToContinue: SuspendedReason = 5; +const SuspendedOnDeprecatedThrowPromise: SuspendedReason = 6; +const SuspendedAndReadyToContinue: SuspendedReason = 7; +const SuspendedOnHydration: SuspendedReason = 8; // When this is true, the work-in-progress fiber just suspended (or errored) and // we've yet to unwind the stack. In some cases, we may yield to the main thread @@ -1871,6 +1878,9 @@ function handleThrow(root: FiberRoot, thrownValue: any): void { // immediately resolved (i.e. in a microtask). Otherwise, trigger the // nearest Suspense fallback. SuspendedOnImmediate; + } else if (thrownValue === SuspenseyCommitException) { + thrownValue = getSuspendedThenable(); + workInProgressSuspendedReason = SuspendedOnInstance; } else if (thrownValue === SelectiveHydrationException) { // An update flowed into a dehydrated boundary. Before we can apply the // update, we need to finish hydrating. Interrupt the work-in-progress @@ -1938,6 +1948,13 @@ function handleThrow(root: FiberRoot, thrownValue: any): void { ); break; } + case SuspendedOnInstance: { + // This is conceptually like a suspend, but it's not associated with + // a particular wakeable. It's associated with a host resource (e.g. + // a CSS file or an image) that hasn't loaded yet. DevTools doesn't + // handle this currently. + break; + } case SuspendedOnHydration: { // This is conceptually like a suspend, but it's not associated with // a particular wakeable. DevTools doesn't seem to care about this case, @@ -2263,7 +2280,7 @@ function renderRootConcurrent(root: FiberRoot, lanes: Lanes) { // replay the suspended component. const unitOfWork = workInProgress; const thrownValue = workInProgressThrownValue; - switch (workInProgressSuspendedReason) { + resumeOrUnwind: switch (workInProgressSuspendedReason) { case SuspendedOnError: { // Unwind then continue with the normal work loop. workInProgressSuspendedReason = NotSuspended; @@ -2310,6 +2327,11 @@ function renderRootConcurrent(root: FiberRoot, lanes: Lanes) { workInProgressSuspendedReason = SuspendedAndReadyToContinue; break outer; } + case SuspendedOnInstance: { + workInProgressSuspendedReason = + SuspendedOnInstanceAndReadyToContinue; + break outer; + } case SuspendedAndReadyToContinue: { const thenable: Thenable = (thrownValue: any); if (isThenableResolved(thenable)) { @@ -2325,6 +2347,62 @@ function renderRootConcurrent(root: FiberRoot, lanes: Lanes) { } break; } + case SuspendedOnInstanceAndReadyToContinue: { + switch (workInProgress.tag) { + case HostComponent: + case HostHoistable: + case HostSingleton: { + // Before unwinding the stack, check one more time if the + // instance is ready. It may have loaded when React yielded to + // the main thread. + + // Assigning this to a constant so Flow knows the binding won't + // be mutated by `preloadInstance`. + const hostFiber = workInProgress; + const type = hostFiber.type; + const props = hostFiber.pendingProps; + const isReady = preloadInstance(type, props); + if (isReady) { + // The data resolved. Resume the work loop as if nothing + // suspended. Unlike when a user component suspends, we don't + // have to replay anything because the host fiber + // already completed. + workInProgressSuspendedReason = NotSuspended; + workInProgressThrownValue = null; + const sibling = hostFiber.sibling; + if (sibling !== null) { + workInProgress = sibling; + } else { + const returnFiber = hostFiber.return; + if (returnFiber !== null) { + workInProgress = returnFiber; + completeUnitOfWork(returnFiber); + } else { + workInProgress = null; + } + } + break resumeOrUnwind; + } + break; + } + default: { + // This will fail gracefully but it's not correct, so log a + // warning in dev. + if (__DEV__) { + console.error( + 'Unexpected type of fiber triggered a suspensey commit. ' + + 'This is a bug in React.', + ); + } + break; + } + } + // Otherwise, unwind then continue with the normal work loop. + workInProgressSuspendedReason = NotSuspended; + workInProgressThrownValue = null; + unwindSuspendedUnitOfWork(unitOfWork, thrownValue); + break; + } case SuspendedOnDeprecatedThrowPromise: { // Suspended by an old implementation that uses the `throw promise` // pattern. The newer replaying behavior can cause subtle issues diff --git a/packages/react-reconciler/src/__tests__/ReactFiberHostContext-test.internal.js b/packages/react-reconciler/src/__tests__/ReactFiberHostContext-test.internal.js index 10e764fd1d22b..a4c32090bfd8a 100644 --- a/packages/react-reconciler/src/__tests__/ReactFiberHostContext-test.internal.js +++ b/packages/react-reconciler/src/__tests__/ReactFiberHostContext-test.internal.js @@ -71,9 +71,12 @@ describe('ReactFiberHostContext', () => { return DefaultEventPriority; }, requestPostPaintCallback: function () {}, - shouldSuspendCommit(type, props) { + maySuspendCommit(type, props) { return false; }, + preloadInstance(type, props) { + return true; + }, startSuspendingCommit() {}, suspendInstance(type, props) {}, waitForCommitToBeReady() { diff --git a/packages/react-reconciler/src/__tests__/ReactSuspenseyCommitPhase-test.js b/packages/react-reconciler/src/__tests__/ReactSuspenseyCommitPhase-test.js index 19260ebff93ab..4acdc339e04c8 100644 --- a/packages/react-reconciler/src/__tests__/ReactSuspenseyCommitPhase-test.js +++ b/packages/react-reconciler/src/__tests__/ReactSuspenseyCommitPhase-test.js @@ -8,6 +8,7 @@ let SuspenseList; let Scheduler; let act; let assertLog; +let waitForPaint; describe('ReactSuspenseyCommitPhase', () => { beforeEach(() => { @@ -28,6 +29,7 @@ describe('ReactSuspenseyCommitPhase', () => { const InternalTestUtils = require('internal-test-utils'); act = InternalTestUtils.act; assertLog = InternalTestUtils.assertLog; + waitForPaint = InternalTestUtils.waitForPaint; }); function Text({text}) { @@ -108,12 +110,13 @@ describe('ReactSuspenseyCommitPhase', () => { , ); }); - // NOTE: `shouldSuspendCommit` is called even during synchronous renders - // because if this node is ever hidden, then revealed again, we want to know - // whether it's capable of suspending the commit. We track this using a - // fiber flag. - assertLog(['Image requested [A]']); - expect(getSuspenseyThingStatus('A')).toBe('pending'); + // We intentionally don't preload during an urgent update because the + // resource will be inserted synchronously, anyway. + // TODO: Maybe we should, though? Could be that the browser is able to start + // the preload in background even though the main thread is blocked. Likely + // a micro-optimization either way because typically new content is loaded + // during a transition, not an urgent render. + expect(getSuspenseyThingStatus('A')).toBe(null); expect(root).toMatchRenderedOutput(); }); @@ -228,4 +231,52 @@ describe('ReactSuspenseyCommitPhase', () => { , ); }); + + test('avoid triggering a fallback if resource loads immediately', async () => { + const root = ReactNoop.createRoot(); + await act(async () => { + startTransition(() => { + // Intentionally rendering s in a variety of tree + // positions to test that the work loop resumes correctly in each case. + root.render( + }> + Scheduler.log('Request [A]')}> + Scheduler.log('Request [B]')} + /> + + Scheduler.log('Request [C]')} + /> + , + ); + }); + // React will yield right after the resource suspends. + // TODO: The child is preloaded first because we preload in the complete + // phase. Ideally it should be in the begin phase, but we currently don't + // create the instance until complete. However, it's unclear if we even + // need the instance for preloading. So we should probably move this to + // the begin phase. + await waitForPaint(['Request [B]']); + // Resolve in an immediate task. This could happen if the resource is + // already loaded into the cache. + resolveSuspenseyThing('B'); + await waitForPaint(['Request [A]']); + resolveSuspenseyThing('A'); + await waitForPaint(['Request [C]']); + resolveSuspenseyThing('C'); + }); + expect(root).toMatchRenderedOutput( + <> + + + + + , + ); + }); }); diff --git a/packages/react-reconciler/src/forks/ReactFiberHostConfig.custom.js b/packages/react-reconciler/src/forks/ReactFiberHostConfig.custom.js index d947045297c86..1360e4e0c634c 100644 --- a/packages/react-reconciler/src/forks/ReactFiberHostConfig.custom.js +++ b/packages/react-reconciler/src/forks/ReactFiberHostConfig.custom.js @@ -68,7 +68,8 @@ export const getInstanceFromScope = $$$hostConfig.getInstanceFromScope; export const getCurrentEventPriority = $$$hostConfig.getCurrentEventPriority; export const detachDeletedInstance = $$$hostConfig.detachDeletedInstance; export const requestPostPaintCallback = $$$hostConfig.requestPostPaintCallback; -export const shouldSuspendCommit = $$$hostConfig.shouldSuspendCommit; +export const maySuspendCommit = $$$hostConfig.maySuspendCommit; +export const preloadInstance = $$$hostConfig.preloadInstance; export const startSuspendingCommit = $$$hostConfig.startSuspendingCommit; export const suspendInstance = $$$hostConfig.suspendInstance; export const waitForCommitToBeReady = $$$hostConfig.waitForCommitToBeReady; diff --git a/packages/react-test-renderer/src/ReactTestHostConfig.js b/packages/react-test-renderer/src/ReactTestHostConfig.js index 3c6ada072e7b4..e306f9e1a0b03 100644 --- a/packages/react-test-renderer/src/ReactTestHostConfig.js +++ b/packages/react-test-renderer/src/ReactTestHostConfig.js @@ -324,10 +324,15 @@ export function requestPostPaintCallback(callback: (time: number) => void) { // noop } -export function shouldSuspendCommit(type: Type, props: Props): boolean { +export function maySuspendCommit(type: Type, props: Props): boolean { return false; } +export function preloadInstance(type: Type, props: Props): boolean { + // Return true to indicate it's already loaded + return true; +} + export function startSuspendingCommit(): void {} export function suspendInstance(type: Type, props: Props): void {} diff --git a/scripts/error-codes/codes.json b/scripts/error-codes/codes.json index 543893c7937fc..2427b1284d843 100644 --- a/scripts/error-codes/codes.json +++ b/scripts/error-codes/codes.json @@ -458,5 +458,6 @@ "470": "Only global symbols received from Symbol.for(...) can be passed to Server Functions. The symbol Symbol.for(%s) cannot be found among global symbols.", "471": "BigInt (%s) is not yet supported as an argument to a Server Function.", "472": "Type %s is not supported as an argument to a Server Function.", - "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." -} \ No newline at end of file + "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.", + "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." +}