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 c612fd1eb941f..0f4a59c77f6f0 100644 --- a/packages/react-native-renderer/src/ReactFabricHostConfig.js +++ b/packages/react-native-renderer/src/ReactFabricHostConfig.js @@ -421,10 +421,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." +}