,
+ );
+ });
});
diff --git a/packages/react-dom/src/server/ReactDOMServerFormatConfig.js b/packages/react-dom/src/server/ReactDOMServerFormatConfig.js
index 45cd0fecb5e96..521fe04439c11 100644
--- a/packages/react-dom/src/server/ReactDOMServerFormatConfig.js
+++ b/packages/react-dom/src/server/ReactDOMServerFormatConfig.js
@@ -48,6 +48,10 @@ import hasOwnProperty from 'shared/hasOwnProperty';
import sanitizeURL from '../shared/sanitizeURL';
import isArray from 'shared/isArray';
+// Used to distinguish these contexts from ones used in other renderers.
+// E.g. this can be used to distinguish legacy renderers from this modern one.
+export const isPrimaryRenderer = true;
+
// Per response, global state that is not contextual to the rendering subtree.
export type ResponseState = {
placeholderPrefix: PrecomputedChunk,
diff --git a/packages/react-native-renderer/src/server/ReactNativeServerFormatConfig.js b/packages/react-native-renderer/src/server/ReactNativeServerFormatConfig.js
index 40ab6b84b26af..223734c0483de 100644
--- a/packages/react-native-renderer/src/server/ReactNativeServerFormatConfig.js
+++ b/packages/react-native-renderer/src/server/ReactNativeServerFormatConfig.js
@@ -23,6 +23,8 @@ import {
import invariant from 'shared/invariant';
+export const isPrimaryRenderer = true;
+
// Every list of children or string is null terminated.
const END_TAG = 0;
// Tree node tags.
diff --git a/packages/react-server/src/ReactFizzClassComponent.js b/packages/react-server/src/ReactFizzClassComponent.js
index d56bc633b8498..a7385d54aa781 100644
--- a/packages/react-server/src/ReactFizzClassComponent.js
+++ b/packages/react-server/src/ReactFizzClassComponent.js
@@ -8,6 +8,7 @@
*/
import {emptyContextObject} from './ReactFizzContext';
+import {readContext} from './ReactFizzNewContext';
import {disableLegacyContext} from 'shared/ReactFeatureFlags';
import {get as getInstance, set as setInstance} from 'shared/ReactInstanceMap';
@@ -211,9 +212,7 @@ export function constructClassInstance(
}
if (typeof contextType === 'object' && contextType !== null) {
- // TODO: Implement Context.
- // context = readContext((contextType: any));
- throw new Error('Context is not yet implemented.');
+ context = readContext((contextType: any));
} else if (!disableLegacyContext) {
context = maskedLegacyContext;
}
@@ -617,9 +616,7 @@ export function mountClassInstance(
const contextType = ctor.contextType;
if (typeof contextType === 'object' && contextType !== null) {
- // TODO: Implement Context.
- // instance.context = readContext(contextType);
- throw new Error('Context is not yet implemented.');
+ instance.context = readContext(contextType);
} else if (disableLegacyContext) {
instance.context = emptyContextObject;
} else {
diff --git a/packages/react-server/src/ReactFizzNewContext.js b/packages/react-server/src/ReactFizzNewContext.js
new file mode 100644
index 0000000000000..43f364717d81f
--- /dev/null
+++ b/packages/react-server/src/ReactFizzNewContext.js
@@ -0,0 +1,278 @@
+/**
+ * Copyright (c) Facebook, Inc. and its affiliates.
+ *
+ * This source code is licensed under the MIT license found in the
+ * LICENSE file in the root directory of this source tree.
+ *
+ * @flow
+ */
+
+import type {ReactContext} from 'shared/ReactTypes';
+
+import {isPrimaryRenderer} from './ReactServerFormatConfig';
+
+import invariant from 'shared/invariant';
+
+let rendererSigil;
+if (__DEV__) {
+ // Use this to detect multiple renderers using the same context
+ rendererSigil = {};
+}
+
+// Used to store the parent path of all context overrides in a shared linked list.
+// Forming a reverse tree.
+type ContextNode = {
+ parent: null | ContextNode,
+ depth: number, // Short hand to compute the depth of the tree at this node.
+ context: ReactContext,
+ parentValue: T,
+ value: T,
+};
+
+// The structure of a context snapshot is an implementation of this file.
+// Currently, it's implemented as tracking the current active node.
+export opaque type ContextSnapshot = null | ContextNode;
+
+export const rootContextSnapshot: ContextSnapshot = null;
+
+// We assume that this runtime owns the "current" field on all ReactContext instances.
+// This global (actually thread local) state represents what state all those "current",
+// fields are currently in.
+let currentActiveSnapshot: ContextSnapshot = null;
+
+function popNode(prev: ContextNode): void {
+ if (isPrimaryRenderer) {
+ prev.context._currentValue = prev.parentValue;
+ } else {
+ prev.context._currentValue2 = prev.parentValue;
+ }
+}
+
+function pushNode(next: ContextNode): void {
+ if (isPrimaryRenderer) {
+ next.context._currentValue = next.value;
+ } else {
+ next.context._currentValue2 = next.value;
+ }
+}
+
+function popToNearestCommonAncestor(
+ prev: ContextNode,
+ next: ContextNode,
+): void {
+ if (prev === next) {
+ // We've found a shared ancestor. We don't need to pop nor reapply this one or anything above.
+ } else {
+ popNode(prev);
+ const parentPrev = prev.parent;
+ const parentNext = next.parent;
+ if (parentPrev === null) {
+ invariant(
+ parentNext === null,
+ 'The stacks must reach the root at the same time. This is a bug in React.',
+ );
+ } else {
+ invariant(
+ parentNext !== null,
+ 'The stacks must reach the root at the same time. This is a bug in React.',
+ );
+ popToNearestCommonAncestor(parentPrev, parentNext);
+ // On the way back, we push the new ones that weren't common.
+ pushNode(next);
+ }
+ }
+}
+
+function popAllPrevious(prev: ContextNode): void {
+ popNode(prev);
+ const parentPrev = prev.parent;
+ if (parentPrev !== null) {
+ popAllPrevious(parentPrev);
+ }
+}
+
+function pushAllNext(next: ContextNode): void {
+ const parentNext = next.parent;
+ if (parentNext !== null) {
+ pushAllNext(parentNext);
+ }
+ pushNode(next);
+}
+
+function popPreviousToCommonLevel(
+ prev: ContextNode,
+ next: ContextNode,
+): void {
+ popNode(prev);
+ const parentPrev = prev.parent;
+ invariant(
+ parentPrev !== null,
+ 'The depth must equal at least at zero before reaching the root. This is a bug in React.',
+ );
+ if (parentPrev.depth === next.depth) {
+ // We found the same level. Now we just need to find a shared ancestor.
+ popToNearestCommonAncestor(parentPrev, next);
+ } else {
+ // We must still be deeper.
+ popPreviousToCommonLevel(parentPrev, next);
+ }
+}
+
+function popNextToCommonLevel(
+ prev: ContextNode,
+ next: ContextNode,
+): void {
+ const parentNext = next.parent;
+ invariant(
+ parentNext !== null,
+ 'The depth must equal at least at zero before reaching the root. This is a bug in React.',
+ );
+ if (prev.depth === parentNext.depth) {
+ // We found the same level. Now we just need to find a shared ancestor.
+ popToNearestCommonAncestor(prev, parentNext);
+ } else {
+ // We must still be deeper.
+ popNextToCommonLevel(prev, parentNext);
+ }
+ pushNode(next);
+}
+
+// Perform context switching to the new snapshot.
+// To make it cheap to read many contexts, while not suspending, we make the switch eagerly by
+// updating all the context's current values. That way reads, always just read the current value.
+// At the cost of updating contexts even if they're never read by this subtree.
+export function switchContext(newSnapshot: ContextSnapshot): void {
+ // The basic algorithm we need to do is to pop back any contexts that are no longer on the stack.
+ // We also need to update any new contexts that are now on the stack with the deepest value.
+ // The easiest way to update new contexts is to just reapply them in reverse order from the
+ // perspective of the backpointers. To avoid allocating a lot when switching, we use the stack
+ // for that. Therefore this algorithm is recursive.
+ // 1) First we pop which ever snapshot tree was deepest. Popping old contexts as we go.
+ // 2) Then we find the nearest common ancestor from there. Popping old contexts as we go.
+ // 3) Then we reapply new contexts on the way back up the stack.
+ const prev = currentActiveSnapshot;
+ const next = newSnapshot;
+ if (prev !== next) {
+ if (prev === null) {
+ // $FlowFixMe: This has to be non-null since it's not equal to prev.
+ pushAllNext(next);
+ } else if (next === null) {
+ popAllPrevious(prev);
+ } else if (prev.depth === next.depth) {
+ popToNearestCommonAncestor(prev, next);
+ } else if (prev.depth > next.depth) {
+ popPreviousToCommonLevel(prev, next);
+ } else {
+ popNextToCommonLevel(prev, next);
+ }
+ currentActiveSnapshot = next;
+ }
+}
+
+export function pushProvider(
+ context: ReactContext,
+ nextValue: T,
+): ContextSnapshot {
+ let prevValue;
+ if (isPrimaryRenderer) {
+ prevValue = context._currentValue;
+ context._currentValue = nextValue;
+ if (__DEV__) {
+ if (
+ context._currentRenderer !== undefined &&
+ context._currentRenderer !== null &&
+ context._currentRenderer !== rendererSigil
+ ) {
+ console.error(
+ 'Detected multiple renderers concurrently rendering the ' +
+ 'same context provider. This is currently unsupported.',
+ );
+ }
+ context._currentRenderer = rendererSigil;
+ }
+ } else {
+ prevValue = context._currentValue2;
+ context._currentValue2 = nextValue;
+ if (__DEV__) {
+ if (
+ context._currentRenderer2 !== undefined &&
+ context._currentRenderer2 !== null &&
+ context._currentRenderer2 !== rendererSigil
+ ) {
+ console.error(
+ 'Detected multiple renderers concurrently rendering the ' +
+ 'same context provider. This is currently unsupported.',
+ );
+ }
+ context._currentRenderer2 = rendererSigil;
+ }
+ }
+ const prevNode = currentActiveSnapshot;
+ const newNode: ContextNode = {
+ parent: prevNode,
+ depth: prevNode === null ? 0 : prevNode.depth + 1,
+ context: context,
+ parentValue: prevValue,
+ value: nextValue,
+ };
+ currentActiveSnapshot = newNode;
+ return newNode;
+}
+
+export function popProvider(context: ReactContext): ContextSnapshot {
+ const prevSnapshot = currentActiveSnapshot;
+ invariant(
+ prevSnapshot !== null,
+ 'Tried to pop a Context at the root of the app. This is a bug in React.',
+ );
+ if (__DEV__) {
+ if (prevSnapshot.context !== context) {
+ console.error(
+ 'The parent context is not the expected context. This is probably a bug in React.',
+ );
+ }
+ }
+ if (isPrimaryRenderer) {
+ prevSnapshot.context._currentValue = prevSnapshot.parentValue;
+ if (__DEV__) {
+ if (
+ context._currentRenderer !== undefined &&
+ context._currentRenderer !== null &&
+ context._currentRenderer !== rendererSigil
+ ) {
+ console.error(
+ 'Detected multiple renderers concurrently rendering the ' +
+ 'same context provider. This is currently unsupported.',
+ );
+ }
+ context._currentRenderer = rendererSigil;
+ }
+ } else {
+ prevSnapshot.context._currentValue2 = prevSnapshot.parentValue;
+ if (__DEV__) {
+ if (
+ context._currentRenderer2 !== undefined &&
+ context._currentRenderer2 !== null &&
+ context._currentRenderer2 !== rendererSigil
+ ) {
+ console.error(
+ 'Detected multiple renderers concurrently rendering the ' +
+ 'same context provider. This is currently unsupported.',
+ );
+ }
+ context._currentRenderer2 = rendererSigil;
+ }
+ }
+ return (currentActiveSnapshot = prevSnapshot.parent);
+}
+
+export function getActiveContext(): ContextSnapshot {
+ return currentActiveSnapshot;
+}
+
+export function readContext(context: ReactContext): T {
+ const value = isPrimaryRenderer
+ ? context._currentValue
+ : context._currentValue2;
+ return value;
+}
diff --git a/packages/react-server/src/ReactFizzServer.js b/packages/react-server/src/ReactFizzServer.js
index a3b1babdf5463..c1d51c3b2422e 100644
--- a/packages/react-server/src/ReactFizzServer.js
+++ b/packages/react-server/src/ReactFizzServer.js
@@ -13,12 +13,18 @@ import type {
Chunk,
PrecomputedChunk,
} from './ReactServerStreamConfig';
-import type {ReactNodeList} from 'shared/ReactTypes';
+import type {
+ ReactNodeList,
+ ReactContext,
+ ReactProviderType,
+} from 'shared/ReactTypes';
+import type {LazyComponent as LazyComponentType} from 'react/src/ReactLazy';
import type {
SuspenseBoundaryID,
ResponseState,
FormatContext,
} from './ReactServerFormatConfig';
+import type {ContextSnapshot} from './ReactFizzNewContext';
import {
scheduleWork,
@@ -56,6 +62,15 @@ import {
processChildContext,
emptyContextObject,
} from './ReactFizzContext';
+import {
+ readContext,
+ rootContextSnapshot,
+ switchContext,
+ getActiveContext,
+ pushProvider,
+ popProvider,
+} from './ReactFizzNewContext';
+
import {
getIteratorFn,
REACT_ELEMENT_TYPE,
@@ -68,6 +83,10 @@ import {
REACT_PROFILER_TYPE,
REACT_SUSPENSE_LIST_TYPE,
REACT_FRAGMENT_TYPE,
+ REACT_FORWARD_REF_TYPE,
+ REACT_MEMO_TYPE,
+ REACT_PROVIDER_TYPE,
+ REACT_CONTEXT_TYPE,
} from 'shared/ReactSymbols';
import ReactSharedInternals from 'shared/ReactSharedInternals';
import {
@@ -104,6 +123,7 @@ type Task = {
blockedSegment: Segment, // the segment we'll write to
abortSet: Set, // the abortable set that this task belongs to
legacyContext: LegacyContext, // the current legacy context that this task is executing in
+ context: ContextSnapshot, // the current new context that this task is executing in
assignID: null | SuspenseBoundaryID, // id to assign to the content
};
@@ -220,6 +240,7 @@ export function createRequest(
rootSegment,
abortSet,
emptyContextObject,
+ rootContextSnapshot,
null,
);
pingedTasks.push(rootTask);
@@ -257,6 +278,7 @@ function createTask(
blockedSegment: Segment,
abortSet: Set,
legacyContext: LegacyContext,
+ context: ContextSnapshot,
assignID: null | SuspenseBoundaryID,
): Task {
request.allPendingTasks++;
@@ -272,6 +294,7 @@ function createTask(
blockedSegment,
abortSet,
legacyContext,
+ context,
assignID,
};
abortSet.add(task);
@@ -394,6 +417,7 @@ function renderSuspenseBoundary(
boundarySegment,
fallbackAbortSet,
task.legacyContext,
+ task.context,
newBoundary.id, // This is the ID we want to give this fallback so we can replace it later.
);
// TODO: This should be queued at a separate lower priority queue so that we only work
@@ -529,6 +553,7 @@ let didWarnAboutReassigningProps = false;
const didWarnAboutDefaultPropsOnFunctionComponent = {};
let didWarnAboutGenerators = false;
let didWarnAboutMaps = false;
+let hasWarnedAboutUsingContextAsConsumer = false;
// This would typically be a function component but we still support module pattern
// components for some reason.
@@ -694,49 +719,185 @@ function validateFunctionComponentInDev(Component: any): void {
}
}
+function renderForwardRef(
+ request: Request,
+ task: Task,
+ type: any,
+ props: Object,
+): void {
+ throw new Error('Not yet implemented element type.');
+}
+
+function renderMemo(
+ request: Request,
+ task: Task,
+ type: any,
+ props: Object,
+): void {
+ throw new Error('Not yet implemented element type.');
+}
+
+function renderContextConsumer(
+ request: Request,
+ task: Task,
+ context: ReactContext,
+ props: Object,
+): void {
+ // The logic below for Context differs depending on PROD or DEV mode. In
+ // DEV mode, we create a separate object for Context.Consumer that acts
+ // like a proxy to Context. This proxy object adds unnecessary code in PROD
+ // so we use the old behaviour (Context.Consumer references Context) to
+ // reduce size and overhead. The separate object references context via
+ // a property called "_context", which also gives us the ability to check
+ // in DEV mode if this property exists or not and warn if it does not.
+ if (__DEV__) {
+ if ((context: any)._context === undefined) {
+ // This may be because it's a Context (rather than a Consumer).
+ // Or it may be because it's older React where they're the same thing.
+ // We only want to warn if we're sure it's a new React.
+ if (context !== context.Consumer) {
+ if (!hasWarnedAboutUsingContextAsConsumer) {
+ hasWarnedAboutUsingContextAsConsumer = true;
+ console.error(
+ 'Rendering directly is not supported and will be removed in ' +
+ 'a future major release. Did you mean to render instead?',
+ );
+ }
+ }
+ } else {
+ context = (context: any)._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.',
+ );
+ }
+ }
+
+ const newValue = readContext(context);
+ const newChildren = render(newValue);
+
+ renderNodeDestructive(request, task, newChildren);
+}
+
+function renderContextProvider(
+ request: Request,
+ task: Task,
+ type: ReactProviderType,
+ props: Object,
+): void {
+ const context = type._context;
+ const value = props.value;
+ const children = props.children;
+ let prevSnapshot;
+ if (__DEV__) {
+ prevSnapshot = task.context;
+ }
+ task.context = pushProvider(context, value);
+ renderNodeDestructive(request, task, children);
+ task.context = popProvider(context);
+ if (__DEV__) {
+ if (prevSnapshot !== task.context) {
+ console.error(
+ 'Popping the context provider did not return back to the original snapshot. This is a bug in React.',
+ );
+ }
+ }
+}
+
+function renderLazyComponent(
+ request: Request,
+ task: Task,
+ type: LazyComponentType,
+ props: Object,
+): void {
+ throw new Error('Not yet implemented element type.');
+}
+
function renderElement(
request: Request,
task: Task,
type: any,
props: Object,
- node: ReactNodeList,
): void {
if (typeof type === 'function') {
if (shouldConstruct(type)) {
renderClassComponent(request, task, type, props);
+ return;
} else {
renderIndeterminateComponent(request, task, type, props);
+ return;
}
- } else if (typeof type === 'string') {
+ }
+ if (typeof type === 'string') {
renderHostElement(request, task, type, props);
- } else {
- switch (type) {
- // TODO: LegacyHidden acts the same as a fragment. This only works
- // because we currently assume that every instance of LegacyHidden is
- // accompanied by a host component wrapper. In the hidden mode, the host
- // component is given a `hidden` attribute, which ensures that the
- // initial HTML is not visible. To support the use of LegacyHidden as a
- // true fragment, without an extra DOM node, we would have to hide the
- // initial HTML in some other way.
- // TODO: Add REACT_OFFSCREEN_TYPE here too with the same capability.
- case REACT_LEGACY_HIDDEN_TYPE:
- case REACT_DEBUG_TRACING_MODE_TYPE:
- case REACT_STRICT_MODE_TYPE:
- case REACT_PROFILER_TYPE:
- case REACT_SUSPENSE_LIST_TYPE: // TODO: SuspenseList should control the boundaries.
- case REACT_FRAGMENT_TYPE: {
- renderNodeDestructive(request, task, props.children);
- break;
+ return;
+ }
+
+ switch (type) {
+ // TODO: LegacyHidden acts the same as a fragment. This only works
+ // because we currently assume that every instance of LegacyHidden is
+ // accompanied by a host component wrapper. In the hidden mode, the host
+ // component is given a `hidden` attribute, which ensures that the
+ // initial HTML is not visible. To support the use of LegacyHidden as a
+ // true fragment, without an extra DOM node, we would have to hide the
+ // initial HTML in some other way.
+ // TODO: Add REACT_OFFSCREEN_TYPE here too with the same capability.
+ case REACT_LEGACY_HIDDEN_TYPE:
+ case REACT_DEBUG_TRACING_MODE_TYPE:
+ case REACT_STRICT_MODE_TYPE:
+ case REACT_PROFILER_TYPE:
+ case REACT_SUSPENSE_LIST_TYPE: // TODO: SuspenseList should control the boundaries.
+ case REACT_FRAGMENT_TYPE: {
+ renderNodeDestructive(request, task, props.children);
+ return;
+ }
+ case REACT_SUSPENSE_TYPE: {
+ renderSuspenseBoundary(request, task, props);
+ return;
+ }
+ }
+
+ if (typeof type === 'object' && type !== null) {
+ switch (type.$$typeof) {
+ case REACT_FORWARD_REF_TYPE: {
+ renderForwardRef(request, task, type, props);
+ return;
}
- case REACT_SUSPENSE_TYPE: {
- renderSuspenseBoundary(request, task, props);
- break;
+ case REACT_MEMO_TYPE: {
+ renderMemo(request, task, type, props);
+ return;
}
- default: {
- throw new Error('Not yet implemented element type.');
+ case REACT_PROVIDER_TYPE: {
+ renderContextProvider(request, task, type, props);
+ return;
+ }
+ case REACT_CONTEXT_TYPE: {
+ renderContextConsumer(request, task, type, props);
+ return;
+ }
+ case REACT_LAZY_TYPE: {
+ renderLazyComponent(request, task, type, props);
+ return;
}
}
}
+
+ invariant(
+ false,
+ 'Element type is invalid: expected a string (for built-in ' +
+ 'components) or a class/function (for composite components) ' +
+ 'but got: %s.%s',
+ type == null ? type : typeof type,
+ '',
+ );
}
function validateIterable(iterable, iteratorFn: Function): void {
@@ -791,7 +952,7 @@ function renderNodeDestructive(
const element: React$Element = (node: any);
const type = element.type;
const props = element.props;
- renderElement(request, task, type, props, node);
+ renderElement(request, task, type, props);
return;
}
case REACT_PORTAL_TYPE:
@@ -918,6 +1079,7 @@ function spawnNewSuspendedTask(
newSegment,
task.abortSet,
task.legacyContext,
+ task.context,
task.assignID,
);
// We've delegated the assignment.
@@ -936,6 +1098,7 @@ function renderNode(request: Request, task: Task, node: ReactNodeList): void {
// process.
const previousFormatContext = task.blockedSegment.formatContext;
const previousLegacyContext = task.legacyContext;
+ const previousContext = task.context;
try {
return renderNodeDestructive(request, task, node);
} catch (x) {
@@ -945,6 +1108,9 @@ function renderNode(request: Request, task: Task, node: ReactNodeList): void {
// functions in case nothing throws so we don't use "finally" here.
task.blockedSegment.formatContext = previousFormatContext;
task.legacyContext = previousLegacyContext;
+ task.context = previousContext;
+ // Restore all active ReactContexts to what they were before.
+ switchContext(previousContext);
} else {
// We assume that we don't need the correct context.
// Let's terminate the rest of the tree and don't render any siblings.
@@ -1104,6 +1270,10 @@ function retryTask(request: Request, task: Task): void {
// We completed this by other means before we had a chance to retry it.
return;
}
+ // We restore the context to what it was when we suspended.
+ // We don't restore it after we leave because it's likely that we'll end up
+ // needing a very similar context soon again.
+ switchContext(task.context);
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.
@@ -1129,6 +1299,7 @@ function performWork(request: Request): void {
if (request.status === CLOSED) {
return;
}
+ const prevContext = getActiveContext();
const prevDispatcher = ReactCurrentDispatcher.current;
ReactCurrentDispatcher.current = Dispatcher;
@@ -1148,6 +1319,16 @@ function performWork(request: Request): void {
fatalError(request, error);
} finally {
ReactCurrentDispatcher.current = prevDispatcher;
+ if (prevDispatcher === Dispatcher) {
+ // This means that we were in a reentrant work loop. This could happen
+ // in a renderer that supports synchronous work like renderToString,
+ // when it's called from within another renderer.
+ // Normally we don't bother switching the contexts to their root/default
+ // values when leaving because we'll likely need the same or similar
+ // context again. However, when we're inside a synchronous loop like this
+ // we'll to restore the context to what it was before returning.
+ switchContext(prevContext);
+ }
}
}
diff --git a/packages/react-server/src/forks/ReactServerFormatConfig.custom.js b/packages/react-server/src/forks/ReactServerFormatConfig.custom.js
index 58f2d504b46b8..1ab6f95090822 100644
--- a/packages/react-server/src/forks/ReactServerFormatConfig.custom.js
+++ b/packages/react-server/src/forks/ReactServerFormatConfig.custom.js
@@ -29,6 +29,8 @@ export opaque type ResponseState = mixed;
export opaque type FormatContext = mixed;
export opaque type SuspenseBoundaryID = mixed;
+export const isPrimaryRenderer = false;
+
export const getChildFormatContext = $$$hostConfig.getChildFormatContext;
export const createSuspenseBoundaryID = $$$hostConfig.createSuspenseBoundaryID;
export const pushEmpty = $$$hostConfig.pushEmpty;
diff --git a/scripts/error-codes/codes.json b/scripts/error-codes/codes.json
index 17c28c3bc7018..4dbb95f55e809 100644
--- a/scripts/error-codes/codes.json
+++ b/scripts/error-codes/codes.json
@@ -388,5 +388,8 @@
"397": "Unknown insertion mode. This is a bug in React.",
"398": "`dangerouslySetInnerHTML` does not work on