From 01168df6dfa753207a66fdbe00b181cdae785960 Mon Sep 17 00:00:00 2001 From: Dan Abramov Date: Sun, 11 Feb 2024 01:31:39 +0000 Subject: [PATCH] Add a feature flag for new behavior --- .../src/ReactFlightReplyClient.js | 7 +- .../react-debug-tools/src/ReactDebugHooks.js | 6 +- ...eactDOMServerIntegrationNewContext-test.js | 6 + .../__tests__/ReactServerRendering-test.js | 1 + packages/react-is/src/ReactIs.js | 33 +- packages/react-reconciler/src/ReactFiber.js | 25 +- .../src/ReactFiberBeginWork.js | 30 +- .../src/ReactFiberCompleteWork.js | 8 +- .../src/ReactFiberNewContext.js | 8 +- .../react-reconciler/src/ReactFiberScope.js | 10 +- .../src/ReactFiberUnwindWork.js | 15 +- .../src/__tests__/ReactNewContext-test.js | 2 + .../src/getComponentNameFromFiber.js | 23 +- packages/react-server/src/ReactFizzServer.js | 364 ++++++++++-------- packages/react/src/ReactContext.js | 78 +++- .../__tests__/ReactContextValidator-test.js | 1 + packages/shared/ReactFeatureFlags.js | 2 + packages/shared/ReactSymbols.js | 1 + .../forks/ReactFeatureFlags.native-fb.js | 1 + .../forks/ReactFeatureFlags.native-oss.js | 1 + .../forks/ReactFeatureFlags.test-renderer.js | 1 + .../ReactFeatureFlags.test-renderer.native.js | 1 + .../ReactFeatureFlags.test-renderer.www.js | 1 + .../forks/ReactFeatureFlags.www-dynamic.js | 1 + .../shared/forks/ReactFeatureFlags.www.js | 1 + packages/shared/getComponentNameFromType.js | 28 +- packages/shared/isValidElementType.js | 5 +- 27 files changed, 452 insertions(+), 208 deletions(-) diff --git a/packages/react-client/src/ReactFlightReplyClient.js b/packages/react-client/src/ReactFlightReplyClient.js index 6bf4e11a43d53..0e4dfb047ff90 100644 --- a/packages/react-client/src/ReactFlightReplyClient.js +++ b/packages/react-client/src/ReactFlightReplyClient.js @@ -14,11 +14,13 @@ import type { RejectedThenable, ReactCustomFormAction, } from 'shared/ReactTypes'; +import {enableRenderableContext} from 'shared/ReactFeatureFlags'; import { REACT_ELEMENT_TYPE, REACT_LAZY_TYPE, REACT_CONTEXT_TYPE, + REACT_PROVIDER_TYPE, getIteratorFn, } from 'shared/ReactSymbols'; @@ -297,7 +299,10 @@ export function processReply( 'React Lazy cannot be passed to Server Functions from the Client.%s', describeObjectForErrorMessage(parent, key), ); - } else if ((value: any).$$typeof === REACT_CONTEXT_TYPE) { + } else if ( + (value: any).$$typeof === + (enableRenderableContext ? REACT_CONTEXT_TYPE : REACT_PROVIDER_TYPE) + ) { console.error( 'React Context Providers cannot be passed to Server Functions from the Client.%s', describeObjectForErrorMessage(parent, key), diff --git a/packages/react-debug-tools/src/ReactDebugHooks.js b/packages/react-debug-tools/src/ReactDebugHooks.js index 23229dd57c4ff..31308c73ca2e7 100644 --- a/packages/react-debug-tools/src/ReactDebugHooks.js +++ b/packages/react-debug-tools/src/ReactDebugHooks.js @@ -821,7 +821,11 @@ function setupContexts(contextMap: Map, any>, fiber: Fiber) { let current: null | Fiber = fiber; while (current) { if (current.tag === ContextProvider) { - const context: ReactContext = current.type; + let context: ReactContext = current.type; + if ((context: any)._context !== undefined) { + // Support inspection of pre-19+ providers. + context = (context: any)._context; + } if (!contextMap.has(context)) { // Store the current value that we're going to restore later. contextMap.set(context, context._currentValue); diff --git a/packages/react-dom/src/__tests__/ReactDOMServerIntegrationNewContext-test.js b/packages/react-dom/src/__tests__/ReactDOMServerIntegrationNewContext-test.js index 1c3a91d160b68..a460e2104ce88 100644 --- a/packages/react-dom/src/__tests__/ReactDOMServerIntegrationNewContext-test.js +++ b/packages/react-dom/src/__tests__/ReactDOMServerIntegrationNewContext-test.js @@ -296,6 +296,12 @@ describe('ReactDOMServerIntegration', () => { }); itRenders('should treat Context as Context.Provider', async render => { + // The `itRenders` helpers don't work with the gate pragma, so we have to do + // this instead. + if (gate(flags => !flags.enableRenderableContext)) { + return; + } + const Theme = React.createContext('dark'); const Language = React.createContext('french'); diff --git a/packages/react-dom/src/__tests__/ReactServerRendering-test.js b/packages/react-dom/src/__tests__/ReactServerRendering-test.js index 42a0952350785..b95c20f9e3182 100644 --- a/packages/react-dom/src/__tests__/ReactServerRendering-test.js +++ b/packages/react-dom/src/__tests__/ReactServerRendering-test.js @@ -1000,6 +1000,7 @@ describe('ReactDOMServer', () => { ]); }); + // @gate enableRenderableContext || !__DEV__ it('should warn if an invalid contextType is defined', () => { const Context = React.createContext(); class ComponentA extends React.Component { diff --git a/packages/react-is/src/ReactIs.js b/packages/react-is/src/ReactIs.js index 9541ed854b8e8..6e935bd8f7c64 100644 --- a/packages/react-is/src/ReactIs.js +++ b/packages/react-is/src/ReactIs.js @@ -18,12 +18,14 @@ import { REACT_MEMO_TYPE, REACT_PORTAL_TYPE, REACT_PROFILER_TYPE, + REACT_PROVIDER_TYPE, REACT_CONSUMER_TYPE, REACT_STRICT_MODE_TYPE, REACT_SUSPENSE_TYPE, REACT_SUSPENSE_LIST_TYPE, } from 'shared/ReactSymbols'; import isValidElementType from 'shared/isValidElementType'; +import {enableRenderableContext} from 'shared/ReactFeatureFlags'; export function typeOf(object: any): mixed { if (typeof object === 'object' && object !== null) { @@ -47,8 +49,17 @@ export function typeOf(object: any): mixed { case REACT_FORWARD_REF_TYPE: case REACT_LAZY_TYPE: case REACT_MEMO_TYPE: - case REACT_CONSUMER_TYPE: return $$typeofType; + case REACT_CONSUMER_TYPE: + if (enableRenderableContext) { + return $$typeofType; + } + // Fall through + case REACT_PROVIDER_TYPE: + if (!enableRenderableContext) { + return $$typeofType; + } + // Fall through default: return $$typeof; } @@ -61,8 +72,12 @@ export function typeOf(object: any): mixed { return undefined; } -export const ContextConsumer = REACT_CONSUMER_TYPE; -export const ContextProvider = REACT_CONTEXT_TYPE; +export const ContextConsumer: symbol = enableRenderableContext + ? REACT_CONSUMER_TYPE + : REACT_CONTEXT_TYPE; +export const ContextProvider: symbol = enableRenderableContext + ? REACT_CONTEXT_TYPE + : REACT_PROVIDER_TYPE; export const Element = REACT_ELEMENT_TYPE; export const ForwardRef = REACT_FORWARD_REF_TYPE; export const Fragment = REACT_FRAGMENT_TYPE; @@ -77,10 +92,18 @@ export const SuspenseList = REACT_SUSPENSE_LIST_TYPE; export {isValidElementType}; export function isContextConsumer(object: any): boolean { - return typeOf(object) === REACT_CONSUMER_TYPE; + if (enableRenderableContext) { + return typeOf(object) === REACT_CONSUMER_TYPE; + } else { + return typeOf(object) === REACT_CONTEXT_TYPE; + } } export function isContextProvider(object: any): boolean { - return typeOf(object) === REACT_CONTEXT_TYPE; + if (enableRenderableContext) { + return typeOf(object) === REACT_CONTEXT_TYPE; + } else { + return typeOf(object) === REACT_PROVIDER_TYPE; + } } export function isElement(object: any): boolean { return ( diff --git a/packages/react-reconciler/src/ReactFiber.js b/packages/react-reconciler/src/ReactFiber.js index 77eb247f95231..5ef098fa12d52 100644 --- a/packages/react-reconciler/src/ReactFiber.js +++ b/packages/react-reconciler/src/ReactFiber.js @@ -38,6 +38,7 @@ import { enableDebugTracing, enableFloat, enableDO_NOT_USE_disableStrictPassiveEffect, + enableRenderableContext, } from 'shared/ReactFeatureFlags'; import {NoFlags, Placement, StaticMask} from './ReactFiberFlags'; import {ConcurrentRoot} from './ReactRootTags'; @@ -94,6 +95,7 @@ import { REACT_DEBUG_TRACING_MODE_TYPE, REACT_STRICT_MODE_TYPE, REACT_PROFILER_TYPE, + REACT_PROVIDER_TYPE, REACT_CONTEXT_TYPE, REACT_CONSUMER_TYPE, REACT_SUSPENSE_TYPE, @@ -578,13 +580,26 @@ export function createFiberFromTypeAndProps( default: { if (typeof type === 'object' && type !== null) { switch (type.$$typeof) { + case REACT_PROVIDER_TYPE: + if (!enableRenderableContext) { + fiberTag = ContextProvider; + break getTag; + } + // Fall through case REACT_CONTEXT_TYPE: - fiberTag = ContextProvider; - break getTag; + if (enableRenderableContext) { + fiberTag = ContextProvider; + break getTag; + } else { + fiberTag = ContextConsumer; + break getTag; + } case REACT_CONSUMER_TYPE: - // This is a consumer - fiberTag = ContextConsumer; - break getTag; + if (enableRenderableContext) { + fiberTag = ContextConsumer; + break getTag; + } + // Fall through case REACT_FORWARD_REF_TYPE: fiberTag = ForwardRef; if (__DEV__) { diff --git a/packages/react-reconciler/src/ReactFiberBeginWork.js b/packages/react-reconciler/src/ReactFiberBeginWork.js index 54d4ee67136e8..94749f8181cee 100644 --- a/packages/react-reconciler/src/ReactFiberBeginWork.js +++ b/packages/react-reconciler/src/ReactFiberBeginWork.js @@ -110,6 +110,7 @@ import { enableFormActions, enableAsyncActions, enablePostpone, + enableRenderableContext, } from 'shared/ReactFeatureFlags'; import isArray from 'shared/isArray'; import shallowEqual from 'shared/shallowEqual'; @@ -3528,7 +3529,12 @@ function updateContextProvider( workInProgress: Fiber, renderLanes: Lanes, ) { - const context: ReactContext = workInProgress.type; + let context: ReactContext; + if (enableRenderableContext) { + context = workInProgress.type; + } else { + context = workInProgress.type._context; + } const newProps = workInProgress.pendingProps; const oldProps = workInProgress.memoizedProps; @@ -3590,9 +3596,18 @@ function updateContextConsumer( workInProgress: Fiber, renderLanes: Lanes, ) { - const consumerType: ReactConsumerType = workInProgress.type; - const context = consumerType._context; - + let context: ReactContext; + if (enableRenderableContext) { + const consumerType: ReactConsumerType = workInProgress.type; + context = consumerType._context; + } else { + context = workInProgress.type; + if (__DEV__) { + if ((context: any)._context !== undefined) { + context = (context: any)._context; + } + } + } const newProps = workInProgress.pendingProps; const render = newProps.children; @@ -3838,7 +3853,12 @@ function attemptEarlyBailoutIfNoScheduledUpdate( break; case ContextProvider: { const newValue = workInProgress.memoizedProps.value; - const context: ReactContext = workInProgress.type; + let context: ReactContext; + if (enableRenderableContext) { + context = workInProgress.type; + } else { + context = workInProgress.type._context; + } pushProvider(workInProgress, context, newValue); break; } diff --git a/packages/react-reconciler/src/ReactFiberCompleteWork.js b/packages/react-reconciler/src/ReactFiberCompleteWork.js index 9a4269af8f38d..639cec6cbf0d5 100644 --- a/packages/react-reconciler/src/ReactFiberCompleteWork.js +++ b/packages/react-reconciler/src/ReactFiberCompleteWork.js @@ -39,6 +39,7 @@ import { enableCache, enableTransitionTracing, enableFloat, + enableRenderableContext, passChildrenWhenCloningPersistedNodes, } from 'shared/ReactFeatureFlags'; @@ -1505,7 +1506,12 @@ function completeWork( return null; case ContextProvider: // Pop provider fiber - const context: ReactContext = workInProgress.type; + let context: ReactContext; + if (enableRenderableContext) { + context = workInProgress.type; + } else { + context = workInProgress.type._context; + } popProvider(context, workInProgress); bubbleProperties(workInProgress); return null; diff --git a/packages/react-reconciler/src/ReactFiberNewContext.js b/packages/react-reconciler/src/ReactFiberNewContext.js index 3c1ff79e25149..5d9b165635a1e 100644 --- a/packages/react-reconciler/src/ReactFiberNewContext.js +++ b/packages/react-reconciler/src/ReactFiberNewContext.js @@ -46,6 +46,7 @@ import { enableLazyContextPropagation, enableFormActions, enableAsyncActions, + enableRenderableContext, } from 'shared/ReactFeatureFlags'; import { getHostTransitionProvider, @@ -561,7 +562,12 @@ function propagateParentContextChanges( const oldProps = currentParent.memoizedProps; if (oldProps !== null) { - const context: ReactContext = parent.type; + let context: ReactContext; + if (enableRenderableContext) { + context = parent.type; + } else { + context = parent.type._context; + } const newProps = parent.pendingProps; const newValue = newProps.value; diff --git a/packages/react-reconciler/src/ReactFiberScope.js b/packages/react-reconciler/src/ReactFiberScope.js index 8f9f1cdea577c..0cb1c62ba8d02 100644 --- a/packages/react-reconciler/src/ReactFiberScope.js +++ b/packages/react-reconciler/src/ReactFiberScope.js @@ -22,7 +22,10 @@ import { import {isFiberSuspenseAndTimedOut} from './ReactFiberTreeReflection'; import {HostComponent, ScopeComponent, ContextProvider} from './ReactWorkTags'; -import {enableScopeAPI} from 'shared/ReactFeatureFlags'; +import { + enableScopeAPI, + enableRenderableContext, +} from 'shared/ReactFeatureFlags'; function getSuspenseFallbackChild(fiber: Fiber): Fiber | null { return ((((fiber.child: any): Fiber).sibling: any): Fiber).child; @@ -113,7 +116,10 @@ function collectNearestContextValues( context: ReactContext, childContextValues: Array, ): void { - if (node.tag === ContextProvider && node.type === context) { + if ( + node.tag === ContextProvider && + (enableRenderableContext ? node.type : node.type._context) === context + ) { const contextValue = node.memoizedProps.value; childContextValues.push(contextValue); } else { diff --git a/packages/react-reconciler/src/ReactFiberUnwindWork.js b/packages/react-reconciler/src/ReactFiberUnwindWork.js index d9a9eb5c77860..cc77647cb8556 100644 --- a/packages/react-reconciler/src/ReactFiberUnwindWork.js +++ b/packages/react-reconciler/src/ReactFiberUnwindWork.js @@ -35,6 +35,7 @@ import { enableProfilerTimer, enableCache, enableTransitionTracing, + enableRenderableContext, } from 'shared/ReactFeatureFlags'; import {popHostContainer, popHostContext} from './ReactFiberHostContext'; @@ -160,7 +161,12 @@ function unwindWork( popHostContainer(workInProgress); return null; case ContextProvider: - const context: ReactContext = workInProgress.type; + let context: ReactContext; + if (enableRenderableContext) { + context = workInProgress.type; + } else { + context = workInProgress.type._context; + } popProvider(context, workInProgress); return null; case OffscreenComponent: @@ -250,7 +256,12 @@ function unwindInterruptedWork( popSuspenseListContext(interruptedWork); break; case ContextProvider: - const context: ReactContext = interruptedWork.type; + let context: ReactContext; + if (enableRenderableContext) { + context = interruptedWork.type; + } else { + context = interruptedWork.type._context; + } popProvider(context, interruptedWork); break; case OffscreenComponent: diff --git a/packages/react-reconciler/src/__tests__/ReactNewContext-test.js b/packages/react-reconciler/src/__tests__/ReactNewContext-test.js index a7be10d04436f..a6cabd6c641f7 100644 --- a/packages/react-reconciler/src/__tests__/ReactNewContext-test.js +++ b/packages/react-reconciler/src/__tests__/ReactNewContext-test.js @@ -1339,6 +1339,7 @@ describe('ReactNewContext', () => { ); }); + // @gate enableRenderableContext || !__DEV__ it('warns when passed a consumer', async () => { const Context = React.createContext(0); function Foo() { @@ -1635,6 +1636,7 @@ Context fuzz tester error! Copy and paste the following line into the test suite }); }); + // @gate enableRenderableContext it('should treat Context as Context.Provider', async () => { const BarContext = React.createContext({value: 'bar-initial'}); expect(BarContext.Provider).toBe(BarContext); diff --git a/packages/react-reconciler/src/getComponentNameFromFiber.js b/packages/react-reconciler/src/getComponentNameFromFiber.js index 61b100a856429..1a8464835ce4f 100644 --- a/packages/react-reconciler/src/getComponentNameFromFiber.js +++ b/packages/react-reconciler/src/getComponentNameFromFiber.js @@ -10,7 +10,10 @@ import type {ReactContext, ReactConsumerType} from 'shared/ReactTypes'; import type {Fiber} from './ReactInternalTypes'; -import {enableLegacyHidden} from 'shared/ReactFeatureFlags'; +import { + enableLegacyHidden, + enableRenderableContext, +} from 'shared/ReactFeatureFlags'; import { FunctionComponent, @@ -68,11 +71,21 @@ export default function getComponentNameFromFiber(fiber: Fiber): string | null { case CacheComponent: return 'Cache'; case ContextConsumer: - const consumer: ReactConsumerType = (type: any); - return getContextName(consumer._context) + '.Consumer'; + if (enableRenderableContext) { + const consumer: ReactConsumerType = (type: any); + return getContextName(consumer._context) + '.Consumer'; + } else { + const context: ReactContext = (type: any); + return getContextName(context) + '.Consumer'; + } case ContextProvider: - const context: ReactContext = (type: any); - return getContextName(context) + '.Provider'; + if (enableRenderableContext) { + const context: ReactContext = (type: any); + return getContextName(context) + '.Provider'; + } else { + const provider = (type: any); + return getContextName(provider._context) + '.Provider'; + } case DehydratedFragment: return 'DehydratedFragment'; case ForwardRef: diff --git a/packages/react-server/src/ReactFizzServer.js b/packages/react-server/src/ReactFizzServer.js index d76b906ff7494..ade81cc78f285 100644 --- a/packages/react-server/src/ReactFizzServer.js +++ b/packages/react-server/src/ReactFizzServer.js @@ -9,29 +9,30 @@ import type { Destination, - Chunk, - PrecomputedChunk, + Chunk, + PrecomputedChunk, } from './ReactServerStreamConfig'; import type { ReactNodeList, - ReactContext, - ReactConsumerType, - OffscreenMode, - Wakeable, - Thenable, - ReactFormState, + ReactContext, + ReactConsumerType, + OffscreenMode, + Wakeable, + Thenable, + ReactFormState, } from 'shared/ReactTypes'; -import type {LazyComponent as LazyComponentType} from 'react/src/ReactLazy'; +import type { LazyComponent as LazyComponentType } from 'react/src/ReactLazy'; import type { RenderState, - ResumableState, - FormatContext, - HoistableState, + ResumableState, + FormatContext, + HoistableState, } from './ReactFizzConfig'; -import type {ContextSnapshot} from './ReactFizzNewContext'; -import type {ComponentStackNode} from './ReactFizzComponentStack'; -import type {TreeContext} from './ReactFizzTreeContext'; -import type {ThenableState} from './ReactFizzThenable'; +import type { ContextSnapshot } from './ReactFizzNewContext'; +import type { ComponentStackNode } from './ReactFizzComponentStack'; +import type { TreeContext } from './ReactFizzTreeContext'; +import type { ThenableState } from './ReactFizzThenable'; +import { enableRenderableContext } from 'shared/ReactFeatureFlags'; import { scheduleWork, @@ -109,9 +110,9 @@ import { getFormStateCount, getFormStateMatchingIndex, } from './ReactFizzHooks'; -import {DefaultCacheDispatcher} from './ReactFizzCache'; -import {getStackByComponentStackNode} from './ReactFizzComponentStack'; -import {emptyTreeContext, pushTreeContext} from './ReactFizzTreeContext'; +import { DefaultCacheDispatcher } from './ReactFizzCache'; +import { getStackByComponentStackNode } from './ReactFizzComponentStack'; +import { emptyTreeContext, pushTreeContext } from './ReactFizzTreeContext'; import { getIteratorFn, @@ -127,6 +128,7 @@ import { REACT_FRAGMENT_TYPE, REACT_FORWARD_REF_TYPE, REACT_MEMO_TYPE, + REACT_PROVIDER_TYPE, REACT_CONTEXT_TYPE, REACT_CONSUMER_TYPE, REACT_SCOPE_TYPE, @@ -147,8 +149,8 @@ import { import assign from 'shared/assign'; import getComponentNameFromType from 'shared/getComponentNameFromType'; import isArray from 'shared/isArray'; -import {SuspenseException, getSuspendedThenable} from './ReactFizzThenable'; -import type {Postpone} from 'react/src/ReactPostpone'; +import { SuspenseException, getSuspendedThenable } from './ReactFizzThenable'; +import type { Postpone } from 'react/src/ReactPostpone'; const ReactCurrentDispatcher = ReactSharedInternals.ReactCurrentDispatcher; const ReactCurrentCache = ReactSharedInternals.ReactCurrentCache; @@ -166,7 +168,7 @@ export type KeyNode = [ type ResumeSlots = | null // nothing to resume | number // resume with segment ID at the root position - | {[index: number]: number}; // resume with segmentID at the index + | { [index: number]: number }; // resume with segmentID at the index type ReplaySuspenseBoundary = [ string | null /* name */, @@ -179,11 +181,11 @@ type ReplaySuspenseBoundary = [ type ReplayNode = | [ - string | null /* name */, - string | number /* key */, - Array /* keyed children */, - ResumeSlots /* resumable slots */, - ] + string | null /* name */, + string | number /* key */, + Array /* keyed children */, + ResumeSlots /* resumable slots */, + ] | ReplaySuspenseBoundary; type PostponedHoles = { @@ -277,15 +279,15 @@ type Segment = { parentFlushed: boolean, // typically a segment will be flushed by its parent, except if its parent was already flushed id: number, // starts as 0 and is lazily assigned if the parent flushes early +index: number, // the index within the parent's chunks or 0 at the root - +chunks: Array, - +children: Array, - // The context that this segment was created in. - parentFormatContext: FormatContext, - // If this segment represents a fallback, this is the content that will replace that fallback. - +boundary: null | SuspenseBoundary, - // used to discern when text separator boundaries are needed - lastPushedText: boolean, - textEmbedded: boolean, + +chunks: Array < Chunk | PrecomputedChunk >, + +children: Array < Segment >, + // The context that this segment was created in. + parentFormatContext: FormatContext, + // If this segment represents a fallback, this is the content that will replace that fallback. + +boundary: null | SuspenseBoundary, + // used to discern when text separator boundaries are needed + lastPushedText: boolean, + textEmbedded: boolean, }; const OPEN = 0; @@ -297,41 +299,41 @@ export opaque type Request = { flushScheduled: boolean, +resumableState: ResumableState, +renderState: RenderState, - +rootFormatContext: FormatContext, - +progressiveChunkSize: number, - status: 0 | 1 | 2, - fatalError: mixed, - nextSegmentId: number, - allPendingTasks: number, // when it reaches zero, we can close the connection. - pendingRootTasks: number, // when this reaches zero, we've finished at least the root boundary. - completedRootSegment: null | Segment, // Completed but not yet flushed root segments. - abortableTasks: Set, - pingedTasks: Array, // High priority tasks that should be worked on first. - // Queues to flush in order of priority - clientRenderedBoundaries: Array, // Errored or client rendered but not yet flushed. - completedBoundaries: Array, // Completed but not yet fully flushed boundaries to show. - partialBoundaries: Array, // Partially completed boundaries that can flush its segments early. - trackedPostpones: null | PostponedHoles, // Gets set to non-null while we want to track postponed holes. I.e. during a prerender. - // onError is called when an error happens anywhere in the tree. It might recover. - // The return string is used in production primarily to avoid leaking internals, secondarily to save bytes. - // Returning null/undefined will cause a defualt error message in production - onError: (error: mixed, errorInfo: ThrownInfo) => ?string, - // onAllReady is called when all pending task is done but it may not have flushed yet. - // This is a good time to start writing if you want only HTML and no intermediate steps. - onAllReady: () => void, - // onShellReady is called when there is at least a root fallback ready to show. - // Typically you don't need this callback because it's best practice to always have a - // root fallback ready so there's no need to wait. - onShellReady: () => void, - // onShellError is called when the shell didn't complete. That means you probably want to - // emit a different response to the stream instead. - onShellError: (error: mixed) => void, - onFatalError: (error: mixed) => void, - // onPostpone is called when postpone() is called anywhere in the tree, which will defer - // rendering - e.g. to the client. This is considered intentional and not an error. - onPostpone: (reason: string, postponeInfo: ThrownInfo) => void, - // Form state that was the result of an MPA submission, if it was provided. - formState: null | ReactFormState, + +rootFormatContext: FormatContext, + +progressiveChunkSize: number, + status: 0 | 1 | 2, + fatalError: mixed, + nextSegmentId: number, + allPendingTasks: number, // when it reaches zero, we can close the connection. + pendingRootTasks: number, // when this reaches zero, we've finished at least the root boundary. + completedRootSegment: null | Segment, // Completed but not yet flushed root segments. + abortableTasks: Set < Task >, + pingedTasks: Array < Task >, // High priority tasks that should be worked on first. + // Queues to flush in order of priority + clientRenderedBoundaries: Array < SuspenseBoundary >, // Errored or client rendered but not yet flushed. + completedBoundaries: Array < SuspenseBoundary >, // Completed but not yet fully flushed boundaries to show. + partialBoundaries: Array < SuspenseBoundary >, // Partially completed boundaries that can flush its segments early. + trackedPostpones: null | PostponedHoles, // Gets set to non-null while we want to track postponed holes. I.e. during a prerender. + // onError is called when an error happens anywhere in the tree. It might recover. + // The return string is used in production primarily to avoid leaking internals, secondarily to save bytes. + // Returning null/undefined will cause a defualt error message in production + onError: (error: mixed, errorInfo: ThrownInfo) => ? string, + // onAllReady is called when all pending task is done but it may not have flushed yet. + // This is a good time to start writing if you want only HTML and no intermediate steps. + onAllReady: () => void, + // onShellReady is called when there is at least a root fallback ready to show. + // Typically you don't need this callback because it's best practice to always have a + // root fallback ready so there's no need to wait. + onShellReady: () => void, + // onShellError is called when the shell didn't complete. That means you probably want to + // emit a different response to the stream instead. + onShellError: (error: mixed) => void, + onFatalError: (error: mixed) => void, + // onPostpone is called when postpone() is called anywhere in the tree, which will defer + // rendering - e.g. to the client. This is considered intentional and not an error. + onPostpone: (reason: string, postponeInfo: ThrownInfo) => void, + // Form state that was the result of an MPA submission, if it was provided. + formState: null | ReactFormState < any, any >, }; // This is a default heuristic for how to split up the HTML content into progressive @@ -356,7 +358,7 @@ function defaultErrorHandler(error: mixed) { return null; } -function noop(): void {} +function noop(): void { } export function createRequest( children: ReactNodeList, @@ -364,7 +366,7 @@ export function createRequest( renderState: RenderState, rootFormatContext: FormatContext, progressiveChunkSize: void | number, - onError: void | ((error: mixed, errorInfo: ErrorInfo) => ?string), + onError: void | ((error: mixed, errorInfo: ErrorInfo) =>?string), onAllReady: void | (() => void), onShellReady: void | (() => void), onShellError: void | ((error: mixed) => void), @@ -444,7 +446,7 @@ export function createPrerenderRequest( renderState: RenderState, rootFormatContext: FormatContext, progressiveChunkSize: void | number, - onError: void | ((error: mixed, errorInfo: ErrorInfo) => ?string), + onError: void | ((error: mixed, errorInfo: ErrorInfo) =>?string), onAllReady: void | (() => void), onShellReady: void | (() => void), onShellError: void | ((error: mixed) => void), @@ -478,7 +480,7 @@ export function resumeRequest( children: ReactNodeList, postponedState: PostponedState, renderState: RenderState, - onError: void | ((error: mixed, errorInfo: ErrorInfo) => ?string), + onError: void | ((error: mixed, errorInfo: ErrorInfo) =>?string), onAllReady: void | (() => void), onShellReady: void | (() => void), onShellError: void | ((error: mixed) => void), @@ -1106,7 +1108,7 @@ function replaySuspenseBoundary( // we're writing to. If something suspends, it'll spawn new suspended task with that context. task.blockedBoundary = resumedBoundary; task.hoistableState = resumedBoundary.contentState; - task.replay = {nodes: childNodes, slots: childSlots, pendingTasks: 1}; + task.replay = { nodes: childNodes, slots: childSlots, pendingTasks: 1 }; try { // We use the safe form because we don't handle suspending here. Only error handling. @@ -1115,7 +1117,7 @@ function replaySuspenseBoundary( if (task.replay.pendingTasks === 1 && task.replay.nodes.length > 0) { throw new Error( "Couldn't find all resumable slots by key/index during replaying. " + - "The tree doesn't match so React will fallback to client rendering.", + "The tree doesn't match so React will fallback to client rendering.", ); } task.replay.pendingTasks--; @@ -1336,7 +1338,7 @@ function finishClassComponent( if (!didWarnAboutReassigningProps) { console.error( 'It looks like %s is reassigning its own `this.props` while rendering. ' + - 'This is not supported and can lead to confusing bugs.', + 'This is not supported and can lead to confusing bugs.', getComponentNameFromType(Component) || 'a component', ); } @@ -1385,12 +1387,12 @@ function renderClassComponent( task.componentStack = previousComponentStack; } -const didWarnAboutBadClass: {[string]: boolean} = {}; -const didWarnAboutModulePatternComponent: {[string]: boolean} = {}; -const didWarnAboutContextTypeOnFunctionComponent: {[string]: boolean} = {}; -const didWarnAboutGetDerivedStateOnFunctionComponent: {[string]: boolean} = {}; +const didWarnAboutBadClass: { [string]: boolean } = {}; +const didWarnAboutModulePatternComponent: { [string]: boolean } = {}; +const didWarnAboutContextTypeOnFunctionComponent: { [string]: boolean } = {}; +const didWarnAboutGetDerivedStateOnFunctionComponent: { [string]: boolean } = {}; let didWarnAboutReassigningProps = false; -const didWarnAboutDefaultPropsOnFunctionComponent: {[string]: boolean} = {}; +const didWarnAboutDefaultPropsOnFunctionComponent: { [string]: boolean } = {}; let didWarnAboutGenerators = false; let didWarnAboutMaps = false; @@ -1420,7 +1422,7 @@ function renderIndeterminateComponent( if (!didWarnAboutBadClass[componentName]) { console.error( "The <%s /> component appears to have a render method, but doesn't extend React.Component. " + - 'This is likely to cause errors. Change %s to extend React.Component instead.', + 'This is likely to cause errors. Change %s to extend React.Component instead.', componentName, componentName, ); @@ -1454,10 +1456,10 @@ function renderIndeterminateComponent( if (!didWarnAboutModulePatternComponent[componentName]) { console.error( 'The <%s /> component appears to be a function component that returns a class instance. ' + - 'Change %s to a class that extends React.Component instead. ' + - "If you can't use a class try assigning the prototype on the function as a workaround. " + - "`%s.prototype = React.Component.prototype`. Don't use an arrow function since it " + - 'cannot be called with `new` by React.', + 'Change %s to a class that extends React.Component instead. ' + + "If you can't use a class try assigning the prototype on the function as a workaround. " + + "`%s.prototype = React.Component.prototype`. Don't use an arrow function since it " + + 'cannot be called with `new` by React.', componentName, componentName, componentName, @@ -1481,10 +1483,10 @@ function renderIndeterminateComponent( if (!didWarnAboutModulePatternComponent[componentName]) { console.error( 'The <%s /> component appears to be a function component that returns a class instance. ' + - 'Change %s to a class that extends React.Component instead. ' + - "If you can't use a class try assigning the prototype on the function as a workaround. " + - "`%s.prototype = React.Component.prototype`. Don't use an arrow function since it " + - 'cannot be called with `new` by React.', + 'Change %s to a class that extends React.Component instead. ' + + "If you can't use a class try assigning the prototype on the function as a workaround. " + + "`%s.prototype = React.Component.prototype`. Don't use an arrow function since it " + + 'cannot be called with `new` by React.', componentName, componentName, componentName, @@ -1501,7 +1503,7 @@ function renderIndeterminateComponent( if (disableLegacyContext && Component.contextTypes) { console.error( '%s uses the legacy contextTypes API which is no longer supported. ' + - 'Use React.createContext() with React.useContext() instead.', + 'Use React.createContext() with React.useContext() instead.', getComponentNameFromType(Component) || 'Unknown', ); } @@ -1598,7 +1600,7 @@ function validateFunctionComponentInDev(Component: any): void { if (!didWarnAboutDefaultPropsOnFunctionComponent[componentName]) { console.error( '%s: Support for defaultProps will be removed from function components ' + - 'in a future major release. Use JavaScript default parameters instead.', + 'in a future major release. Use JavaScript default parameters instead.', componentName, ); didWarnAboutDefaultPropsOnFunctionComponent[componentName] = true; @@ -1699,19 +1701,18 @@ function renderContextConsumer( request: Request, task: Task, keyPath: KeyNode, - type: ReactConsumerType, + context: ReactContext, props: Object, ): void { - const context = type._context; const render = props.children; if (__DEV__) { if (typeof render !== 'function') { console.error( 'A context consumer was rendered with multiple children, or a child ' + - "that isn't a function. A context consumer expects a single child " + - 'that is a function. If you did pass a function, make sure there ' + - 'is no trailing or leading whitespace around it.', + "that isn't a function. A context consumer expects a single child " + + 'that is a function. If you did pass a function, make sure there ' + + 'is no trailing or leading whitespace around it.', ); } } @@ -1882,13 +1883,38 @@ function renderElement( renderMemo(request, task, keyPath, type, props, ref); return; } + case REACT_PROVIDER_TYPE: { + if (!enableRenderableContext) { + const context: ReactContext = (type: any)._context; + renderContextProvider(request, task, keyPath, context, props); + return; + } + // Fall through + } case REACT_CONTEXT_TYPE: { - renderContextProvider(request, task, keyPath, type, props); - return; + if (enableRenderableContext) { + const context = type; + renderContextProvider(request, task, keyPath, context, props); + return; + } else { + let context: ReactContext = (type: any); + if (__DEV__) { + if ((context: any)._context !== undefined) { + context = (context: any)._context; + } + } + renderContextConsumer(request, task, keyPath, context, props); + return; + } } case REACT_CONSUMER_TYPE: { - renderContextConsumer(request, task, keyPath, type, props); - return; + if (enableRenderableContext) { + const context: ReactContext = (type: ReactConsumerType) + ._context; + renderContextConsumer(request, task, keyPath, context, props); + return; + } + // Fall through } case REACT_LAZY_TYPE: { renderLazyComponent(request, task, keyPath, type, props); @@ -1914,8 +1940,8 @@ function renderElement( throw new Error( 'Element type is invalid: expected a string (for built-in ' + - 'components) or a class/function (for composite components) ' + - `but got: ${type == null ? type : typeof type}.${info}`, + 'components) or a class/function (for composite components) ' + + `but got: ${type == null ? type : typeof type}.${info}`, ); } @@ -1986,17 +2012,17 @@ function replayElement( if (name !== null && name !== node[0]) { throw new Error( 'Expected the resume to render <' + - (node[0]: any) + - '> in this slot but instead it rendered <' + - name + - '>. ' + - "The tree doesn't match so React will fallback to client rendering.", + (node[0]: any) + + '> in this slot but instead it rendered <' + + name + + '>. ' + + "The tree doesn't match so React will fallback to client rendering.", ); } const childNodes = node[2]; const childSlots = node[3]; const currentNode = task.node; - task.replay = {nodes: childNodes, slots: childSlots, pendingTasks: 1}; + task.replay = { nodes: childNodes, slots: childSlots, pendingTasks: 1 }; try { renderElement(request, task, keyPath, type, props, ref); if ( @@ -2006,7 +2032,7 @@ function replayElement( ) { throw new Error( "Couldn't find all resumable slots by key/index during replaying. " + - "The tree doesn't match so React will fallback to client rendering.", + "The tree doesn't match so React will fallback to client rendering.", ); } task.replay.pendingTasks--; @@ -2046,11 +2072,11 @@ function replayElement( const expectedType = 'Suspense'; throw new Error( 'Expected the resume to render <' + - expectedType + - '> in this slot but instead it rendered <' + - (getComponentNameFromType(type) || 'Unknown') + - '>. ' + - "The tree doesn't match so React will fallback to client rendering.", + expectedType + + '> in this slot but instead it rendered <' + + (getComponentNameFromType(type) || 'Unknown') + + '>. ' + + "The tree doesn't match so React will fallback to client rendering.", ); } // Matched a replayable path. @@ -2087,10 +2113,10 @@ function validateIterable(iterable, iteratorFn: Function): void { if (!didWarnAboutGenerators) { console.error( 'Using Generators as children is unsupported and will likely yield ' + - 'unexpected results because enumerating a generator mutates it. ' + - 'You may convert it to an array with `Array.from()` or the ' + - '`[...spread]` operator before rendering. Keep in mind ' + - 'you might need to polyfill these features for older browsers.', + 'unexpected results because enumerating a generator mutates it. ' + + 'You may convert it to an array with `Array.from()` or the ' + + '`[...spread]` operator before rendering. Keep in mind ' + + 'you might need to polyfill these features for older browsers.', ); } didWarnAboutGenerators = true; @@ -2101,7 +2127,7 @@ function validateIterable(iterable, iteratorFn: Function): void { if (!didWarnAboutMaps) { console.error( 'Using Maps as children is not supported. ' + - 'Use an array of keyed ReactElements instead.', + 'Use an array of keyed ReactElements instead.', ); } didWarnAboutMaps = true; @@ -2167,10 +2193,10 @@ function renderNodeDestructive( return; } case REACT_PORTAL_TYPE: - throw new Error( - 'Portals are not currently supported by the server renderer. ' + - 'Render them conditionally so that they only appear on the client render.', - ); + throw new Error( + 'Portals are not currently supported by the server renderer. ' + + 'Render them conditionally so that they only appear on the client render.', + ); case REACT_LAZY_TYPE: { const previousComponentStack = task.componentStack; task.componentStack = createBuiltInComponentStack(task, 'Lazy'); @@ -2258,12 +2284,12 @@ function renderNodeDestructive( throw new Error( `Objects are not valid as a React child (found: ${ - childString === '[object Object]' - ? 'object with keys {' + Object.keys(node).join(', ') + '}' - : childString + childString === '[object Object]' + ? 'object with keys {' + Object.keys(node).join(', ') + '}' + : childString }). ` + - 'If you meant to render a collection of children, use an array ' + - 'instead.', + 'If you meant to render a collection of children, use an array ' + + 'instead.', ); } @@ -2303,8 +2329,8 @@ function renderNodeDestructive( if (typeof node === 'function') { console.error( 'Functions are not valid as a React child. This may happen if ' + - 'you return a Component instead of from render. ' + - 'Or maybe you meant to call this function rather than return it.', + 'you return a Component instead of from render. ' + + 'Or maybe you meant to call this function rather than return it.', ); } } @@ -2328,13 +2354,13 @@ function replayFragment( // Matched a replayable path. const childNodes = node[2]; const childSlots = node[3]; - task.replay = {nodes: childNodes, slots: childSlots, pendingTasks: 1}; + task.replay = { nodes: childNodes, slots: childSlots, pendingTasks: 1 }; try { renderChildrenArray(request, task, children, -1); if (task.replay.pendingTasks === 1 && task.replay.nodes.length > 0) { throw new Error( "Couldn't find all resumable slots by key/index during replaying. " + - "The tree doesn't match so React will fallback to client rendering.", + "The tree doesn't match so React will fallback to client rendering.", ); } task.replay.pendingTasks--; @@ -2557,18 +2583,18 @@ function trackPostpone( if (keyPath === null) { slots = trackedPostpones.rootSlots; if (slots === null) { - slots = trackedPostpones.rootSlots = ({}: {[index: number]: number}); + slots = trackedPostpones.rootSlots = ({}: { [index: number]: number }); } else if (typeof slots === 'number') { throw new Error( 'It should not be possible to postpone both at the root of an element ' + - 'as well as a slot below. This is a bug in React.', + 'as well as a slot below. This is a bug in React.', ); } } else { const workingMap = trackedPostpones.workingMap; let resumableNode = workingMap.get(keyPath); if (resumableNode === undefined) { - slots = ({}: {[index: number]: number}); + slots = ({}: { [index: number]: number }); resumableNode = ([ keyPath[1], keyPath[2], @@ -2580,11 +2606,11 @@ function trackPostpone( } else { slots = resumableNode[3]; if (slots === null) { - slots = resumableNode[3] = ({}: {[index: number]: number}); + slots = resumableNode[3] = ({}: { [index: number]: number }); } else if (typeof slots === 'number') { throw new Error( 'It should not be possible to postpone both at the root of an element ' + - 'as well as a slot below. This is a bug in React.', + 'as well as a slot below. This is a bug in React.', ); } } @@ -2753,11 +2779,11 @@ function renderNode( x = thrownValue === SuspenseException ? // This is a special type of exception used for Suspense. For historical - // reasons, the rest of the Suspense implementation expects the thrown - // value to be a thenable, because before `use` existed that was the - // (unstable) API for suspending. This implementation detail can change - // later, once we deprecate the old API in favor of `use`. - getSuspendedThenable() + // reasons, the rest of the Suspense implementation expects the thrown + // value to be a thenable, because before `use` existed that was the + // (unstable) API for suspending. This implementation detail can change + // later, once we deprecate the old API in favor of `use`. + getSuspendedThenable() : thrownValue; if (typeof x === 'object' && x !== null) { @@ -2805,11 +2831,11 @@ function renderNode( x = thrownValue === SuspenseException ? // This is a special type of exception used for Suspense. For historical - // reasons, the rest of the Suspense implementation expects the thrown - // value to be a thenable, because before `use` existed that was the - // (unstable) API for suspending. This implementation detail can change - // later, once we deprecate the old API in favor of `use`. - getSuspendedThenable() + // reasons, the rest of the Suspense implementation expects the thrown + // value to be a thenable, because before `use` existed that was the + // (unstable) API for suspending. This implementation detail can change + // later, once we deprecate the old API in favor of `use`. + getSuspendedThenable() : thrownValue; if (typeof x === 'object' && x !== null) { @@ -3065,7 +3091,7 @@ function abortRemainingReplayNodes( if (boundary === null) { throw new Error( 'We should not have any resumable nodes in the shell. ' + - 'This is a bug in React.', + 'This is a bug in React.', ); } else if (boundary.status !== CLIENT_RENDERED) { boundary.status = CLIENT_RENDERED; @@ -3108,7 +3134,7 @@ function abortTask(task: Task, request: Request, error: mixed): void { const postponeInstance: Postpone = (error: any); const fatal = new Error( 'The render was aborted with postpone when the shell is incomplete. Reason: ' + - postponeInstance.message, + postponeInstance.message, ); logRecoverableError(request, fatal, errorInfo); fatalError(request, fatal); @@ -3256,10 +3282,10 @@ function completeAll(request: Request) { const shellComplete = request.trackedPostpones === null ? // Render, we assume it is completed - true + true : // Prerender Request, we use the state of the root segment - request.completedRootSegment === null || - request.completedRootSegment.status !== POSTPONED; + request.completedRootSegment === null || + request.completedRootSegment.status !== POSTPONED; safelyEmitEarlyPreloads(request, shellComplete); const onAllReady = request.onAllReady; onAllReady(); @@ -3430,11 +3456,11 @@ function retryRenderTask( const x = thrownValue === SuspenseException ? // This is a special type of exception used for Suspense. For historical - // reasons, the rest of the Suspense implementation expects the thrown - // value to be a thenable, because before `use` existed that was the - // (unstable) API for suspending. This implementation detail can change - // later, once we deprecate the old API in favor of `use`. - getSuspendedThenable() + // reasons, the rest of the Suspense implementation expects the thrown + // value to be a thenable, because before `use` existed that was the + // (unstable) API for suspending. This implementation detail can change + // later, once we deprecate the old API in favor of `use`. + getSuspendedThenable() : thrownValue; if (typeof x === 'object' && x !== null) { @@ -3502,7 +3528,7 @@ function retryReplayTask(request: Request, task: ReplayTask): void { if (task.replay.pendingTasks === 1 && task.replay.nodes.length > 0) { throw new Error( "Couldn't find all resumable slots by key/index during replaying. " + - "The tree doesn't match so React will fallback to client rendering.", + "The tree doesn't match so React will fallback to client rendering.", ); } task.replay.pendingTasks--; @@ -3515,11 +3541,11 @@ function retryReplayTask(request: Request, task: ReplayTask): void { const x = thrownValue === SuspenseException ? // This is a special type of exception used for Suspense. For historical - // reasons, the rest of the Suspense implementation expects the thrown - // value to be a thenable, because before `use` existed that was the - // (unstable) API for suspending. This implementation detail can change - // later, once we deprecate the old API in favor of `use`. - getSuspendedThenable() + // reasons, the rest of the Suspense implementation expects the thrown + // value to be a thenable, because before `use` existed that was the + // (unstable) API for suspending. This implementation detail can change + // later, once we deprecate the old API in favor of `use`. + getSuspendedThenable() : thrownValue; if (typeof x === 'object' && x !== null) { @@ -4145,11 +4171,11 @@ export function prepareForStartFlowingIfBeforeAllReady(request: Request) { const shellComplete = request.trackedPostpones === null ? // Render Request, we define shell complete by the pending root tasks - request.pendingRootTasks === 0 + request.pendingRootTasks === 0 : // Prerender Request, we define shell complete by completedRootSegemtn request.completedRootSegment === null - ? request.pendingRootTasks === 0 - : request.completedRootSegment.status !== POSTPONED; + ? request.pendingRootTasks === 0 + : request.completedRootSegment.status !== POSTPONED; safelyEmitEarlyPreloads(request, shellComplete); } diff --git a/packages/react/src/ReactContext.js b/packages/react/src/ReactContext.js index 5f4fc2585c2b7..24461ebfbb7bf 100644 --- a/packages/react/src/ReactContext.js +++ b/packages/react/src/ReactContext.js @@ -7,9 +7,14 @@ * @flow */ -import {REACT_CONSUMER_TYPE, REACT_CONTEXT_TYPE} from 'shared/ReactSymbols'; +import { + REACT_PROVIDER_TYPE, + REACT_CONSUMER_TYPE, + REACT_CONTEXT_TYPE, +} from 'shared/ReactSymbols'; import type {ReactContext} from 'shared/ReactTypes'; +import {enableRenderableContext} from 'shared/ReactFeatureFlags'; export function createContext(defaultValue: T): ReactContext { // TODO: Second argument used to be an optional `calculateChangedBits` @@ -32,11 +37,72 @@ export function createContext(defaultValue: T): ReactContext { Consumer: (null: any), }; - context.Provider = context; - context.Consumer = { - $$typeof: REACT_CONSUMER_TYPE, - _context: context, - }; + if (enableRenderableContext) { + context.Provider = context; + context.Consumer = { + $$typeof: REACT_CONSUMER_TYPE, + _context: context, + }; + } else { + (context: any).Provider = { + $$typeof: REACT_PROVIDER_TYPE, + _context: context, + }; + if (__DEV__) { + const Consumer: any = { + $$typeof: REACT_CONTEXT_TYPE, + _context: context, + }; + Object.defineProperties(Consumer, { + Provider: { + get() { + return context.Provider; + }, + set(_Provider: any) { + context.Provider = _Provider; + }, + }, + _currentValue: { + get() { + return context._currentValue; + }, + set(_currentValue: T) { + context._currentValue = _currentValue; + }, + }, + _currentValue2: { + get() { + return context._currentValue2; + }, + set(_currentValue2: T) { + context._currentValue2 = _currentValue2; + }, + }, + _threadCount: { + get() { + return context._threadCount; + }, + set(_threadCount: number) { + context._threadCount = _threadCount; + }, + }, + Consumer: { + get() { + return context.Consumer; + }, + }, + displayName: { + get() { + return context.displayName; + }, + set(displayName: void | string) {}, + }, + }); + (context: any).Consumer = Consumer; + } else { + (context: any).Consumer = context; + } + } if (__DEV__) { context._currentRenderer = null; diff --git a/packages/react/src/__tests__/ReactContextValidator-test.js b/packages/react/src/__tests__/ReactContextValidator-test.js index 2e3eaf6e28306..3f0fc6ad419ca 100644 --- a/packages/react/src/__tests__/ReactContextValidator-test.js +++ b/packages/react/src/__tests__/ReactContextValidator-test.js @@ -564,6 +564,7 @@ describe('ReactContextValidator', () => { ); }); + // @gate enableRenderableContext || !__DEV__ it('should warn if an invalid contextType is defined', () => { const Context = React.createContext(); class ComponentA extends React.Component { diff --git a/packages/shared/ReactFeatureFlags.js b/packages/shared/ReactFeatureFlags.js index 3d8a7cf563bc8..c2ffe2a652692 100644 --- a/packages/shared/ReactFeatureFlags.js +++ b/packages/shared/ReactFeatureFlags.js @@ -121,6 +121,8 @@ export const passChildrenWhenCloningPersistedNodes = false; export const enableUseDeferredValueInitialArg = __EXPERIMENTAL__; +export const enableRenderableContext = false; + /** * Enables an expiration time for retry lanes to avoid starvation. */ diff --git a/packages/shared/ReactSymbols.js b/packages/shared/ReactSymbols.js index 8f7de70ea2ad7..6dca3476be945 100644 --- a/packages/shared/ReactSymbols.js +++ b/packages/shared/ReactSymbols.js @@ -17,6 +17,7 @@ export const REACT_PORTAL_TYPE: symbol = Symbol.for('react.portal'); export const REACT_FRAGMENT_TYPE: symbol = Symbol.for('react.fragment'); export const REACT_STRICT_MODE_TYPE: symbol = Symbol.for('react.strict_mode'); export const REACT_PROFILER_TYPE: symbol = Symbol.for('react.profiler'); +export const REACT_PROVIDER_TYPE: symbol = Symbol.for('react.provider'); // TODO: Delete with enableRenderableContext export const REACT_CONSUMER_TYPE: symbol = Symbol.for('react.consumer'); export const REACT_CONTEXT_TYPE: symbol = Symbol.for('react.context'); export const REACT_FORWARD_REF_TYPE: symbol = Symbol.for('react.forward_ref'); diff --git a/packages/shared/forks/ReactFeatureFlags.native-fb.js b/packages/shared/forks/ReactFeatureFlags.native-fb.js index 497a5b6bcf3a6..3990e11a2ac9b 100644 --- a/packages/shared/forks/ReactFeatureFlags.native-fb.js +++ b/packages/shared/forks/ReactFeatureFlags.native-fb.js @@ -66,6 +66,7 @@ export const enableComponentStackLocations = false; export const enableLegacyFBSupport = false; export const enableFilterEmptyStringAttributesDOM = true; export const enableGetInspectorDataForInstanceInProduction = true; +export const enableRenderableContext = false; export const enableRetryLaneExpiration = false; export const retryLaneExpirationMs = 5000; diff --git a/packages/shared/forks/ReactFeatureFlags.native-oss.js b/packages/shared/forks/ReactFeatureFlags.native-oss.js index 67b220974b517..d29f699abbbde 100644 --- a/packages/shared/forks/ReactFeatureFlags.native-oss.js +++ b/packages/shared/forks/ReactFeatureFlags.native-oss.js @@ -49,6 +49,7 @@ export const enableComponentStackLocations = false; export const enableLegacyFBSupport = false; export const enableFilterEmptyStringAttributesDOM = true; export const enableGetInspectorDataForInstanceInProduction = false; +export const enableRenderableContext = false; export const enableRetryLaneExpiration = false; export const retryLaneExpirationMs = 5000; diff --git a/packages/shared/forks/ReactFeatureFlags.test-renderer.js b/packages/shared/forks/ReactFeatureFlags.test-renderer.js index 63025e79fb18b..6577d2a338893 100644 --- a/packages/shared/forks/ReactFeatureFlags.test-renderer.js +++ b/packages/shared/forks/ReactFeatureFlags.test-renderer.js @@ -49,6 +49,7 @@ export const enableComponentStackLocations = true; export const enableLegacyFBSupport = false; export const enableFilterEmptyStringAttributesDOM = true; export const enableGetInspectorDataForInstanceInProduction = false; +export const enableRenderableContext = false; export const enableRetryLaneExpiration = false; export const retryLaneExpirationMs = 5000; diff --git a/packages/shared/forks/ReactFeatureFlags.test-renderer.native.js b/packages/shared/forks/ReactFeatureFlags.test-renderer.native.js index 4baed7ddd2b07..fc53c654b16d9 100644 --- a/packages/shared/forks/ReactFeatureFlags.test-renderer.native.js +++ b/packages/shared/forks/ReactFeatureFlags.test-renderer.native.js @@ -51,6 +51,7 @@ export const enableUseEffectEventHook = false; export const enableClientRenderFallbackOnTextMismatch = true; export const enableUseRefAccessWarning = false; export const enableInfiniteRenderLoopDetection = false; +export const enableRenderableContext = false; export const enableRetryLaneExpiration = false; export const retryLaneExpirationMs = 5000; diff --git a/packages/shared/forks/ReactFeatureFlags.test-renderer.www.js b/packages/shared/forks/ReactFeatureFlags.test-renderer.www.js index 0105b2bed97c1..8e21c3b1a6286 100644 --- a/packages/shared/forks/ReactFeatureFlags.test-renderer.www.js +++ b/packages/shared/forks/ReactFeatureFlags.test-renderer.www.js @@ -49,6 +49,7 @@ export const enableComponentStackLocations = true; export const enableLegacyFBSupport = false; export const enableFilterEmptyStringAttributesDOM = true; export const enableGetInspectorDataForInstanceInProduction = false; +export const enableRenderableContext = false; export const enableRetryLaneExpiration = false; export const retryLaneExpirationMs = 5000; diff --git a/packages/shared/forks/ReactFeatureFlags.www-dynamic.js b/packages/shared/forks/ReactFeatureFlags.www-dynamic.js index 9b15bddded3fd..cd073c6429982 100644 --- a/packages/shared/forks/ReactFeatureFlags.www-dynamic.js +++ b/packages/shared/forks/ReactFeatureFlags.www-dynamic.js @@ -29,6 +29,7 @@ export const enableFormActions = __VARIANT__; export const alwaysThrottleRetries = __VARIANT__; export const enableDO_NOT_USE_disableStrictPassiveEffect = __VARIANT__; export const enableUseDeferredValueInitialArg = __VARIANT__; +export const enableRenderableContext = __VARIANT__; export const enableRetryLaneExpiration = __VARIANT__; export const retryLaneExpirationMs = 5000; diff --git a/packages/shared/forks/ReactFeatureFlags.www.js b/packages/shared/forks/ReactFeatureFlags.www.js index 3a3f0f86a6719..798134ab70c24 100644 --- a/packages/shared/forks/ReactFeatureFlags.www.js +++ b/packages/shared/forks/ReactFeatureFlags.www.js @@ -37,6 +37,7 @@ export const { syncLaneExpirationMs, transitionLaneExpirationMs, enableInfiniteRenderLoopDetection, + enableRenderableContext, } = dynamicFeatureFlags; // On WWW, __EXPERIMENTAL__ is used for a new modern build. diff --git a/packages/shared/getComponentNameFromType.js b/packages/shared/getComponentNameFromType.js index 390c46ff0a14e..fb63ff465a6af 100644 --- a/packages/shared/getComponentNameFromType.js +++ b/packages/shared/getComponentNameFromType.js @@ -18,6 +18,7 @@ import { REACT_PORTAL_TYPE, REACT_MEMO_TYPE, REACT_PROFILER_TYPE, + REACT_PROVIDER_TYPE, REACT_STRICT_MODE_TYPE, REACT_SUSPENSE_TYPE, REACT_SUSPENSE_LIST_TYPE, @@ -26,7 +27,11 @@ import { REACT_TRACING_MARKER_TYPE, } from 'shared/ReactSymbols'; -import {enableTransitionTracing, enableCache} from './ReactFeatureFlags'; +import { + enableTransitionTracing, + enableCache, + enableRenderableContext, +} from './ReactFeatureFlags'; // Keep in sync with react-reconciler/getComponentNameFromFiber function getWrappedName( @@ -98,12 +103,27 @@ export default function getComponentNameFromType(type: mixed): string | null { } } switch (type.$$typeof) { + case REACT_PROVIDER_TYPE: + if (enableRenderableContext) { + return null; + } else { + const provider = (type: any); + return getContextName(provider._context) + '.Provider'; + } case REACT_CONTEXT_TYPE: const context: ReactContext = (type: any); - return getContextName(context) + '.Provider'; + if (enableRenderableContext) { + return getContextName(context) + '.Provider'; + } else { + return getContextName(context) + '.Consumer'; + } case REACT_CONSUMER_TYPE: - const consumer: ReactConsumerType = (type: any); - return getContextName(consumer._context) + '.Consumer'; + if (enableRenderableContext) { + const consumer: ReactConsumerType = (type: any); + return getContextName(consumer._context) + '.Consumer'; + } else { + return null; + } case REACT_FORWARD_REF_TYPE: return getWrappedName(type, type.render, 'ForwardRef'); case REACT_MEMO_TYPE: diff --git a/packages/shared/isValidElementType.js b/packages/shared/isValidElementType.js index 4529e71f3f393..aa2092a923dfc 100644 --- a/packages/shared/isValidElementType.js +++ b/packages/shared/isValidElementType.js @@ -10,6 +10,7 @@ import { REACT_CONTEXT_TYPE, REACT_CONSUMER_TYPE, + REACT_PROVIDER_TYPE, REACT_FORWARD_REF_TYPE, REACT_FRAGMENT_TYPE, REACT_PROFILER_TYPE, @@ -31,6 +32,7 @@ import { enableTransitionTracing, enableDebugTracing, enableLegacyHidden, + enableRenderableContext, } from './ReactFeatureFlags'; const REACT_CLIENT_REFERENCE: symbol = Symbol.for('react.client.reference'); @@ -62,7 +64,8 @@ export default function isValidElementType(type: mixed): boolean { type.$$typeof === REACT_LAZY_TYPE || type.$$typeof === REACT_MEMO_TYPE || type.$$typeof === REACT_CONTEXT_TYPE || - type.$$typeof === REACT_CONSUMER_TYPE || + (!enableRenderableContext && type.$$typeof === REACT_PROVIDER_TYPE) || + (enableRenderableContext && type.$$typeof === REACT_CONSUMER_TYPE) || type.$$typeof === REACT_FORWARD_REF_TYPE || // This needs to include all possible module reference object // types supported by any Flight configuration anywhere since