From 2fe17df4b8fe404b16bde5bbe55cfec2fb98a4d0 Mon Sep 17 00:00:00 2001 From: Rick Hanlon Date: Fri, 30 Sep 2022 12:06:11 -0400 Subject: [PATCH] Revert "experimental_use(promise) for SSR (#25214)" This reverts commit a0abef3e --- .../src/__tests__/ReactDOMFizzServer-test.js | 260 ------------------ packages/react-server/src/ReactFizzHooks.js | 118 +------- packages/react-server/src/ReactFizzServer.js | 127 ++------- .../react-server/src/ReactFizzWakeable.js | 106 ------- packages/react-server/src/ReactFlightHooks.js | 11 +- .../react-server/src/ReactFlightWakeable.js | 4 +- 6 files changed, 38 insertions(+), 588 deletions(-) diff --git a/packages/react-dom/src/__tests__/ReactDOMFizzServer-test.js b/packages/react-dom/src/__tests__/ReactDOMFizzServer-test.js index 1153ac42498dc..a54bf9c35e806 100644 --- a/packages/react-dom/src/__tests__/ReactDOMFizzServer-test.js +++ b/packages/react-dom/src/__tests__/ReactDOMFizzServer-test.js @@ -19,7 +19,6 @@ let Suspense; let SuspenseList; let useSyncExternalStore; let useSyncExternalStoreWithSelector; -let use; let PropTypes; let textCache; let window; @@ -43,7 +42,6 @@ describe('ReactDOMFizzServer', () => { Suspense = React.Suspense; if (gate(flags => flags.enableSuspenseList)) { SuspenseList = React.SuspenseList; - use = React.experimental_use; } PropTypes = require('prop-types'); @@ -5054,264 +5052,6 @@ describe('ReactDOMFizzServer', () => { console.error = originalConsoleError; } }); - - // @gate enableUseHook - it('basic use(promise)', async () => { - const promiseA = Promise.resolve('A'); - const promiseB = Promise.resolve('B'); - const promiseC = Promise.resolve('C'); - - function Async() { - return use(promiseA) + use(promiseB) + use(promiseC); - } - - function App() { - return ( - - - - ); - } - - await act(async () => { - const {pipe} = ReactDOMFizzServer.renderToPipeableStream(); - pipe(writable); - }); - - // TODO: The `act` implementation in this file doesn't unwrap microtasks - // automatically. We can't use the same `act` we use for Fiber tests - // because that relies on the mock Scheduler. Doesn't affect any public - // API but we might want to fix this for our own internal tests. - // - // For now, wait for each promise in sequence. - await act(async () => { - await promiseA; - }); - await act(async () => { - await promiseB; - }); - await act(async () => { - await promiseC; - }); - - expect(getVisibleChildren(container)).toEqual('ABC'); - - ReactDOMClient.hydrateRoot(container, ); - expect(Scheduler).toFlushAndYield([]); - expect(getVisibleChildren(container)).toEqual('ABC'); - }); - - // @gate enableUseHook - it('basic use(context)', async () => { - const ContextA = React.createContext('default'); - const ContextB = React.createContext('B'); - const ServerContext = React.createServerContext( - 'ServerContext', - 'default', - ); - function Client() { - return use(ContextA) + use(ContextB); - } - function ServerComponent() { - return use(ServerContext); - } - function Server() { - return ( - - - - ); - } - function App() { - return ( - <> - - - - - - ); - } - - await act(async () => { - const {pipe} = ReactDOMFizzServer.renderToPipeableStream(); - pipe(writable); - }); - expect(getVisibleChildren(container)).toEqual(['AB', 'C']); - - // Hydration uses a different renderer runtime (Fiber instead of Fizz). - // We reset _currentRenderer here to not trigger a warning about multiple - // renderers concurrently using these contexts - ContextA._currentRenderer = null; - ServerContext._currentRenderer = null; - ReactDOMClient.hydrateRoot(container, ); - expect(Scheduler).toFlushAndYield([]); - expect(getVisibleChildren(container)).toEqual(['AB', 'C']); - }); - - // @gate enableUseHook - it('use(promise) in multiple components', async () => { - const promiseA = Promise.resolve('A'); - const promiseB = Promise.resolve('B'); - const promiseC = Promise.resolve('C'); - const promiseD = Promise.resolve('D'); - - function Child({prefix}) { - return prefix + use(promiseC) + use(promiseD); - } - - function Parent() { - return ; - } - - function App() { - return ( - - - - ); - } - - await act(async () => { - const {pipe} = ReactDOMFizzServer.renderToPipeableStream(); - pipe(writable); - }); - - // TODO: The `act` implementation in this file doesn't unwrap microtasks - // automatically. We can't use the same `act` we use for Fiber tests - // because that relies on the mock Scheduler. Doesn't affect any public - // API but we might want to fix this for our own internal tests. - // - // For now, wait for each promise in sequence. - await act(async () => { - await promiseA; - }); - await act(async () => { - await promiseB; - }); - await act(async () => { - await promiseC; - }); - await act(async () => { - await promiseD; - }); - - expect(getVisibleChildren(container)).toEqual('ABCD'); - - ReactDOMClient.hydrateRoot(container, ); - expect(Scheduler).toFlushAndYield([]); - expect(getVisibleChildren(container)).toEqual('ABCD'); - }); - - // @gate enableUseHook - it('using a rejected promise will throw', async () => { - const promiseA = Promise.resolve('A'); - const promiseB = Promise.reject(new Error('Oops!')); - const promiseC = Promise.resolve('C'); - - // Jest/Node will raise an unhandled rejected error unless we await this. It - // works fine in the browser, though. - await expect(promiseB).rejects.toThrow('Oops!'); - - function Async() { - return use(promiseA) + use(promiseB) + use(promiseC); - } - - class ErrorBoundary extends React.Component { - state = {error: null}; - static getDerivedStateFromError(error) { - return {error}; - } - render() { - if (this.state.error) { - return this.state.error.message; - } - return this.props.children; - } - } - - function App() { - return ( - - - - - - ); - } - - const reportedServerErrors = []; - await act(async () => { - const {pipe} = ReactDOMFizzServer.renderToPipeableStream(, { - onError(error) { - reportedServerErrors.push(error); - }, - }); - pipe(writable); - }); - - // TODO: The `act` implementation in this file doesn't unwrap microtasks - // automatically. We can't use the same `act` we use for Fiber tests - // because that relies on the mock Scheduler. Doesn't affect any public - // API but we might want to fix this for our own internal tests. - // - // For now, wait for each promise in sequence. - await act(async () => { - await promiseA; - }); - await act(async () => { - await expect(promiseB).rejects.toThrow('Oops!'); - }); - await act(async () => { - await promiseC; - }); - - expect(getVisibleChildren(container)).toEqual('Loading...'); - expect(reportedServerErrors.length).toBe(1); - expect(reportedServerErrors[0].message).toBe('Oops!'); - - const reportedClientErrors = []; - ReactDOMClient.hydrateRoot(container, , { - onRecoverableError(error) { - reportedClientErrors.push(error); - }, - }); - expect(Scheduler).toFlushAndYield([]); - expect(getVisibleChildren(container)).toEqual('Oops!'); - expect(reportedClientErrors.length).toBe(1); - if (__DEV__) { - expect(reportedClientErrors[0].message).toBe('Oops!'); - } else { - expect(reportedClientErrors[0].message).toBe( - 'The server could not finish this Suspense boundary, likely due to ' + - 'an error during server rendering. Switched to client rendering.', - ); - } - }); - - // @gate enableUseHook - it("use a promise that's already been instrumented and resolved", async () => { - const thenable = { - status: 'fulfilled', - value: 'Hi', - then() {}, - }; - - // This will never suspend because the thenable already resolved - function App() { - return use(thenable); - } - - await act(async () => { - const {pipe} = ReactDOMFizzServer.renderToPipeableStream(); - pipe(writable); - }); - expect(getVisibleChildren(container)).toEqual('Hi'); - - ReactDOMClient.hydrateRoot(container, ); - expect(Scheduler).toFlushAndYield([]); - expect(getVisibleChildren(container)).toEqual('Hi'); - }); }); describe('useEvent', () => { diff --git a/packages/react-server/src/ReactFizzHooks.js b/packages/react-server/src/ReactFizzHooks.js index f404a71c6ad5f..5d1d8c79a485f 100644 --- a/packages/react-server/src/ReactFizzHooks.js +++ b/packages/react-server/src/ReactFizzHooks.js @@ -15,36 +15,23 @@ import type { MutableSourceSubscribeFn, ReactContext, StartTransitionOptions, - Thenable, - Usable, } from 'shared/ReactTypes'; import type {ResponseState} from './ReactServerFormatConfig'; import type {Task} from './ReactFizzServer'; -import type {ThenableState} from './ReactFizzWakeable'; import {readContext as readContextImpl} from './ReactFizzNewContext'; import {getTreeId} from './ReactFizzTreeContext'; -import { - getPreviouslyUsedThenableAtIndex, - createThenableState, - trackUsedThenable, -} from './ReactFizzWakeable'; import {makeId} from './ReactServerFormatConfig'; import { enableCache, - enableUseHook, enableUseEventHook, enableUseMemoCacheHook, } from 'shared/ReactFeatureFlags'; import is from 'shared/objectIs'; -import { - REACT_SERVER_CONTEXT_TYPE, - REACT_CONTEXT_TYPE, - REACT_MEMO_CACHE_SENTINEL, -} from 'shared/ReactSymbols'; +import {REACT_MEMO_CACHE_SENTINEL} from 'shared/ReactSymbols'; type BasicStateAction = (S => S) | S; type Dispatch = A => void; @@ -75,9 +62,6 @@ let isReRender: boolean = false; let didScheduleRenderPhaseUpdate: boolean = false; // Counts the number of useId hooks in this component let localIdCounter: number = 0; -// Counts the number of use(thenable) calls in this component -let thenableIndexCounter: number = 0; -let thenableState: ThenableState | null = null; // Lazily created map of render-phase updates let renderPhaseUpdates: Map, Update> | null = null; // Counter to prevent infinite loops. @@ -192,11 +176,7 @@ function createWorkInProgressHook(): Hook { return workInProgressHook; } -export function prepareToUseHooks( - task: Task, - componentIdentity: Object, - prevThenableState: ThenableState | null, -): void { +export function prepareToUseHooks(task: Task, componentIdentity: Object): void { currentlyRenderingComponent = componentIdentity; currentlyRenderingTask = task; if (__DEV__) { @@ -205,14 +185,13 @@ export function prepareToUseHooks( // The following should have already been reset // didScheduleRenderPhaseUpdate = false; + // localIdCounter = 0; // firstWorkInProgressHook = null; // numberOfReRenders = 0; // renderPhaseUpdates = null; // workInProgressHook = null; localIdCounter = 0; - thenableIndexCounter = 0; - thenableState = prevThenableState; } export function finishHooks( @@ -231,7 +210,6 @@ export function finishHooks( // restarting until no more updates are scheduled. didScheduleRenderPhaseUpdate = false; localIdCounter = 0; - thenableIndexCounter = 0; numberOfReRenders += 1; // Start over from the beginning of the list @@ -243,13 +221,7 @@ export function finishHooks( return children; } -export function getThenableStateAfterSuspending(): null | ThenableState { - const state = thenableState; - thenableState = null; - return state; -} - -export function checkDidRenderIdHook(): boolean { +export function checkDidRenderIdHook() { // This should be called immediately after every finishHooks call. // Conceptually, it's part of the return value of finishHooks; it's only a // separate function to avoid using an array tuple. @@ -582,85 +554,6 @@ function useId(): string { return makeId(responseState, treeId, localId); } -function use(usable: Usable): T { - if (usable !== null && typeof usable === 'object') { - // $FlowFixMe[method-unbinding] - if (typeof usable.then === 'function') { - // This is a thenable. - const thenable: Thenable = (usable: any); - - // Track the position of the thenable within this fiber. - const index = thenableIndexCounter; - thenableIndexCounter += 1; - - switch (thenable.status) { - case 'fulfilled': { - const fulfilledValue: T = thenable.value; - return fulfilledValue; - } - case 'rejected': { - const rejectedError = thenable.reason; - throw rejectedError; - } - default: { - const prevThenableAtIndex: Thenable | null = getPreviouslyUsedThenableAtIndex( - thenableState, - index, - ); - if (prevThenableAtIndex !== null) { - if (thenable !== prevThenableAtIndex) { - // Avoid an unhandled rejection errors for the Promises that we'll - // intentionally ignore. - thenable.then(noop, noop); - } - switch (prevThenableAtIndex.status) { - case 'fulfilled': { - const fulfilledValue: T = prevThenableAtIndex.value; - return fulfilledValue; - } - case 'rejected': { - const rejectedError: mixed = prevThenableAtIndex.reason; - throw rejectedError; - } - default: { - // The thenable still hasn't resolved. Suspend with the same - // thenable as last time to avoid redundant listeners. - throw prevThenableAtIndex; - } - } - } else { - // This is the first time something has been used at this index. - // Stash the thenable at the current index so we can reuse it during - // the next attempt. - if (thenableState === null) { - thenableState = createThenableState(); - } - trackUsedThenable(thenableState, thenable, index); - - // Suspend. - // TODO: Throwing here is an implementation detail that allows us to - // unwind the call stack. But we shouldn't allow it to leak into - // userspace. Throw an opaque placeholder value instead of the - // actual thenable. If it doesn't get captured by the work loop, log - // a warning, because that means something in userspace must have - // caught it. - throw thenable; - } - } - } - } else if ( - usable.$$typeof === REACT_CONTEXT_TYPE || - usable.$$typeof === REACT_SERVER_CONTEXT_TYPE - ) { - const context: ReactContext = (usable: any); - return readContext(context); - } - } - - // eslint-disable-next-line react-internal/safe-string-coercion - throw new Error('An unsupported type was passed to use(): ' + String(usable)); -} - function unsupportedRefresh() { throw new Error('Cache cannot be refreshed during server rendering.'); } @@ -712,9 +605,6 @@ if (enableUseEventHook) { if (enableUseMemoCacheHook) { HooksDispatcher.useMemoCache = useMemoCache; } -if (enableUseHook) { - HooksDispatcher.use = use; -} export let currentResponseState: null | ResponseState = (null: any); export function setCurrentResponseState( diff --git a/packages/react-server/src/ReactFizzServer.js b/packages/react-server/src/ReactFizzServer.js index f6dc21abcfeb8..8bdd6a69bf60e 100644 --- a/packages/react-server/src/ReactFizzServer.js +++ b/packages/react-server/src/ReactFizzServer.js @@ -17,7 +17,6 @@ import type { ReactContext, ReactProviderType, OffscreenMode, - Wakeable, } from 'shared/ReactTypes'; import type {LazyComponent as LazyComponentType} from 'react/src/ReactLazy'; import type { @@ -30,7 +29,6 @@ import type { import type {ContextSnapshot} from './ReactFizzNewContext'; import type {ComponentStackNode} from './ReactFizzComponentStack'; import type {TreeContext} from './ReactFizzTreeContext'; -import type {ThenableState} from './ReactFizzWakeable'; import { scheduleWork, @@ -100,7 +98,6 @@ import { HooksDispatcher, currentResponseState, setCurrentResponseState, - getThenableStateAfterSuspending, } from './ReactFizzHooks'; import {DefaultCacheDispatcher} from './ReactFizzCache'; import {getStackByComponentStackNode} from './ReactFizzComponentStack'; @@ -139,7 +136,6 @@ import { import assign from 'shared/assign'; import getComponentNameFromType from 'shared/getComponentNameFromType'; import isArray from 'shared/isArray'; -import {trackSuspendedWakeable} from './ReactFizzWakeable'; const ReactCurrentDispatcher = ReactSharedInternals.ReactCurrentDispatcher; const ReactCurrentCache = ReactSharedInternals.ReactCurrentCache; @@ -174,7 +170,6 @@ export type Task = { context: ContextSnapshot, // the current new context that this task is executing in treeContext: TreeContext, // the current tree context that this task is executing in componentStack: null | ComponentStackNode, // DEV-only component stack - thenableState: null | ThenableState, }; const PENDING = 0; @@ -320,7 +315,6 @@ export function createRequest( rootSegment.parentFlushed = true; const rootTask = createTask( request, - null, children, null, rootSegment, @@ -361,7 +355,6 @@ function createSuspenseBoundary( function createTask( request: Request, - thenableState: ThenableState | null, node: ReactNodeList, blockedBoundary: Root | SuspenseBoundary, blockedSegment: Segment, @@ -385,7 +378,6 @@ function createTask( legacyContext, context, treeContext, - thenableState, }: any); if (__DEV__) { task.componentStack = null; @@ -636,7 +628,6 @@ function renderSuspenseBoundary( // on it yet in case we finish the main content, so we queue for later. const suspendedFallbackTask = createTask( request, - null, fallback, parentBoundary, boundarySegment, @@ -726,13 +717,12 @@ function shouldConstruct(Component) { function renderWithHooks( request: Request, task: Task, - prevThenableState: ThenableState | null, Component: (p: Props, arg: SecondArg) => any, props: Props, secondArg: SecondArg, ): any { const componentIdentity = {}; - prepareToUseHooks(task, componentIdentity, prevThenableState); + prepareToUseHooks(task, componentIdentity); const result = Component(props, secondArg); return finishHooks(Component, props, result, secondArg); } @@ -770,13 +760,13 @@ function finishClassComponent( childContextTypes, ); task.legacyContext = mergedContext; - renderNodeDestructive(request, task, null, nextChildren); + renderNodeDestructive(request, task, nextChildren); task.legacyContext = previousContext; return; } } - renderNodeDestructive(request, task, null, nextChildren); + renderNodeDestructive(request, task, nextChildren); } function renderClassComponent( @@ -810,7 +800,6 @@ let hasWarnedAboutUsingContextAsConsumer = false; function renderIndeterminateComponent( request: Request, task: Task, - prevThenableState: ThenableState | null, Component: any, props: any, ): void { @@ -839,14 +828,7 @@ function renderIndeterminateComponent( } } - const value = renderWithHooks( - request, - task, - prevThenableState, - Component, - props, - legacyContext, - ); + const value = renderWithHooks(request, task, Component, props, legacyContext); const hasId = checkDidRenderIdHook(); if (__DEV__) { @@ -927,12 +909,12 @@ function renderIndeterminateComponent( const index = 0; task.treeContext = pushTreeContext(prevTreeContext, totalChildren, index); try { - renderNodeDestructive(request, task, null, value); + renderNodeDestructive(request, task, value); } finally { task.treeContext = prevTreeContext; } } else { - renderNodeDestructive(request, task, null, value); + renderNodeDestructive(request, task, value); } } popComponentStackInDEV(task); @@ -1012,20 +994,12 @@ function resolveDefaultProps(Component: any, baseProps: Object): Object { function renderForwardRef( request: Request, task: Task, - prevThenableState, type: any, props: Object, ref: any, ): void { pushFunctionComponentStackInDEV(task, type.render); - const children = renderWithHooks( - request, - task, - prevThenableState, - type.render, - props, - ref, - ); + const children = renderWithHooks(request, task, type.render, props, ref); const hasId = checkDidRenderIdHook(); if (hasId) { // This component materialized an id. We treat this as its own level, with @@ -1035,12 +1009,12 @@ function renderForwardRef( const index = 0; task.treeContext = pushTreeContext(prevTreeContext, totalChildren, index); try { - renderNodeDestructive(request, task, null, children); + renderNodeDestructive(request, task, children); } finally { task.treeContext = prevTreeContext; } } else { - renderNodeDestructive(request, task, null, children); + renderNodeDestructive(request, task, children); } popComponentStackInDEV(task); } @@ -1048,21 +1022,13 @@ function renderForwardRef( function renderMemo( request: Request, task: Task, - prevThenableState: ThenableState | null, type: any, props: Object, ref: any, ): void { const innerType = type.type; const resolvedProps = resolveDefaultProps(innerType, props); - renderElement( - request, - task, - prevThenableState, - innerType, - resolvedProps, - ref, - ); + renderElement(request, task, innerType, resolvedProps, ref); } function renderContextConsumer( @@ -1112,7 +1078,7 @@ function renderContextConsumer( const newValue = readContext(context); const newChildren = render(newValue); - renderNodeDestructive(request, task, null, newChildren); + renderNodeDestructive(request, task, newChildren); } function renderContextProvider( @@ -1129,7 +1095,7 @@ function renderContextProvider( prevSnapshot = task.context; } task.context = pushProvider(context, value); - renderNodeDestructive(request, task, null, children); + renderNodeDestructive(request, task, children); task.context = popProvider(context); if (__DEV__) { if (prevSnapshot !== task.context) { @@ -1143,7 +1109,6 @@ function renderContextProvider( function renderLazyComponent( request: Request, task: Task, - prevThenableState: ThenableState | null, lazyComponent: LazyComponentType, props: Object, ref: any, @@ -1153,14 +1118,7 @@ function renderLazyComponent( const init = lazyComponent._init; const Component = init(payload); const resolvedProps = resolveDefaultProps(Component, props); - renderElement( - request, - task, - prevThenableState, - Component, - resolvedProps, - ref, - ); + renderElement(request, task, Component, resolvedProps, ref); popComponentStackInDEV(task); } @@ -1172,14 +1130,13 @@ function renderOffscreen(request: Request, task: Task, props: Object): void { } else { // A visible Offscreen boundary is treated exactly like a fragment: a // pure indirection. - renderNodeDestructive(request, task, null, props.children); + renderNodeDestructive(request, task, props.children); } } function renderElement( request: Request, task: Task, - prevThenableState: ThenableState | null, type: any, props: Object, ref: any, @@ -1189,13 +1146,7 @@ function renderElement( renderClassComponent(request, task, type, props); return; } else { - renderIndeterminateComponent( - request, - task, - prevThenableState, - type, - props, - ); + renderIndeterminateComponent(request, task, type, props); return; } } @@ -1219,7 +1170,7 @@ function renderElement( case REACT_STRICT_MODE_TYPE: case REACT_PROFILER_TYPE: case REACT_FRAGMENT_TYPE: { - renderNodeDestructive(request, task, null, props.children); + renderNodeDestructive(request, task, props.children); return; } case REACT_OFFSCREEN_TYPE: { @@ -1229,13 +1180,13 @@ function renderElement( case REACT_SUSPENSE_LIST_TYPE: { pushBuiltInComponentStackInDEV(task, 'SuspenseList'); // TODO: SuspenseList should control the boundaries. - renderNodeDestructive(request, task, null, props.children); + renderNodeDestructive(request, task, props.children); popComponentStackInDEV(task); return; } case REACT_SCOPE_TYPE: { if (enableScopeAPI) { - renderNodeDestructive(request, task, null, props.children); + renderNodeDestructive(request, task, props.children); return; } throw new Error('ReactDOMServer does not yet support scope components.'); @@ -1257,11 +1208,11 @@ function renderElement( if (typeof type === 'object' && type !== null) { switch (type.$$typeof) { case REACT_FORWARD_REF_TYPE: { - renderForwardRef(request, task, prevThenableState, type, props, ref); + renderForwardRef(request, task, type, props, ref); return; } case REACT_MEMO_TYPE: { - renderMemo(request, task, prevThenableState, type, props, ref); + renderMemo(request, task, type, props, ref); return; } case REACT_PROVIDER_TYPE: { @@ -1273,7 +1224,7 @@ function renderElement( return; } case REACT_LAZY_TYPE: { - renderLazyComponent(request, task, prevThenableState, type, props); + renderLazyComponent(request, task, type, props); return; } } @@ -1338,9 +1289,6 @@ function validateIterable(iterable, iteratorFn: Function): void { function renderNodeDestructive( request: Request, task: Task, - // The thenable state reused from the previous attempt, if any. This is almost - // always null, except when called by retryTask. - prevThenableState: ThenableState | null, node: ReactNodeList, ): void { if (__DEV__) { @@ -1348,7 +1296,7 @@ function renderNodeDestructive( // a component stack at the right place in the tree. We don't do this in renderNode // becuase it is not called at every layer of the tree and we may lose frames try { - return renderNodeDestructiveImpl(request, task, prevThenableState, node); + return renderNodeDestructiveImpl(request, task, node); } catch (x) { if (typeof x === 'object' && x !== null && typeof x.then === 'function') { // This is a Wakable, noop @@ -1363,7 +1311,7 @@ function renderNodeDestructive( throw x; } } else { - return renderNodeDestructiveImpl(request, task, prevThenableState, node); + return renderNodeDestructiveImpl(request, task, node); } } @@ -1372,7 +1320,6 @@ function renderNodeDestructive( function renderNodeDestructiveImpl( request: Request, task: Task, - prevThenableState: ThenableState | null, node: ReactNodeList, ): void { // Stash the node we're working on. We'll pick up from this task in case @@ -1387,7 +1334,7 @@ function renderNodeDestructiveImpl( const type = element.type; const props = element.props; const ref = element.ref; - renderElement(request, task, prevThenableState, type, props, ref); + renderElement(request, task, type, props, ref); return; } case REACT_PORTAL_TYPE: @@ -1421,7 +1368,7 @@ function renderNodeDestructiveImpl( } else { resolvedNode = init(payload); } - renderNodeDestructive(request, task, null, resolvedNode); + renderNodeDestructive(request, task, resolvedNode); return; } } @@ -1523,7 +1470,6 @@ function renderChildrenArray(request, task, children) { function spawnNewSuspendedTask( request: Request, task: Task, - thenableState: ThenableState | null, x: Promise, ): void { // Something suspended, we'll need to create a new segment and resolve it later. @@ -1544,7 +1490,6 @@ function spawnNewSuspendedTask( segment.lastPushedText = false; const newTask = createTask( request, - thenableState, task.node, task.blockedBoundary, newSegment, @@ -1553,9 +1498,6 @@ function spawnNewSuspendedTask( task.context, task.treeContext, ); - - trackSuspendedWakeable(x); - if (__DEV__) { if (task.componentStack !== null) { // We pop one task off the stack because the node that suspended will be tried again, @@ -1583,13 +1525,11 @@ function renderNode(request: Request, task: Task, node: ReactNodeList): void { previousComponentStack = task.componentStack; } try { - return renderNodeDestructive(request, task, null, node); + return renderNodeDestructive(request, task, node); } catch (x) { resetHooksState(); if (typeof x === 'object' && x !== null && typeof x.then === 'function') { - const thenableState = getThenableStateAfterSuspending(); - spawnNewSuspendedTask(request, task, thenableState, x); - + spawnNewSuspendedTask(request, task, x); // Restore the context. We assume that this will be restored by the inner // functions in case nothing throws so we don't use "finally" here. task.blockedSegment.formatContext = previousFormatContext; @@ -1855,14 +1795,7 @@ function retryTask(request: Request, task: Task): void { try { // We call the destructive form that mutates this task. That way if something // suspends again, we can reuse the same task instead of spawning a new one. - - // Reset the task's thenable state before continuing, so that if a later - // component suspends we can reuse the same task object. If the same - // component suspends again, the thenable state will be restored. - const prevThenableState = task.thenableState; - task.thenableState = null; - - renderNodeDestructive(request, task, prevThenableState, task.node); + renderNodeDestructive(request, task, task.node); pushSegmentFinale( segment.chunks, request.responseState, @@ -1879,10 +1812,6 @@ function retryTask(request: Request, task: Task): void { // Something suspended again, let's pick it back up later. const ping = task.ping; x.then(ping, ping); - - const wakeable: Wakeable = x; - trackSuspendedWakeable(wakeable); - task.thenableState = getThenableStateAfterSuspending(); } else { task.abortSet.delete(task); segment.status = ERRORED; diff --git a/packages/react-server/src/ReactFizzWakeable.js b/packages/react-server/src/ReactFizzWakeable.js index 56066a9cc24c2..e69de29bb2d1d 100644 --- a/packages/react-server/src/ReactFizzWakeable.js +++ b/packages/react-server/src/ReactFizzWakeable.js @@ -1,106 +0,0 @@ -/** - * Copyright (c) Meta Platforms, Inc. and affiliates. - * - * This source code is licensed under the MIT license found in the - * LICENSE file in the root directory of this source tree. - * - * @flow - */ - -// Corresponds to ReactFiberWakeable and ReactFlightWakeable modules. Generally, -// changes to one module should be reflected in the others. - -// TODO: Rename this module and the corresponding Fiber one to "Thenable" -// instead of "Wakeable". Or some other more appropriate name. - -import type { - Wakeable, - Thenable, - PendingThenable, - FulfilledThenable, - RejectedThenable, -} from 'shared/ReactTypes'; - -// TODO: Sparse arrays are bad for performance. -export opaque type ThenableState = Array | void>; - -export function createThenableState(): ThenableState { - // The ThenableState is created the first time a component suspends. If it - // suspends again, we'll reuse the same state. - return []; -} - -export function trackSuspendedWakeable(wakeable: Wakeable) { - // If this wakeable isn't already a thenable, turn it into one now. Then, - // when we resume the work loop, we can check if its status is - // still pending. - // TODO: Get rid of the Wakeable type? It's superseded by UntrackedThenable. - const thenable: Thenable = (wakeable: any); - - // We use an expando to track the status and result of a thenable so that we - // can synchronously unwrap the value. Think of this as an extension of the - // Promise API, or a custom interface that is a superset of Thenable. - // - // If the thenable doesn't have a status, set it to "pending" and attach - // a listener that will update its status and result when it resolves. - switch (thenable.status) { - case 'fulfilled': - case 'rejected': - // A thenable that already resolved shouldn't have been thrown, so this is - // unexpected. Suggests a mistake in a userspace data library. Don't track - // this thenable, because if we keep trying it will likely infinite loop - // without ever resolving. - // TODO: Log a warning? - break; - default: { - if (typeof thenable.status === 'string') { - // Only instrument the thenable if the status if not defined. If - // it's defined, but an unknown value, assume it's been instrumented by - // some custom userspace implementation. We treat it as "pending". - break; - } - const pendingThenable: PendingThenable = (thenable: any); - pendingThenable.status = 'pending'; - pendingThenable.then( - fulfilledValue => { - if (thenable.status === 'pending') { - const fulfilledThenable: FulfilledThenable = (thenable: any); - fulfilledThenable.status = 'fulfilled'; - fulfilledThenable.value = fulfilledValue; - } - }, - (error: mixed) => { - if (thenable.status === 'pending') { - const rejectedThenable: RejectedThenable = (thenable: any); - rejectedThenable.status = 'rejected'; - rejectedThenable.reason = error; - } - }, - ); - break; - } - } -} - -export function trackUsedThenable( - thenableState: ThenableState, - thenable: Thenable, - index: number, -) { - // This is only a separate function from trackSuspendedWakeable for symmetry - // with Fiber. - thenableState[index] = thenable; -} - -export function getPreviouslyUsedThenableAtIndex( - thenableState: ThenableState | null, - index: number, -): Thenable | null { - if (thenableState !== null) { - const thenable = thenableState[index]; - if (thenable !== undefined) { - return thenable; - } - } - return null; -} diff --git a/packages/react-server/src/ReactFlightHooks.js b/packages/react-server/src/ReactFlightHooks.js index 1547f29dbcfbe..78a50d8310e49 100644 --- a/packages/react-server/src/ReactFlightHooks.js +++ b/packages/react-server/src/ReactFlightHooks.js @@ -42,10 +42,8 @@ export function prepareToUseHooksForComponent( thenableState = prevThenableState; } -export function getThenableStateAfterSuspending(): null | ThenableState { - const state = thenableState; - thenableState = null; - return state; +export function getThenableStateAfterSuspending() { + return thenableState; } function readContext(context: ReactServerContext): T { @@ -189,9 +187,8 @@ function use(usable: Usable): T { } } } - } else if (usable.$$typeof === REACT_SERVER_CONTEXT_TYPE) { - const context: ReactServerContext = (usable: any); - return readContext(context); + } else { + // TODO: Add support for Context } } diff --git a/packages/react-server/src/ReactFlightWakeable.js b/packages/react-server/src/ReactFlightWakeable.js index 7c344d1d01126..3e658d78c95d8 100644 --- a/packages/react-server/src/ReactFlightWakeable.js +++ b/packages/react-server/src/ReactFlightWakeable.js @@ -7,8 +7,8 @@ * @flow */ -// Corresponds to ReactFiberWakeable and ReactFizzWakeable modules. Generally, -// changes to one module should be reflected in the others. +// Corresponds to ReactFiberWakeable module. Generally, changes to one module +// should be reflected in the other. // TODO: Rename this module and the corresponding Fiber one to "Thenable" // instead of "Wakeable". Or some other more appropriate name.