diff --git a/packages/react-client/src/__tests__/ReactFlight-test.js b/packages/react-client/src/__tests__/ReactFlight-test.js
index b0f539bf2572c..d642a02c8c6cf 100644
--- a/packages/react-client/src/__tests__/ReactFlight-test.js
+++ b/packages/react-client/src/__tests__/ReactFlight-test.js
@@ -3884,4 +3884,19 @@ describe('ReactFlight', () => {
,
);
});
+
+ // @gate enableOptimisticKey
+ it('collapses optimistic keys to an optimistic key', async () => {
+ function Bar({text}) {
+ return
;
+ }
+ function Foo() {
+ return ;
+ }
+ const transport = ReactNoopFlightServer.render({
+ element: ,
+ });
+ const model = await ReactNoopFlightClient.read(transport);
+ expect(model.element.key).toBe(React.optimisticKey);
+ });
});
diff --git a/packages/react-devtools-shared/src/backend/fiber/renderer.js b/packages/react-devtools-shared/src/backend/fiber/renderer.js
index fa802e53a6f26..4f755fda29392 100644
--- a/packages/react-devtools-shared/src/backend/fiber/renderer.js
+++ b/packages/react-devtools-shared/src/backend/fiber/renderer.js
@@ -120,6 +120,7 @@ import {
MEMO_SYMBOL_STRING,
SERVER_CONTEXT_SYMBOL_STRING,
LAZY_SYMBOL_STRING,
+ REACT_OPTIMISTIC_KEY,
} from '../shared/ReactSymbols';
import {enableStyleXFeatures} from 'react-devtools-feature-flags';
@@ -4849,7 +4850,10 @@ export function attach(
}
let previousSiblingOfBestMatch = null;
let bestMatch = remainingReconcilingChildren;
- if (componentInfo.key != null) {
+ if (
+ componentInfo.key != null &&
+ componentInfo.key !== REACT_OPTIMISTIC_KEY
+ ) {
// If there is a key try to find a matching key in the set.
bestMatch = remainingReconcilingChildren;
while (bestMatch !== null) {
@@ -6145,7 +6149,7 @@ export function attach(
return {
displayName: getDisplayNameForFiber(fiber) || 'Anonymous',
id: instance.id,
- key: fiber.key,
+ key: fiber.key === REACT_OPTIMISTIC_KEY ? null : fiber.key,
env: null,
stack:
fiber._debugOwner == null || fiber._debugStack == null
@@ -6158,7 +6162,11 @@ export function attach(
return {
displayName: componentInfo.name || 'Anonymous',
id: instance.id,
- key: componentInfo.key == null ? null : componentInfo.key,
+ key:
+ componentInfo.key == null ||
+ componentInfo.key === REACT_OPTIMISTIC_KEY
+ ? null
+ : componentInfo.key,
env: componentInfo.env == null ? null : componentInfo.env,
stack:
componentInfo.owner == null || componentInfo.debugStack == null
@@ -7082,7 +7090,7 @@ export function attach(
// Does the component have legacy context attached to it.
hasLegacyContext,
- key: key != null ? key : null,
+ key: key != null && key !== REACT_OPTIMISTIC_KEY ? key : null,
type: elementType,
@@ -8641,7 +8649,7 @@ export function attach(
}
return {
displayName,
- key,
+ key: key === REACT_OPTIMISTIC_KEY ? null : key,
index,
};
}
@@ -8649,7 +8657,11 @@ export function attach(
function getVirtualPathFrame(virtualInstance: VirtualInstance): PathFrame {
return {
displayName: virtualInstance.data.name || '',
- key: virtualInstance.data.key == null ? null : virtualInstance.data.key,
+ key:
+ virtualInstance.data.key == null ||
+ virtualInstance.data.key === REACT_OPTIMISTIC_KEY
+ ? null
+ : virtualInstance.data.key,
index: -1, // We use -1 to indicate that this is a virtual path frame.
};
}
diff --git a/packages/react-devtools-shared/src/backend/shared/ReactSymbols.js b/packages/react-devtools-shared/src/backend/shared/ReactSymbols.js
index 7a7a9c107e93f..483671b900383 100644
--- a/packages/react-devtools-shared/src/backend/shared/ReactSymbols.js
+++ b/packages/react-devtools-shared/src/backend/shared/ReactSymbols.js
@@ -72,3 +72,9 @@ export const SERVER_CONTEXT_DEFAULT_VALUE_NOT_LOADED_SYMBOL_STRING =
export const REACT_MEMO_CACHE_SENTINEL: symbol = Symbol.for(
'react.memo_cache_sentinel',
);
+
+import type {ReactOptimisticKey} from 'shared/ReactTypes';
+
+export const REACT_OPTIMISTIC_KEY: ReactOptimisticKey = (Symbol.for(
+ 'react.optimistic_key',
+): any);
diff --git a/packages/react-dom/src/__tests__/ReactDOMFizzStaticBrowser-test.js b/packages/react-dom/src/__tests__/ReactDOMFizzStaticBrowser-test.js
index beebd3a1165b5..334bb2ddce761 100644
--- a/packages/react-dom/src/__tests__/ReactDOMFizzStaticBrowser-test.js
+++ b/packages/react-dom/src/__tests__/ReactDOMFizzStaticBrowser-test.js
@@ -1111,4 +1111,64 @@ describe('ReactDOMFizzStaticBrowser', () => {
,
);
});
+
+ // @gate enableHalt && enableOptimisticKey
+ it('can resume an optimistic keyed slot', async () => {
+ const errors = [];
+
+ let resolve;
+ const promise = new Promise(r => (resolve = r));
+
+ async function Component() {
+ await promise;
+ return 'Hi';
+ }
+
+ if (React.optimisticKey === undefined) {
+ throw new Error('optimisticKey missing');
+ }
+
+ function App() {
+ return (
+
+
+
+
+
+ );
+ }
+
+ const controller = new AbortController();
+ const pendingResult = serverAct(() =>
+ ReactDOMFizzStatic.prerender(, {
+ signal: controller.signal,
+ onError(x) {
+ errors.push(x.message);
+ },
+ }),
+ );
+
+ await serverAct(() => {
+ controller.abort();
+ });
+
+ const prerendered = await pendingResult;
+
+ const postponedState = JSON.stringify(prerendered.postponed);
+
+ await readIntoContainer(prerendered.prelude);
+ expect(getVisibleChildren(container)).toEqual(Loading
);
+
+ expect(prerendered.postponed).not.toBe(null);
+
+ await resolve();
+
+ const dynamic = await serverAct(() =>
+ ReactDOMFizzServer.resume(, JSON.parse(postponedState)),
+ );
+
+ await readIntoContainer(dynamic);
+
+ expect(getVisibleChildren(container)).toEqual(Hi
);
+ });
});
diff --git a/packages/react-reconciler/src/ReactChildFiber.js b/packages/react-reconciler/src/ReactChildFiber.js
index 8daf7fde4b4e0..2a726447263c7 100644
--- a/packages/react-reconciler/src/ReactChildFiber.js
+++ b/packages/react-reconciler/src/ReactChildFiber.js
@@ -15,6 +15,8 @@ import type {
ReactDebugInfo,
ReactComponentInfo,
SuspenseListRevealOrder,
+ ReactKey,
+ ReactOptimisticKey,
} from 'shared/ReactTypes';
import type {Fiber} from './ReactInternalTypes';
import type {Lanes} from './ReactFiberLane';
@@ -37,6 +39,7 @@ import {
REACT_LAZY_TYPE,
REACT_CONTEXT_TYPE,
REACT_LEGACY_ELEMENT_TYPE,
+ REACT_OPTIMISTIC_KEY,
} from 'shared/ReactSymbols';
import {
HostRoot,
@@ -50,6 +53,7 @@ import {
enableAsyncIterableChildren,
disableLegacyMode,
enableFragmentRefs,
+ enableOptimisticKey,
} from 'shared/ReactFeatureFlags';
import {
@@ -462,18 +466,33 @@ function createChildReconciler(
function mapRemainingChildren(
currentFirstChild: Fiber,
- ): Map {
+ ): Map {
// Add the remaining children to a temporary map so that we can find them by
// keys quickly. Implicit (null) keys get added to this set with their index
// instead.
- const existingChildren: Map = new Map();
+ const existingChildren: Map<
+ | string
+ | number
+ // This type is only here for the case when enableOptimisticKey is disabled.
+ // Remove it after it ships.
+ | ReactOptimisticKey,
+ Fiber,
+ > = new Map();
let existingChild: null | Fiber = currentFirstChild;
while (existingChild !== null) {
- if (existingChild.key !== null) {
- existingChildren.set(existingChild.key, existingChild);
- } else {
+ if (existingChild.key === null) {
existingChildren.set(existingChild.index, existingChild);
+ } else if (
+ enableOptimisticKey &&
+ existingChild.key === REACT_OPTIMISTIC_KEY
+ ) {
+ // For optimistic keys, we store the negative index (minus one) to differentiate
+ // them from the regular indices. We'll look this up regardless of what the new
+ // key is, if there's no other match.
+ existingChildren.set(-existingChild.index - 1, existingChild);
+ } else {
+ existingChildren.set(existingChild.key, existingChild);
}
existingChild = existingChild.sibling;
}
@@ -636,6 +655,10 @@ function createChildReconciler(
} else {
// Update
const existing = useFiber(current, portal.children || []);
+ if (enableOptimisticKey) {
+ // If the old key was optimistic we need to now save the real one.
+ existing.key = portal.key;
+ }
existing.return = returnFiber;
if (__DEV__) {
existing._debugInfo = currentDebugInfo;
@@ -649,7 +672,7 @@ function createChildReconciler(
current: Fiber | null,
fragment: Iterable,
lanes: Lanes,
- key: null | string,
+ key: ReactKey,
): Fiber {
if (current === null || current.tag !== Fragment) {
// Insert
@@ -670,6 +693,10 @@ function createChildReconciler(
} else {
// Update
const existing = useFiber(current, fragment);
+ if (enableOptimisticKey) {
+ // If the old key was optimistic we need to now save the real one.
+ existing.key = key;
+ }
existing.return = returnFiber;
if (__DEV__) {
existing._debugInfo = currentDebugInfo;
@@ -840,7 +867,13 @@ function createChildReconciler(
if (typeof newChild === 'object' && newChild !== null) {
switch (newChild.$$typeof) {
case REACT_ELEMENT_TYPE: {
- if (newChild.key === key) {
+ if (
+ // If the old child was an optimisticKey, then we'd normally consider that a match,
+ // but instead, we'll bail to return null from the slot which will bail to slow path.
+ // That's to ensure that if the new key has a match elsewhere in the list, then that
+ // takes precedence over assuming the identity of an optimistic slot.
+ newChild.key === key
+ ) {
const prevDebugInfo = pushDebugInfo(newChild._debugInfo);
const updated = updateElement(
returnFiber,
@@ -855,7 +888,13 @@ function createChildReconciler(
}
}
case REACT_PORTAL_TYPE: {
- if (newChild.key === key) {
+ if (
+ // If the old child was an optimisticKey, then we'd normally consider that a match,
+ // but instead, we'll bail to return null from the slot which will bail to slow path.
+ // That's to ensure that if the new key has a match elsewhere in the list, then that
+ // takes precedence over assuming the identity of an optimistic slot.
+ newChild.key === key
+ ) {
return updatePortal(returnFiber, oldFiber, newChild, lanes);
} else {
return null;
@@ -939,7 +978,7 @@ function createChildReconciler(
}
function updateFromMap(
- existingChildren: Map,
+ existingChildren: Map,
returnFiber: Fiber,
newIdx: number,
newChild: any,
@@ -968,7 +1007,11 @@ function createChildReconciler(
const matchedFiber =
existingChildren.get(
newChild.key === null ? newIdx : newChild.key,
- ) || null;
+ ) ||
+ (enableOptimisticKey &&
+ // If the existing child was an optimistic key, we may still match on the index.
+ existingChildren.get(-newIdx - 1)) ||
+ null;
const prevDebugInfo = pushDebugInfo(newChild._debugInfo);
const updated = updateElement(
returnFiber,
@@ -983,7 +1026,11 @@ function createChildReconciler(
const matchedFiber =
existingChildren.get(
newChild.key === null ? newIdx : newChild.key,
- ) || null;
+ ) ||
+ (enableOptimisticKey &&
+ // If the existing child was an optimistic key, we may still match on the index.
+ existingChildren.get(-newIdx - 1)) ||
+ null;
return updatePortal(returnFiber, matchedFiber, newChild, lanes);
}
case REACT_LAZY_TYPE: {
@@ -1274,14 +1321,22 @@ function createChildReconciler(
);
}
if (shouldTrackSideEffects) {
- if (newFiber.alternate !== null) {
+ const currentFiber = newFiber.alternate;
+ if (currentFiber !== null) {
// The new fiber is a work in progress, but if there exists a
// current, that means that we reused the fiber. We need to delete
// it from the child list so that we don't add it to the deletion
// list.
- existingChildren.delete(
- newFiber.key === null ? newIdx : newFiber.key,
- );
+ if (
+ enableOptimisticKey &&
+ currentFiber.key === REACT_OPTIMISTIC_KEY
+ ) {
+ existingChildren.delete(-newIdx - 1);
+ } else {
+ existingChildren.delete(
+ currentFiber.key === null ? newIdx : currentFiber.key,
+ );
+ }
}
}
lastPlacedIndex = placeChild(newFiber, lastPlacedIndex, newIdx);
@@ -1568,14 +1623,22 @@ function createChildReconciler(
);
}
if (shouldTrackSideEffects) {
- if (newFiber.alternate !== null) {
+ const currentFiber = newFiber.alternate;
+ if (currentFiber !== null) {
// The new fiber is a work in progress, but if there exists a
// current, that means that we reused the fiber. We need to delete
// it from the child list so that we don't add it to the deletion
// list.
- existingChildren.delete(
- newFiber.key === null ? newIdx : newFiber.key,
- );
+ if (
+ enableOptimisticKey &&
+ currentFiber.key === REACT_OPTIMISTIC_KEY
+ ) {
+ existingChildren.delete(-newIdx - 1);
+ } else {
+ existingChildren.delete(
+ currentFiber.key === null ? newIdx : currentFiber.key,
+ );
+ }
}
}
lastPlacedIndex = placeChild(newFiber, lastPlacedIndex, newIdx);
@@ -1642,12 +1705,19 @@ function createChildReconciler(
while (child !== null) {
// TODO: If key === null and child.key === null, then this only applies to
// the first item in the list.
- if (child.key === key) {
+ if (
+ child.key === key ||
+ (enableOptimisticKey && child.key === REACT_OPTIMISTIC_KEY)
+ ) {
const elementType = element.type;
if (elementType === REACT_FRAGMENT_TYPE) {
if (child.tag === Fragment) {
deleteRemainingChildren(returnFiber, child.sibling);
const existing = useFiber(child, element.props.children);
+ if (enableOptimisticKey) {
+ // If the old key was optimistic we need to now save the real one.
+ existing.key = key;
+ }
if (enableFragmentRefs) {
coerceRef(existing, element);
}
@@ -1677,6 +1747,10 @@ function createChildReconciler(
) {
deleteRemainingChildren(returnFiber, child.sibling);
const existing = useFiber(child, element.props);
+ if (enableOptimisticKey) {
+ // If the old key was optimistic we need to now save the real one.
+ existing.key = key;
+ }
coerceRef(existing, element);
existing.return = returnFiber;
if (__DEV__) {
@@ -1736,7 +1810,10 @@ function createChildReconciler(
while (child !== null) {
// TODO: If key === null and child.key === null, then this only applies to
// the first item in the list.
- if (child.key === key) {
+ if (
+ child.key === key ||
+ (enableOptimisticKey && child.key === REACT_OPTIMISTIC_KEY)
+ ) {
if (
child.tag === HostPortal &&
child.stateNode.containerInfo === portal.containerInfo &&
@@ -1744,6 +1821,10 @@ function createChildReconciler(
) {
deleteRemainingChildren(returnFiber, child.sibling);
const existing = useFiber(child, portal.children || []);
+ if (enableOptimisticKey) {
+ // If the old key was optimistic we need to now save the real one.
+ existing.key = key;
+ }
existing.return = returnFiber;
return existing;
} else {
diff --git a/packages/react-reconciler/src/ReactFiber.js b/packages/react-reconciler/src/ReactFiber.js
index fb2c7347010b6..7ab798ea22bc4 100644
--- a/packages/react-reconciler/src/ReactFiber.js
+++ b/packages/react-reconciler/src/ReactFiber.js
@@ -14,6 +14,7 @@ import type {
ReactScope,
ViewTransitionProps,
ActivityProps,
+ ReactKey,
} from 'shared/ReactTypes';
import type {Fiber} from './ReactInternalTypes';
import type {RootTag} from './ReactRootTags';
@@ -43,6 +44,7 @@ import {
enableObjectFiber,
enableViewTransition,
enableSuspenseyImages,
+ enableOptimisticKey,
} from 'shared/ReactFeatureFlags';
import {NoFlags, Placement, StaticMask} from './ReactFiberFlags';
import {ConcurrentRoot} from './ReactRootTags';
@@ -137,7 +139,7 @@ function FiberNode(
this: $FlowFixMe,
tag: WorkTag,
pendingProps: mixed,
- key: null | string,
+ key: ReactKey,
mode: TypeOfMode,
) {
// Instance
@@ -224,7 +226,7 @@ function FiberNode(
function createFiberImplClass(
tag: WorkTag,
pendingProps: mixed,
- key: null | string,
+ key: ReactKey,
mode: TypeOfMode,
): Fiber {
// $FlowFixMe[invalid-constructor]: the shapes are exact here but Flow doesn't like constructors
@@ -234,7 +236,7 @@ function createFiberImplClass(
function createFiberImplObject(
tag: WorkTag,
pendingProps: mixed,
- key: null | string,
+ key: ReactKey,
mode: TypeOfMode,
): Fiber {
const fiber: Fiber = {
@@ -364,6 +366,12 @@ export function createWorkInProgress(current: Fiber, pendingProps: any): Fiber {
workInProgress.subtreeFlags = NoFlags;
workInProgress.deletions = null;
+ if (enableOptimisticKey) {
+ // For optimistic keys, the Fibers can have different keys if one is optimistic
+ // and the other one is filled in.
+ workInProgress.key = current.key;
+ }
+
if (enableProfilerTimer) {
// We intentionally reset, rather than copy, actualDuration & actualStartTime.
// This prevents time from endlessly accumulating in new commits.
@@ -488,8 +496,15 @@ export function resetWorkInProgress(
workInProgress.memoizedState = current.memoizedState;
workInProgress.updateQueue = current.updateQueue;
// Needed because Blocks store data on type.
+ // TODO: Blocks don't exist anymore. Do we still need this?
workInProgress.type = current.type;
+ if (enableOptimisticKey) {
+ // For optimistic keys, the Fibers can have different keys if one is optimistic
+ // and the other one is filled in.
+ workInProgress.key = current.key;
+ }
+
// Clone the dependencies object. This is mutated during the render phase, so
// it cannot be shared with the current fiber.
const currentDependencies = current.dependencies;
@@ -545,7 +560,7 @@ export function createHostRootFiber(
// TODO: Get rid of this helper. Only createFiberFromElement should exist.
export function createFiberFromTypeAndProps(
type: any, // React$ElementType
- key: null | string,
+ key: ReactKey,
pendingProps: any,
owner: null | ReactComponentInfo | Fiber,
mode: TypeOfMode,
@@ -747,7 +762,7 @@ export function createFiberFromFragment(
elements: ReactFragment,
mode: TypeOfMode,
lanes: Lanes,
- key: null | string,
+ key: ReactKey,
): Fiber {
const fiber = createFiber(Fragment, elements, key, mode);
fiber.lanes = lanes;
@@ -759,7 +774,7 @@ function createFiberFromScope(
pendingProps: any,
mode: TypeOfMode,
lanes: Lanes,
- key: null | string,
+ key: ReactKey,
) {
const fiber = createFiber(ScopeComponent, pendingProps, key, mode);
fiber.type = scope;
@@ -772,7 +787,7 @@ function createFiberFromProfiler(
pendingProps: any,
mode: TypeOfMode,
lanes: Lanes,
- key: null | string,
+ key: ReactKey,
): Fiber {
if (__DEV__) {
if (typeof pendingProps.id !== 'string') {
@@ -801,7 +816,7 @@ export function createFiberFromSuspense(
pendingProps: any,
mode: TypeOfMode,
lanes: Lanes,
- key: null | string,
+ key: ReactKey,
): Fiber {
const fiber = createFiber(SuspenseComponent, pendingProps, key, mode);
fiber.elementType = REACT_SUSPENSE_TYPE;
@@ -813,7 +828,7 @@ export function createFiberFromSuspenseList(
pendingProps: any,
mode: TypeOfMode,
lanes: Lanes,
- key: null | string,
+ key: ReactKey,
): Fiber {
const fiber = createFiber(SuspenseListComponent, pendingProps, key, mode);
fiber.elementType = REACT_SUSPENSE_LIST_TYPE;
@@ -825,7 +840,7 @@ export function createFiberFromOffscreen(
pendingProps: OffscreenProps,
mode: TypeOfMode,
lanes: Lanes,
- key: null | string,
+ key: ReactKey,
): Fiber {
const fiber = createFiber(OffscreenComponent, pendingProps, key, mode);
fiber.lanes = lanes;
@@ -835,7 +850,7 @@ export function createFiberFromActivity(
pendingProps: ActivityProps,
mode: TypeOfMode,
lanes: Lanes,
- key: null | string,
+ key: ReactKey,
): Fiber {
const fiber = createFiber(ActivityComponent, pendingProps, key, mode);
fiber.elementType = REACT_ACTIVITY_TYPE;
@@ -847,7 +862,7 @@ export function createFiberFromViewTransition(
pendingProps: ViewTransitionProps,
mode: TypeOfMode,
lanes: Lanes,
- key: null | string,
+ key: ReactKey,
): Fiber {
if (!enableSuspenseyImages) {
// Render a ViewTransition component opts into SuspenseyImages mode even
@@ -871,7 +886,7 @@ export function createFiberFromLegacyHidden(
pendingProps: LegacyHiddenProps,
mode: TypeOfMode,
lanes: Lanes,
- key: null | string,
+ key: ReactKey,
): Fiber {
const fiber = createFiber(LegacyHiddenComponent, pendingProps, key, mode);
fiber.elementType = REACT_LEGACY_HIDDEN_TYPE;
@@ -883,7 +898,7 @@ export function createFiberFromTracingMarker(
pendingProps: any,
mode: TypeOfMode,
lanes: Lanes,
- key: null | string,
+ key: ReactKey,
): Fiber {
const fiber = createFiber(TracingMarkerComponent, pendingProps, key, mode);
fiber.elementType = REACT_TRACING_MARKER_TYPE;
diff --git a/packages/react-reconciler/src/ReactInternalTypes.js b/packages/react-reconciler/src/ReactInternalTypes.js
index 95c3b8ca89cb9..775b69d211f76 100644
--- a/packages/react-reconciler/src/ReactInternalTypes.js
+++ b/packages/react-reconciler/src/ReactInternalTypes.js
@@ -17,6 +17,7 @@ import type {
Awaited,
ReactComponentInfo,
ReactDebugInfo,
+ ReactKey,
} from 'shared/ReactTypes';
import type {TransitionTypes} from 'react/src/ReactTransitionType';
import type {WorkTag} from './ReactWorkTags';
@@ -100,7 +101,7 @@ export type Fiber = {
tag: WorkTag,
// Unique identifier of this child.
- key: null | string,
+ key: ReactKey,
// The value of element.type which is used to preserve the identity during
// reconciliation of this child.
diff --git a/packages/react-reconciler/src/ReactPortal.js b/packages/react-reconciler/src/ReactPortal.js
index 06764c58cc87f..78d9d7f63720f 100644
--- a/packages/react-reconciler/src/ReactPortal.js
+++ b/packages/react-reconciler/src/ReactPortal.js
@@ -7,25 +7,37 @@
* @flow
*/
-import {REACT_PORTAL_TYPE} from 'shared/ReactSymbols';
+import {REACT_PORTAL_TYPE, REACT_OPTIMISTIC_KEY} from 'shared/ReactSymbols';
import {checkKeyStringCoercion} from 'shared/CheckStringCoercion';
-import type {ReactNodeList, ReactPortal} from 'shared/ReactTypes';
+import type {
+ ReactNodeList,
+ ReactPortal,
+ ReactOptimisticKey,
+} from 'shared/ReactTypes';
export function createPortal(
children: ReactNodeList,
containerInfo: any,
// TODO: figure out the API for cross-renderer implementation.
implementation: any,
- key: ?string = null,
+ key: ?string | ReactOptimisticKey = null,
): ReactPortal {
- if (__DEV__) {
- checkKeyStringCoercion(key);
+ let resolvedKey;
+ if (key == null) {
+ resolvedKey = null;
+ } else if (key === REACT_OPTIMISTIC_KEY) {
+ resolvedKey = REACT_OPTIMISTIC_KEY;
+ } else {
+ if (__DEV__) {
+ checkKeyStringCoercion(key);
+ }
+ resolvedKey = '' + key;
}
return {
// This tag allow us to uniquely identify this as a React Portal
$$typeof: REACT_PORTAL_TYPE,
- key: key == null ? null : '' + key,
+ key: resolvedKey,
children,
containerInfo,
implementation,
diff --git a/packages/react-reconciler/src/__tests__/ReactAsyncActions-test.js b/packages/react-reconciler/src/__tests__/ReactAsyncActions-test.js
index 20480eb287675..45d939fd13dad 100644
--- a/packages/react-reconciler/src/__tests__/ReactAsyncActions-test.js
+++ b/packages/react-reconciler/src/__tests__/ReactAsyncActions-test.js
@@ -1789,4 +1789,83 @@ describe('ReactAsyncActions', () => {
});
assertLog(['reportError: Oops']);
});
+
+ // @gate enableOptimisticKey
+ it('reconciles against new items when optimisticKey is used', async () => {
+ const startTransition = React.startTransition;
+
+ function Item({text}) {
+ const [initialText] = React.useState(text);
+ return {initialText + '-' + text};
+ }
+
+ let addOptimisticItem;
+ function App({items}) {
+ const [optimisticItems, _addOptimisticItem] = useOptimistic(
+ items,
+ (canonicalItems, optimisticText) =>
+ canonicalItems.concat({
+ id: React.optimisticKey,
+ text: optimisticText,
+ }),
+ );
+ addOptimisticItem = _addOptimisticItem;
+ return (
+
+ {optimisticItems.map(item => (
+
+ ))}
+
+ );
+ }
+
+ const A = {
+ id: 'a',
+ text: 'A',
+ };
+
+ const B = {
+ id: 'b',
+ text: 'B',
+ };
+
+ const root = ReactNoop.createRoot();
+ await act(() => {
+ root.render();
+ });
+ expect(root).toMatchRenderedOutput(
+
+ A-A
+
,
+ );
+
+ // Start an async action using the non-hook form of startTransition. The
+ // action includes an optimistic update.
+ await act(() => {
+ startTransition(async () => {
+ addOptimisticItem('b');
+ await getText('Yield before updating');
+ startTransition(() => root.render());
+ });
+ });
+ // Because the action hasn't finished yet, the optimistic UI is shown.
+ expect(root).toMatchRenderedOutput(
+
+ A-A
+ b-b
+
,
+ );
+
+ // Finish the async action. The optimistic state is reverted and replaced by
+ // the canonical state. The state is transferred to the new row.
+ await act(() => {
+ resolveText('Yield before updating');
+ });
+ expect(root).toMatchRenderedOutput(
+
+ A-A
+ b-B
+
,
+ );
+ });
});
diff --git a/packages/react-server/src/ReactFizzServer.js b/packages/react-server/src/ReactFizzServer.js
index 4f0ece3f98527..4ad48d79fba4f 100644
--- a/packages/react-server/src/ReactFizzServer.js
+++ b/packages/react-server/src/ReactFizzServer.js
@@ -27,6 +27,7 @@ import type {
SuspenseProps,
SuspenseListProps,
SuspenseListRevealOrder,
+ ReactKey,
} from 'shared/ReactTypes';
import type {LazyComponent as LazyComponentType} from 'react/src/ReactLazy';
import type {
@@ -170,6 +171,7 @@ import {
REACT_SCOPE_TYPE,
REACT_VIEW_TRANSITION_TYPE,
REACT_ACTIVITY_TYPE,
+ REACT_OPTIMISTIC_KEY,
} from 'shared/ReactSymbols';
import ReactSharedInternals from 'shared/ReactSharedInternals';
import {
@@ -3253,7 +3255,7 @@ function retryNode(request: Request, task: Task): void {
case REACT_ELEMENT_TYPE: {
const element: any = node;
const type = element.type;
- const key = element.key;
+ const key: ReactKey = element.key;
const props = element.props;
// TODO: We should get the ref off the props object right before using
@@ -3265,7 +3267,11 @@ function retryNode(request: Request, task: Task): void {
const name = getComponentNameFromType(type);
const keyOrIndex =
- key == null ? (childIndex === -1 ? 0 : childIndex) : key;
+ key == null || key === REACT_OPTIMISTIC_KEY
+ ? childIndex === -1
+ ? 0
+ : childIndex
+ : key;
const keyPath = [task.keyPath, name, keyOrIndex];
if (task.replay !== null) {
if (debugTask) {
diff --git a/packages/react-server/src/ReactFlightServer.js b/packages/react-server/src/ReactFlightServer.js
index f9729b535e073..31af0363fcf32 100644
--- a/packages/react-server/src/ReactFlightServer.js
+++ b/packages/react-server/src/ReactFlightServer.js
@@ -65,6 +65,7 @@ import type {
ReactFunctionLocation,
ReactErrorInfo,
ReactErrorInfoDev,
+ ReactKey,
} from 'shared/ReactTypes';
import type {ReactElement} from 'shared/ReactElementType';
import type {LazyComponent} from 'react/src/ReactLazy';
@@ -136,6 +137,7 @@ import {
REACT_LAZY_TYPE,
REACT_MEMO_TYPE,
ASYNC_ITERATOR,
+ REACT_OPTIMISTIC_KEY,
} from 'shared/ReactSymbols';
import {
@@ -532,7 +534,7 @@ type Task = {
model: ReactClientValue,
ping: () => void,
toJSON: (key: string, value: ReactClientValue) => ReactJSONValue,
- keyPath: null | string, // parent server component keys
+ keyPath: ReactKey, // parent server component keys
implicitSlot: boolean, // true if the root server component of this sequence had a null key
formatContext: FormatContext, // an approximate parent context from host components
thenableState: ThenableState | null,
@@ -1641,7 +1643,7 @@ function processServerComponentReturnValue(
function renderFunctionComponent(
request: Request,
task: Task,
- key: null | string,
+ key: ReactKey,
Component: (p: Props, arg: void) => any,
props: Props,
validated: number, // DEV-only
@@ -1812,7 +1814,12 @@ function renderFunctionComponent(
if (key !== null) {
// Append the key to the path. Technically a null key should really add the child
// index. We don't do that to hold the payload small and implementation simple.
- task.keyPath = prevKeyPath === null ? key : prevKeyPath + ',' + key;
+ if (key === REACT_OPTIMISTIC_KEY || prevKeyPath === REACT_OPTIMISTIC_KEY) {
+ // The optimistic key is viral. It turns the whole key into optimistic if any part is.
+ task.keyPath = REACT_OPTIMISTIC_KEY;
+ } else {
+ task.keyPath = prevKeyPath === null ? key : prevKeyPath + ',' + key;
+ }
} else if (prevKeyPath === null) {
// This sequence of Server Components has no keys. This means that it was rendered
// in a slot that needs to assign an implicit key. Even if children below have
@@ -1828,7 +1835,7 @@ function renderFunctionComponent(
function warnForMissingKey(
request: Request,
- key: null | string,
+ key: ReactKey,
componentDebugInfo: ReactComponentInfo,
debugTask: null | ConsoleTask,
): void {
@@ -2022,7 +2029,7 @@ function renderClientElement(
request: Request,
task: Task,
type: any,
- key: null | string,
+ key: ReactKey,
props: any,
validated: number, // DEV-only
): ReactJSONValue {
@@ -2032,7 +2039,12 @@ function renderClientElement(
if (key === null) {
key = keyPath;
} else if (keyPath !== null) {
- key = keyPath + ',' + key;
+ if (keyPath === REACT_OPTIMISTIC_KEY || key === REACT_OPTIMISTIC_KEY) {
+ // Optimistic key is viral and turns the whole key optimistic.
+ key = REACT_OPTIMISTIC_KEY;
+ } else {
+ key = keyPath + ',' + key;
+ }
}
let debugOwner = null;
let debugStack = null;
@@ -2159,7 +2171,7 @@ function renderElement(
request: Request,
task: Task,
type: any,
- key: null | string,
+ key: ReactKey,
ref: mixed,
props: any,
validated: number, // DEV only
@@ -2650,7 +2662,7 @@ function pingTask(request: Request, task: Task): void {
function createTask(
request: Request,
model: ReactClientValue,
- keyPath: null | string,
+ keyPath: ReactKey,
implicitSlot: boolean,
formatContext: FormatContext,
abortSet: Set,
@@ -3504,7 +3516,7 @@ function renderModelDestructive(
element._debugTask === undefined
) {
let key = '';
- if (element.key !== null) {
+ if (element.key !== null && element.key !== REACT_OPTIMISTIC_KEY) {
key = ' key="' + element.key + '"';
}
@@ -3530,7 +3542,7 @@ function renderModelDestructive(
request,
task,
element.type,
- // $FlowFixMe[incompatible-call] the key of an element is null | string
+ // $FlowFixMe[incompatible-call] the key of an element is null | string | ReactOptimisticKey
element.key,
ref,
props,
diff --git a/packages/react/index.experimental.development.js b/packages/react/index.experimental.development.js
index 9b0e301a8c9be..8ff2e1d257f4c 100644
--- a/packages/react/index.experimental.development.js
+++ b/packages/react/index.experimental.development.js
@@ -29,6 +29,7 @@ export {
cache,
cacheSignal,
startTransition,
+ optimisticKey,
Activity,
unstable_getCacheForType,
unstable_SuspenseList,
diff --git a/packages/react/index.experimental.js b/packages/react/index.experimental.js
index 0145e9137e9b4..881a71b2501dd 100644
--- a/packages/react/index.experimental.js
+++ b/packages/react/index.experimental.js
@@ -29,6 +29,7 @@ export {
cache,
cacheSignal,
startTransition,
+ optimisticKey,
Activity,
Activity as unstable_Activity,
unstable_getCacheForType,
diff --git a/packages/react/src/ReactChildren.js b/packages/react/src/ReactChildren.js
index 9b474ac832f2b..d4c41d6669a38 100644
--- a/packages/react/src/ReactChildren.js
+++ b/packages/react/src/ReactChildren.js
@@ -22,7 +22,9 @@ import {
REACT_ELEMENT_TYPE,
REACT_LAZY_TYPE,
REACT_PORTAL_TYPE,
+ REACT_OPTIMISTIC_KEY,
} from 'shared/ReactSymbols';
+import {enableOptimisticKey} from 'shared/ReactFeatureFlags';
import {checkKeyStringCoercion} from 'shared/CheckStringCoercion';
import {isValidElement, cloneAndReplaceKey} from './jsx/ReactJSXElement';
@@ -73,6 +75,13 @@ function getElementKey(element: any, index: number): string {
// Do some typechecking here since we call this blindly. We want to ensure
// that we don't block potential future ES APIs.
if (typeof element === 'object' && element !== null && element.key != null) {
+ if (enableOptimisticKey && element.key === REACT_OPTIMISTIC_KEY) {
+ // For React.Children purposes this is treated as just null.
+ if (__DEV__) {
+ console.error("React.Children helpers don't support optimisticKey.");
+ }
+ return index.toString(36);
+ }
// Explicit key
if (__DEV__) {
checkKeyStringCoercion(element.key);
diff --git a/packages/react/src/ReactClient.js b/packages/react/src/ReactClient.js
index d881030b7d090..5d1c7f3ac05e9 100644
--- a/packages/react/src/ReactClient.js
+++ b/packages/react/src/ReactClient.js
@@ -19,6 +19,7 @@ import {
REACT_SCOPE_TYPE,
REACT_TRACING_MARKER_TYPE,
REACT_VIEW_TRANSITION_TYPE,
+ REACT_OPTIMISTIC_KEY,
} from 'shared/ReactSymbols';
import {Component, PureComponent} from './ReactBaseClasses';
@@ -127,6 +128,8 @@ export {
addTransitionType as addTransitionType,
// enableGestureTransition
startGestureTransition as unstable_startGestureTransition,
+ // enableOptimisticKey
+ REACT_OPTIMISTIC_KEY as optimisticKey,
// DEV-only
useId,
act,
diff --git a/packages/react/src/ReactServer.experimental.development.js b/packages/react/src/ReactServer.experimental.development.js
index 10d0123843d74..dd92bf9104f4c 100644
--- a/packages/react/src/ReactServer.experimental.development.js
+++ b/packages/react/src/ReactServer.experimental.development.js
@@ -18,6 +18,7 @@ import {
REACT_SUSPENSE_LIST_TYPE,
REACT_VIEW_TRANSITION_TYPE,
REACT_ACTIVITY_TYPE,
+ REACT_OPTIMISTIC_KEY,
} from 'shared/ReactSymbols';
import {
cloneElement,
@@ -82,5 +83,7 @@ export {
version,
// Experimental
REACT_SUSPENSE_LIST_TYPE as unstable_SuspenseList,
+ // enableOptimisticKey
+ REACT_OPTIMISTIC_KEY as optimisticKey,
captureOwnerStack, // DEV-only
};
diff --git a/packages/react/src/ReactServer.experimental.js b/packages/react/src/ReactServer.experimental.js
index 9fc2634131472..0eb8eafccd439 100644
--- a/packages/react/src/ReactServer.experimental.js
+++ b/packages/react/src/ReactServer.experimental.js
@@ -18,6 +18,7 @@ import {
REACT_SUSPENSE_LIST_TYPE,
REACT_VIEW_TRANSITION_TYPE,
REACT_ACTIVITY_TYPE,
+ REACT_OPTIMISTIC_KEY,
} from 'shared/ReactSymbols';
import {
cloneElement,
@@ -81,4 +82,6 @@ export {
version,
// Experimental
REACT_SUSPENSE_LIST_TYPE as unstable_SuspenseList,
+ // enableOptimisticKey
+ REACT_OPTIMISTIC_KEY as optimisticKey,
};
diff --git a/packages/react/src/jsx/ReactJSXElement.js b/packages/react/src/jsx/ReactJSXElement.js
index e23c998da511b..3a5a5d51a9b85 100644
--- a/packages/react/src/jsx/ReactJSXElement.js
+++ b/packages/react/src/jsx/ReactJSXElement.js
@@ -13,10 +13,11 @@ import {
REACT_ELEMENT_TYPE,
REACT_FRAGMENT_TYPE,
REACT_LAZY_TYPE,
+ REACT_OPTIMISTIC_KEY,
} from 'shared/ReactSymbols';
import {checkKeyStringCoercion} from 'shared/CheckStringCoercion';
import isArray from 'shared/isArray';
-import {ownerStackLimit} from 'shared/ReactFeatureFlags';
+import {ownerStackLimit, enableOptimisticKey} from 'shared/ReactFeatureFlags';
const createTask =
// eslint-disable-next-line react-internal/no-production-logging
@@ -297,17 +298,25 @@ export function jsxProd(type, config, maybeKey) {
// , because we aren't currently able to tell if
// key is explicitly declared to be undefined or not.
if (maybeKey !== undefined) {
- if (__DEV__) {
- checkKeyStringCoercion(maybeKey);
+ if (enableOptimisticKey && maybeKey === REACT_OPTIMISTIC_KEY) {
+ key = REACT_OPTIMISTIC_KEY;
+ } else {
+ if (__DEV__) {
+ checkKeyStringCoercion(maybeKey);
+ }
+ key = '' + maybeKey;
}
- key = '' + maybeKey;
}
if (hasValidKey(config)) {
- if (__DEV__) {
- checkKeyStringCoercion(config.key);
+ if (enableOptimisticKey && maybeKey === REACT_OPTIMISTIC_KEY) {
+ key = REACT_OPTIMISTIC_KEY;
+ } else {
+ if (__DEV__) {
+ checkKeyStringCoercion(config.key);
+ }
+ key = '' + config.key;
}
- key = '' + config.key;
}
let props;
@@ -536,17 +545,25 @@ function jsxDEVImpl(
// , because we aren't currently able to tell if
// key is explicitly declared to be undefined or not.
if (maybeKey !== undefined) {
- if (__DEV__) {
- checkKeyStringCoercion(maybeKey);
+ if (enableOptimisticKey && maybeKey === REACT_OPTIMISTIC_KEY) {
+ key = REACT_OPTIMISTIC_KEY;
+ } else {
+ if (__DEV__) {
+ checkKeyStringCoercion(maybeKey);
+ }
+ key = '' + maybeKey;
}
- key = '' + maybeKey;
}
if (hasValidKey(config)) {
- if (__DEV__) {
- checkKeyStringCoercion(config.key);
+ if (enableOptimisticKey && config.key === REACT_OPTIMISTIC_KEY) {
+ key = REACT_OPTIMISTIC_KEY;
+ } else {
+ if (__DEV__) {
+ checkKeyStringCoercion(config.key);
+ }
+ key = '' + config.key;
}
- key = '' + config.key;
}
let props;
@@ -637,10 +654,14 @@ export function createElement(type, config, children) {
}
if (hasValidKey(config)) {
- if (__DEV__) {
- checkKeyStringCoercion(config.key);
+ if (enableOptimisticKey && config.key === REACT_OPTIMISTIC_KEY) {
+ key = REACT_OPTIMISTIC_KEY;
+ } else {
+ if (__DEV__) {
+ checkKeyStringCoercion(config.key);
+ }
+ key = '' + config.key;
}
- key = '' + config.key;
}
// Remaining properties are added to a new props object
@@ -769,10 +790,14 @@ export function cloneElement(element, config, children) {
owner = __DEV__ ? getOwner() : undefined;
}
if (hasValidKey(config)) {
- if (__DEV__) {
- checkKeyStringCoercion(config.key);
+ if (enableOptimisticKey && config.key === REACT_OPTIMISTIC_KEY) {
+ key = REACT_OPTIMISTIC_KEY;
+ } else {
+ if (__DEV__) {
+ checkKeyStringCoercion(config.key);
+ }
+ key = '' + config.key;
}
- key = '' + config.key;
}
// Remaining properties override existing props
diff --git a/packages/shared/ReactFeatureFlags.js b/packages/shared/ReactFeatureFlags.js
index a5befa11a0d5c..ebb287568af8a 100644
--- a/packages/shared/ReactFeatureFlags.js
+++ b/packages/shared/ReactFeatureFlags.js
@@ -98,6 +98,8 @@ export const enableHydrationChangeEvent = __EXPERIMENTAL__;
export const enableDefaultTransitionIndicator = __EXPERIMENTAL__;
+export const enableOptimisticKey = __EXPERIMENTAL__;
+
/**
* Switches Fiber creation to a simple object instead of a constructor.
*/
diff --git a/packages/shared/ReactSymbols.js b/packages/shared/ReactSymbols.js
index f8ebf703f463d..d22d4ba12ee98 100644
--- a/packages/shared/ReactSymbols.js
+++ b/packages/shared/ReactSymbols.js
@@ -65,3 +65,12 @@ export function getIteratorFn(maybeIterable: ?any): ?() => ?Iterator {
}
export const ASYNC_ITERATOR = Symbol.asyncIterator;
+
+export const REACT_OPTIMISTIC_KEY: ReactOptimisticKey = (Symbol.for(
+ 'react.optimistic_key',
+): any);
+
+// This is actually a symbol but Flow doesn't support comparison of symbols to refine.
+// We use a boolean since in our code we often expect string (key) or number (index),
+// so by pretending to be a boolean we cover a lot of cases that don't consider this case.
+export type ReactOptimisticKey = true;
diff --git a/packages/shared/ReactTypes.js b/packages/shared/ReactTypes.js
index bcdda6da2a7c2..65ed43c063ce9 100644
--- a/packages/shared/ReactTypes.js
+++ b/packages/shared/ReactTypes.js
@@ -7,6 +7,12 @@
* @flow
*/
+import type {ReactOptimisticKey} from './ReactSymbols';
+
+export type {ReactOptimisticKey};
+
+export type ReactKey = null | string | ReactOptimisticKey;
+
export type ReactNode =
| React$Element
| ReactPortal
@@ -26,7 +32,7 @@ export type ReactText = string | number;
export type ReactProvider = {
$$typeof: symbol | number,
type: ReactContext,
- key: null | string,
+ key: ReactKey,
ref: null,
props: {
value: T,
@@ -42,7 +48,7 @@ export type ReactConsumerType = {
export type ReactConsumer = {
$$typeof: symbol | number,
type: ReactConsumerType,
- key: null | string,
+ key: ReactKey,
ref: null,
props: {
children: (value: T) => ReactNodeList,
@@ -66,7 +72,7 @@ export type ReactContext = {
export type ReactPortal = {
$$typeof: symbol | number,
- key: null | string,
+ key: ReactKey,
containerInfo: any,
children: ReactNodeList,
// TODO: figure out the API for cross-renderer implementation.
@@ -204,7 +210,7 @@ export type ReactFunctionLocation = [
export type ReactComponentInfo = {
+name: string,
+env?: string,
- +key?: null | string,
+ +key?: ReactKey,
+owner?: null | ReactComponentInfo,
+stack?: null | ReactStackTrace,
+props?: null | {[name: string]: mixed},
diff --git a/packages/shared/forks/ReactFeatureFlags.native-fb.js b/packages/shared/forks/ReactFeatureFlags.native-fb.js
index 904b2c9837dd2..d9a91f8a808f5 100644
--- a/packages/shared/forks/ReactFeatureFlags.native-fb.js
+++ b/packages/shared/forks/ReactFeatureFlags.native-fb.js
@@ -85,6 +85,7 @@ export const enableComponentPerformanceTrack: boolean =
export const enablePerformanceIssueReporting: boolean =
enableComponentPerformanceTrack;
export const enableInternalInstanceMap: boolean = false;
+export const enableOptimisticKey: boolean = false;
// Flow magic to verify the exports of this file match the original version.
((((null: any): ExportsType): FeatureFlagsType): ExportsType);
diff --git a/packages/shared/forks/ReactFeatureFlags.native-oss.js b/packages/shared/forks/ReactFeatureFlags.native-oss.js
index 183ae65bc22b4..fa8f336c03f1d 100644
--- a/packages/shared/forks/ReactFeatureFlags.native-oss.js
+++ b/packages/shared/forks/ReactFeatureFlags.native-oss.js
@@ -78,6 +78,8 @@ export const enableFragmentRefsInstanceHandles: boolean = false;
export const enableInternalInstanceMap: boolean = false;
+export const enableOptimisticKey: boolean = false;
+
// Profiling Only
export const enableProfilerTimer: boolean = __PROFILE__;
export const enableProfilerCommitHooks: boolean = __PROFILE__;
diff --git a/packages/shared/forks/ReactFeatureFlags.test-renderer.js b/packages/shared/forks/ReactFeatureFlags.test-renderer.js
index 0747f0e8be433..acf3847bd065a 100644
--- a/packages/shared/forks/ReactFeatureFlags.test-renderer.js
+++ b/packages/shared/forks/ReactFeatureFlags.test-renderer.js
@@ -93,5 +93,7 @@ export const enableReactTestRendererWarning: boolean = true;
export const enableObjectFiber: boolean = false;
+export const enableOptimisticKey: boolean = false;
+
// Flow magic to verify the exports of this file match the original version.
((((null: any): ExportsType): FeatureFlagsType): ExportsType);
diff --git a/packages/shared/forks/ReactFeatureFlags.test-renderer.native-fb.js b/packages/shared/forks/ReactFeatureFlags.test-renderer.native-fb.js
index d9c425319f91a..5d3a551301823 100644
--- a/packages/shared/forks/ReactFeatureFlags.test-renderer.native-fb.js
+++ b/packages/shared/forks/ReactFeatureFlags.test-renderer.native-fb.js
@@ -69,6 +69,7 @@ export const enableDefaultTransitionIndicator = true;
export const enableFragmentRefs = false;
export const enableFragmentRefsScrollIntoView = false;
export const ownerStackLimit = 1e4;
+export const enableOptimisticKey = false;
// Flow magic to verify the exports of this file match the original version.
((((null: any): ExportsType): FeatureFlagsType): ExportsType);
diff --git a/packages/shared/forks/ReactFeatureFlags.test-renderer.www.js b/packages/shared/forks/ReactFeatureFlags.test-renderer.www.js
index 2b40c1b01c6c0..553be202c45ea 100644
--- a/packages/shared/forks/ReactFeatureFlags.test-renderer.www.js
+++ b/packages/shared/forks/ReactFeatureFlags.test-renderer.www.js
@@ -87,5 +87,7 @@ export const ownerStackLimit = 1e4;
export const enableInternalInstanceMap: boolean = false;
+export const enableOptimisticKey: boolean = false;
+
// Flow magic to verify the exports of this file match the original version.
((((null: any): ExportsType): FeatureFlagsType): ExportsType);
diff --git a/packages/shared/forks/ReactFeatureFlags.www.js b/packages/shared/forks/ReactFeatureFlags.www.js
index 029cd0d196e15..87801a9658f68 100644
--- a/packages/shared/forks/ReactFeatureFlags.www.js
+++ b/packages/shared/forks/ReactFeatureFlags.www.js
@@ -114,5 +114,7 @@ export const ownerStackLimit = 1e4;
export const enableFragmentRefsInstanceHandles: boolean = true;
+export const enableOptimisticKey: boolean = false;
+
// Flow magic to verify the exports of this file match the original version.
((((null: any): ExportsType): FeatureFlagsType): ExportsType);