@samitouri / QOS-React-1 / commits / eb89912ee5

Add expertimental `optimisticKey` behind a flag (#35162)

When dealing with optimistic state, a common problem is not knowing the id of the thing we're waiting on. Items in lists need keys (and single items should often have keys too to reset their state). As a result you have to generate fake keys. It's a pain to manage those and when the real item comes in, you often end up rendering that with a different `key` which resets the state of the component tree. That in turns works against the grain of React and a lot of negatives fall out of it. This adds a special `optimisticKey` symbol that can be used in place of a `string` key. ```js import {optimisticKey} from 'react'; ... const [optimisticItems, setOptimisticItems] = useOptimistic([]); const children = savedItems.concat( optimisticItems.map(item => <Item key={optimisticKey} item={item} /> ) ); return <div>{children}</div>; ``` The semantics of this `optimisticKey` is that the assumption is that the newly saved item will be rendered in the same slot as the previous optimistic items. State is transferred into whatever real key ends up in the same slot. This might lead to some incorrect transferring of state in some cases where things don't end up lining up - but it's worth it for simplicity in many cases since dealing with true matching of optimistic state is often very complex for something that only lasts a blink of an eye. If a new item matches a `key` elsewhere in the set, then that's favored over reconciling against the old slot. One quirk with the current algorithm is if the `savedItems` has items removed, then the slots won't line up by index anymore and will be skewed. We might be able to add something where the optimistic set is always reconciled against the end. However, it's probably better to just assume that the set will line up perfectly and otherwise it's just best effort that can lead to weird artifacts. An `optimisticKey` will match itself for updates to the same slot, but it will not match any existing slot that is not an `optimisticKey`. So it's not an `any`, which I originally called it, because it doesn't match existing real keys against new optimistic keys. Only one direction.

Sebastian Markbåge committed Nov 18, 2025 at 16:29 UTC eb89912ee5ace8bf8e616cca5a6aeebcd274b521
27 files changed +454 -83
packages/react-client/src/__tests__/ReactFlight-test.js
+15
@@ -3884,4 +3884,19 @@ describe('ReactFlight', () => {
3884 </main>,
3885 );
3886 });
3887 +
3888 + // @gate enableOptimisticKey
3889 + it('collapses optimistic keys to an optimistic key', async () => {
3890 + function Bar({text}) {
3891 + return <div />;
3892 + }
3893 + function Foo() {
3894 + return <Bar key={ReactServer.optimisticKey} />;
3895 + }
3896 + const transport = ReactNoopFlightServer.render({
3897 + element: <Foo key="Outer Key" />,
3898 + });
3899 + const model = await ReactNoopFlightClient.read(transport);
3900 + expect(model.element.key).toBe(React.optimisticKey);
3901 + });
3902 });
packages/react-devtools-shared/src/backend/fiber/renderer.js
+18 -6
@@ -120,6 +120,7 @@ import {
120 MEMO_SYMBOL_STRING,
121 SERVER_CONTEXT_SYMBOL_STRING,
122 LAZY_SYMBOL_STRING,
123 + REACT_OPTIMISTIC_KEY,
124 } from '../shared/ReactSymbols';
125 import {enableStyleXFeatures} from 'react-devtools-feature-flags';
126
@@ -4849,7 +4850,10 @@ export function attach(
4850 }
4851 let previousSiblingOfBestMatch = null;
4852 let bestMatch = remainingReconcilingChildren;
4852 - if (componentInfo.key != null) {
4853 + if (
4854 + componentInfo.key != null &&
4855 + componentInfo.key !== REACT_OPTIMISTIC_KEY
4856 + ) {
4857 // If there is a key try to find a matching key in the set.
4858 bestMatch = remainingReconcilingChildren;
4859 while (bestMatch !== null) {
@@ -6145,7 +6149,7 @@ export function attach(
6149 return {
6150 displayName: getDisplayNameForFiber(fiber) || 'Anonymous',
6151 id: instance.id,
6148 - key: fiber.key,
6152 + key: fiber.key === REACT_OPTIMISTIC_KEY ? null : fiber.key,
6153 env: null,
6154 stack:
6155 fiber._debugOwner == null || fiber._debugStack == null
@@ -6158,7 +6162,11 @@ export function attach(
6162 return {
6163 displayName: componentInfo.name || 'Anonymous',
6164 id: instance.id,
6161 - key: componentInfo.key == null ? null : componentInfo.key,
6165 + key:
6166 + componentInfo.key == null ||
6167 + componentInfo.key === REACT_OPTIMISTIC_KEY
6168 + ? null
6169 + : componentInfo.key,
6170 env: componentInfo.env == null ? null : componentInfo.env,
6171 stack:
6172 componentInfo.owner == null || componentInfo.debugStack == null
@@ -7082,7 +7090,7 @@ export function attach(
7090 // Does the component have legacy context attached to it.
7091 hasLegacyContext,
7092
7085 - key: key != null ? key : null,
7093 + key: key != null && key !== REACT_OPTIMISTIC_KEY ? key : null,
7094
7095 type: elementType,
7096
@@ -8641,7 +8649,7 @@ export function attach(
8649 }
8650 return {
8651 displayName,
8644 - key,
8652 + key: key === REACT_OPTIMISTIC_KEY ? null : key,
8653 index,
8654 };
8655 }
@@ -8649,7 +8657,11 @@ export function attach(
8657 function getVirtualPathFrame(virtualInstance: VirtualInstance): PathFrame {
8658 return {
8659 displayName: virtualInstance.data.name || '',
8652 - key: virtualInstance.data.key == null ? null : virtualInstance.data.key,
8660 + key:
8661 + virtualInstance.data.key == null ||
8662 + virtualInstance.data.key === REACT_OPTIMISTIC_KEY
8663 + ? null
8664 + : virtualInstance.data.key,
8665 index: -1, // We use -1 to indicate that this is a virtual path frame.
8666 };
8667 }
packages/react-devtools-shared/src/backend/shared/ReactSymbols.js
+6
@@ -72,3 +72,9 @@ export const SERVER_CONTEXT_DEFAULT_VALUE_NOT_LOADED_SYMBOL_STRING =
72 export const REACT_MEMO_CACHE_SENTINEL: symbol = Symbol.for(
73 'react.memo_cache_sentinel',
74 );
75 +
76 +import type {ReactOptimisticKey} from 'shared/ReactTypes';
77 +
78 +export const REACT_OPTIMISTIC_KEY: ReactOptimisticKey = (Symbol.for(
79 + 'react.optimistic_key',
80 +): any);
packages/react-dom/src/__tests__/ReactDOMFizzStaticBrowser-test.js
+60
@@ -1111,4 +1111,64 @@ describe('ReactDOMFizzStaticBrowser', () => {
1111 </div>,
1112 );
1113 });
1114 +
1115 + // @gate enableHalt && enableOptimisticKey
1116 + it('can resume an optimistic keyed slot', async () => {
1117 + const errors = [];
1118 +
1119 + let resolve;
1120 + const promise = new Promise(r => (resolve = r));
1121 +
1122 + async function Component() {
1123 + await promise;
1124 + return 'Hi';
1125 + }
1126 +
1127 + if (React.optimisticKey === undefined) {
1128 + throw new Error('optimisticKey missing');
1129 + }
1130 +
1131 + function App() {
1132 + return (
1133 + <div>
1134 + <Suspense fallback="Loading">
1135 + <Component key={React.optimisticKey} />
1136 + </Suspense>
1137 + </div>
1138 + );
1139 + }
1140 +
1141 + const controller = new AbortController();
1142 + const pendingResult = serverAct(() =>
1143 + ReactDOMFizzStatic.prerender(<App />, {
1144 + signal: controller.signal,
1145 + onError(x) {
1146 + errors.push(x.message);
1147 + },
1148 + }),
1149 + );
1150 +
1151 + await serverAct(() => {
1152 + controller.abort();
1153 + });
1154 +
1155 + const prerendered = await pendingResult;
1156 +
1157 + const postponedState = JSON.stringify(prerendered.postponed);
1158 +
1159 + await readIntoContainer(prerendered.prelude);
1160 + expect(getVisibleChildren(container)).toEqual(<div>Loading</div>);
1161 +
1162 + expect(prerendered.postponed).not.toBe(null);
1163 +
1164 + await resolve();
1165 +
1166 + const dynamic = await serverAct(() =>
1167 + ReactDOMFizzServer.resume(<App />, JSON.parse(postponedState)),
1168 + );
1169 +
1170 + await readIntoContainer(dynamic);
1171 +
1172 + expect(getVisibleChildren(container)).toEqual(<div>Hi</div>);
1173 + });
1174 });
packages/react-reconciler/src/ReactChildFiber.js
+102 -21
@@ -15,6 +15,8 @@ import type {
15 ReactDebugInfo,
16 ReactComponentInfo,
17 SuspenseListRevealOrder,
18 + ReactKey,
19 + ReactOptimisticKey,
20 } from 'shared/ReactTypes';
21 import type {Fiber} from './ReactInternalTypes';
22 import type {Lanes} from './ReactFiberLane';
@@ -37,6 +39,7 @@ import {
39 REACT_LAZY_TYPE,
40 REACT_CONTEXT_TYPE,
41 REACT_LEGACY_ELEMENT_TYPE,
42 + REACT_OPTIMISTIC_KEY,
43 } from 'shared/ReactSymbols';
44 import {
45 HostRoot,
@@ -50,6 +53,7 @@ import {
53 enableAsyncIterableChildren,
54 disableLegacyMode,
55 enableFragmentRefs,
56 + enableOptimisticKey,
57 } from 'shared/ReactFeatureFlags';
58
59 import {
@@ -462,18 +466,33 @@ function createChildReconciler(
466
467 function mapRemainingChildren(
468 currentFirstChild: Fiber,
465 - ): Map<string | number, Fiber> {
469 + ): Map<string | number | ReactOptimisticKey, Fiber> {
470 // Add the remaining children to a temporary map so that we can find them by
471 // keys quickly. Implicit (null) keys get added to this set with their index
472 // instead.
469 - const existingChildren: Map<string | number, Fiber> = new Map();
473 + const existingChildren: Map<
474 + | string
475 + | number
476 + // This type is only here for the case when enableOptimisticKey is disabled.
477 + // Remove it after it ships.
478 + | ReactOptimisticKey,
479 + Fiber,
480 + > = new Map();
481
482 let existingChild: null | Fiber = currentFirstChild;
483 while (existingChild !== null) {
473 - if (existingChild.key !== null) {
474 - existingChildren.set(existingChild.key, existingChild);
475 - } else {
484 + if (existingChild.key === null) {
485 existingChildren.set(existingChild.index, existingChild);
486 + } else if (
487 + enableOptimisticKey &&
488 + existingChild.key === REACT_OPTIMISTIC_KEY
489 + ) {
490 + // For optimistic keys, we store the negative index (minus one) to differentiate
491 + // them from the regular indices. We'll look this up regardless of what the new
492 + // key is, if there's no other match.
493 + existingChildren.set(-existingChild.index - 1, existingChild);
494 + } else {
495 + existingChildren.set(existingChild.key, existingChild);
496 }
497 existingChild = existingChild.sibling;
498 }
@@ -636,6 +655,10 @@ function createChildReconciler(
655 } else {
656 // Update
657 const existing = useFiber(current, portal.children || []);
658 + if (enableOptimisticKey) {
659 + // If the old key was optimistic we need to now save the real one.
660 + existing.key = portal.key;
661 + }
662 existing.return = returnFiber;
663 if (__DEV__) {
664 existing._debugInfo = currentDebugInfo;
@@ -649,7 +672,7 @@ function createChildReconciler(
672 current: Fiber | null,
673 fragment: Iterable<React$Node>,
674 lanes: Lanes,
652 - key: null | string,
675 + key: ReactKey,
676 ): Fiber {
677 if (current === null || current.tag !== Fragment) {
678 // Insert
@@ -670,6 +693,10 @@ function createChildReconciler(
693 } else {
694 // Update
695 const existing = useFiber(current, fragment);
696 + if (enableOptimisticKey) {
697 + // If the old key was optimistic we need to now save the real one.
698 + existing.key = key;
699 + }
700 existing.return = returnFiber;
701 if (__DEV__) {
702 existing._debugInfo = currentDebugInfo;
@@ -840,7 +867,13 @@ function createChildReconciler(
867 if (typeof newChild === 'object' && newChild !== null) {
868 switch (newChild.$$typeof) {
869 case REACT_ELEMENT_TYPE: {
843 - if (newChild.key === key) {
870 + if (
871 + // If the old child was an optimisticKey, then we'd normally consider that a match,
872 + // but instead, we'll bail to return null from the slot which will bail to slow path.
873 + // That's to ensure that if the new key has a match elsewhere in the list, then that
874 + // takes precedence over assuming the identity of an optimistic slot.
875 + newChild.key === key
876 + ) {
877 const prevDebugInfo = pushDebugInfo(newChild._debugInfo);
878 const updated = updateElement(
879 returnFiber,
@@ -855,7 +888,13 @@ function createChildReconciler(
888 }
889 }
890 case REACT_PORTAL_TYPE: {
858 - if (newChild.key === key) {
891 + if (
892 + // If the old child was an optimisticKey, then we'd normally consider that a match,
893 + // but instead, we'll bail to return null from the slot which will bail to slow path.
894 + // That's to ensure that if the new key has a match elsewhere in the list, then that
895 + // takes precedence over assuming the identity of an optimistic slot.
896 + newChild.key === key
897 + ) {
898 return updatePortal(returnFiber, oldFiber, newChild, lanes);
899 } else {
900 return null;
@@ -939,7 +978,7 @@ function createChildReconciler(
978 }
979
980 function updateFromMap(
942 - existingChildren: Map<string | number, Fiber>,
981 + existingChildren: Map<string | number | ReactOptimisticKey, Fiber>,
982 returnFiber: Fiber,
983 newIdx: number,
984 newChild: any,
@@ -968,7 +1007,11 @@ function createChildReconciler(
1007 const matchedFiber =
1008 existingChildren.get(
1009 newChild.key === null ? newIdx : newChild.key,
971 - ) || null;
1010 + ) ||
1011 + (enableOptimisticKey &&
1012 + // If the existing child was an optimistic key, we may still match on the index.
1013 + existingChildren.get(-newIdx - 1)) ||
1014 + null;
1015 const prevDebugInfo = pushDebugInfo(newChild._debugInfo);
1016 const updated = updateElement(
1017 returnFiber,
@@ -983,7 +1026,11 @@ function createChildReconciler(
1026 const matchedFiber =
1027 existingChildren.get(
1028 newChild.key === null ? newIdx : newChild.key,
986 - ) || null;
1029 + ) ||
1030 + (enableOptimisticKey &&
1031 + // If the existing child was an optimistic key, we may still match on the index.
1032 + existingChildren.get(-newIdx - 1)) ||
1033 + null;
1034 return updatePortal(returnFiber, matchedFiber, newChild, lanes);
1035 }
1036 case REACT_LAZY_TYPE: {
@@ -1274,14 +1321,22 @@ function createChildReconciler(
1321 );
1322 }
1323 if (shouldTrackSideEffects) {
1277 - if (newFiber.alternate !== null) {
1324 + const currentFiber = newFiber.alternate;
1325 + if (currentFiber !== null) {
1326 // The new fiber is a work in progress, but if there exists a
1327 // current, that means that we reused the fiber. We need to delete
1328 // it from the child list so that we don't add it to the deletion
1329 // list.
1282 - existingChildren.delete(
1283 - newFiber.key === null ? newIdx : newFiber.key,
1284 - );
1330 + if (
1331 + enableOptimisticKey &&
1332 + currentFiber.key === REACT_OPTIMISTIC_KEY
1333 + ) {
1334 + existingChildren.delete(-newIdx - 1);
1335 + } else {
1336 + existingChildren.delete(
1337 + currentFiber.key === null ? newIdx : currentFiber.key,
1338 + );
1339 + }
1340 }
1341 }
1342 lastPlacedIndex = placeChild(newFiber, lastPlacedIndex, newIdx);
@@ -1568,14 +1623,22 @@ function createChildReconciler(
1623 );
1624 }
1625 if (shouldTrackSideEffects) {
1571 - if (newFiber.alternate !== null) {
1626 + const currentFiber = newFiber.alternate;
1627 + if (currentFiber !== null) {
1628 // The new fiber is a work in progress, but if there exists a
1629 // current, that means that we reused the fiber. We need to delete
1630 // it from the child list so that we don't add it to the deletion
1631 // list.
1576 - existingChildren.delete(
1577 - newFiber.key === null ? newIdx : newFiber.key,
1578 - );
1632 + if (
1633 + enableOptimisticKey &&
1634 + currentFiber.key === REACT_OPTIMISTIC_KEY
1635 + ) {
1636 + existingChildren.delete(-newIdx - 1);
1637 + } else {
1638 + existingChildren.delete(
1639 + currentFiber.key === null ? newIdx : currentFiber.key,
1640 + );
1641 + }
1642 }
1643 }
1644 lastPlacedIndex = placeChild(newFiber, lastPlacedIndex, newIdx);
@@ -1642,12 +1705,19 @@ function createChildReconciler(
1705 while (child !== null) {
1706 // TODO: If key === null and child.key === null, then this only applies to
1707 // the first item in the list.
1645 - if (child.key === key) {
1708 + if (
1709 + child.key === key ||
1710 + (enableOptimisticKey && child.key === REACT_OPTIMISTIC_KEY)
1711 + ) {
1712 const elementType = element.type;
1713 if (elementType === REACT_FRAGMENT_TYPE) {
1714 if (child.tag === Fragment) {
1715 deleteRemainingChildren(returnFiber, child.sibling);
1716 const existing = useFiber(child, element.props.children);
1717 + if (enableOptimisticKey) {
1718 + // If the old key was optimistic we need to now save the real one.
1719 + existing.key = key;
1720 + }
1721 if (enableFragmentRefs) {
1722 coerceRef(existing, element);
1723 }
@@ -1677,6 +1747,10 @@ function createChildReconciler(
1747 ) {
1748 deleteRemainingChildren(returnFiber, child.sibling);
1749 const existing = useFiber(child, element.props);
1750 + if (enableOptimisticKey) {
1751 + // If the old key was optimistic we need to now save the real one.
1752 + existing.key = key;
1753 + }
1754 coerceRef(existing, element);
1755 existing.return = returnFiber;
1756 if (__DEV__) {
@@ -1736,7 +1810,10 @@ function createChildReconciler(
1810 while (child !== null) {
1811 // TODO: If key === null and child.key === null, then this only applies to
1812 // the first item in the list.
1739 - if (child.key === key) {
1813 + if (
1814 + child.key === key ||
1815 + (enableOptimisticKey && child.key === REACT_OPTIMISTIC_KEY)
1816 + ) {
1817 if (
1818 child.tag === HostPortal &&
1819 child.stateNode.containerInfo === portal.containerInfo &&
@@ -1744,6 +1821,10 @@ function createChildReconciler(
1821 ) {
1822 deleteRemainingChildren(returnFiber, child.sibling);
1823 const existing = useFiber(child, portal.children || []);
1824 + if (enableOptimisticKey) {
1825 + // If the old key was optimistic we need to now save the real one.
1826 + existing.key = key;
1827 + }
1828 existing.return = returnFiber;
1829 return existing;
1830 } else {
packages/react-reconciler/src/ReactFiber.js
+29 -14
@@ -14,6 +14,7 @@ import type {
14 ReactScope,
15 ViewTransitionProps,
16 ActivityProps,
17 + ReactKey,
18 } from 'shared/ReactTypes';
19 import type {Fiber} from './ReactInternalTypes';
20 import type {RootTag} from './ReactRootTags';
@@ -43,6 +44,7 @@ import {
44 enableObjectFiber,
45 enableViewTransition,
46 enableSuspenseyImages,
47 + enableOptimisticKey,
48 } from 'shared/ReactFeatureFlags';
49 import {NoFlags, Placement, StaticMask} from './ReactFiberFlags';
50 import {ConcurrentRoot} from './ReactRootTags';
@@ -137,7 +139,7 @@ function FiberNode(
139 this: $FlowFixMe,
140 tag: WorkTag,
141 pendingProps: mixed,
140 - key: null | string,
142 + key: ReactKey,
143 mode: TypeOfMode,
144 ) {
145 // Instance
@@ -224,7 +226,7 @@ function FiberNode(
226 function createFiberImplClass(
227 tag: WorkTag,
228 pendingProps: mixed,
227 - key: null | string,
229 + key: ReactKey,
230 mode: TypeOfMode,
231 ): Fiber {
232 // $FlowFixMe[invalid-constructor]: the shapes are exact here but Flow doesn't like constructors
@@ -234,7 +236,7 @@ function createFiberImplClass(
236 function createFiberImplObject(
237 tag: WorkTag,
238 pendingProps: mixed,
237 - key: null | string,
239 + key: ReactKey,
240 mode: TypeOfMode,
241 ): Fiber {
242 const fiber: Fiber = {
@@ -364,6 +366,12 @@ export function createWorkInProgress(current: Fiber, pendingProps: any): Fiber {
366 workInProgress.subtreeFlags = NoFlags;
367 workInProgress.deletions = null;
368
369 + if (enableOptimisticKey) {
370 + // For optimistic keys, the Fibers can have different keys if one is optimistic
371 + // and the other one is filled in.
372 + workInProgress.key = current.key;
373 + }
374 +
375 if (enableProfilerTimer) {
376 // We intentionally reset, rather than copy, actualDuration & actualStartTime.
377 // This prevents time from endlessly accumulating in new commits.
@@ -488,8 +496,15 @@ export function resetWorkInProgress(
496 workInProgress.memoizedState = current.memoizedState;
497 workInProgress.updateQueue = current.updateQueue;
498 // Needed because Blocks store data on type.
499 + // TODO: Blocks don't exist anymore. Do we still need this?
500 workInProgress.type = current.type;
501
502 + if (enableOptimisticKey) {
503 + // For optimistic keys, the Fibers can have different keys if one is optimistic
504 + // and the other one is filled in.
505 + workInProgress.key = current.key;
506 + }
507 +
508 // Clone the dependencies object. This is mutated during the render phase, so
509 // it cannot be shared with the current fiber.
510 const currentDependencies = current.dependencies;
@@ -545,7 +560,7 @@ export function createHostRootFiber(
560 // TODO: Get rid of this helper. Only createFiberFromElement should exist.
561 export function createFiberFromTypeAndProps(
562 type: any, // React$ElementType
548 - key: null | string,
563 + key: ReactKey,
564 pendingProps: any,
565 owner: null | ReactComponentInfo | Fiber,
566 mode: TypeOfMode,
@@ -747,7 +762,7 @@ export function createFiberFromFragment(
762 elements: ReactFragment,
763 mode: TypeOfMode,
764 lanes: Lanes,
750 - key: null | string,
765 + key: ReactKey,
766 ): Fiber {
767 const fiber = createFiber(Fragment, elements, key, mode);
768 fiber.lanes = lanes;
@@ -759,7 +774,7 @@ function createFiberFromScope(
774 pendingProps: any,
775 mode: TypeOfMode,
776 lanes: Lanes,
762 - key: null | string,
777 + key: ReactKey,
778 ) {
779 const fiber = createFiber(ScopeComponent, pendingProps, key, mode);
780 fiber.type = scope;
@@ -772,7 +787,7 @@ function createFiberFromProfiler(
787 pendingProps: any,
788 mode: TypeOfMode,
789 lanes: Lanes,
775 - key: null | string,
790 + key: ReactKey,
791 ): Fiber {
792 if (__DEV__) {
793 if (typeof pendingProps.id !== 'string') {
@@ -801,7 +816,7 @@ export function createFiberFromSuspense(
816 pendingProps: any,
817 mode: TypeOfMode,
818 lanes: Lanes,
804 - key: null | string,
819 + key: ReactKey,
820 ): Fiber {
821 const fiber = createFiber(SuspenseComponent, pendingProps, key, mode);
822 fiber.elementType = REACT_SUSPENSE_TYPE;
@@ -813,7 +828,7 @@ export function createFiberFromSuspenseList(
828 pendingProps: any,
829 mode: TypeOfMode,
830 lanes: Lanes,
816 - key: null | string,
831 + key: ReactKey,
832 ): Fiber {
833 const fiber = createFiber(SuspenseListComponent, pendingProps, key, mode);
834 fiber.elementType = REACT_SUSPENSE_LIST_TYPE;
@@ -825,7 +840,7 @@ export function createFiberFromOffscreen(
840 pendingProps: OffscreenProps,
841 mode: TypeOfMode,
842 lanes: Lanes,
828 - key: null | string,
843 + key: ReactKey,
844 ): Fiber {
845 const fiber = createFiber(OffscreenComponent, pendingProps, key, mode);
846 fiber.lanes = lanes;
@@ -835,7 +850,7 @@ export function createFiberFromActivity(
850 pendingProps: ActivityProps,
851 mode: TypeOfMode,
852 lanes: Lanes,
838 - key: null | string,
853 + key: ReactKey,
854 ): Fiber {
855 const fiber = createFiber(ActivityComponent, pendingProps, key, mode);
856 fiber.elementType = REACT_ACTIVITY_TYPE;
@@ -847,7 +862,7 @@ export function createFiberFromViewTransition(
862 pendingProps: ViewTransitionProps,
863 mode: TypeOfMode,
864 lanes: Lanes,
850 - key: null | string,
865 + key: ReactKey,
866 ): Fiber {
867 if (!enableSuspenseyImages) {
868 // Render a ViewTransition component opts into SuspenseyImages mode even
@@ -871,7 +886,7 @@ export function createFiberFromLegacyHidden(
886 pendingProps: LegacyHiddenProps,
887 mode: TypeOfMode,
888 lanes: Lanes,
874 - key: null | string,
889 + key: ReactKey,
890 ): Fiber {
891 const fiber = createFiber(LegacyHiddenComponent, pendingProps, key, mode);
892 fiber.elementType = REACT_LEGACY_HIDDEN_TYPE;
@@ -883,7 +898,7 @@ export function createFiberFromTracingMarker(
898 pendingProps: any,
899 mode: TypeOfMode,
900 lanes: Lanes,
886 - key: null | string,
901 + key: ReactKey,
902 ): Fiber {
903 const fiber = createFiber(TracingMarkerComponent, pendingProps, key, mode);
904 fiber.elementType = REACT_TRACING_MARKER_TYPE;
packages/react-reconciler/src/ReactInternalTypes.js
+2 -1
@@ -17,6 +17,7 @@ import type {
17 Awaited,
18 ReactComponentInfo,
19 ReactDebugInfo,
20 + ReactKey,
21 } from 'shared/ReactTypes';
22 import type {TransitionTypes} from 'react/src/ReactTransitionType';
23 import type {WorkTag} from './ReactWorkTags';
@@ -100,7 +101,7 @@ export type Fiber = {
101 tag: WorkTag,
102
103 // Unique identifier of this child.
103 - key: null | string,
104 + key: ReactKey,
105
106 // The value of element.type which is used to preserve the identity during
107 // reconciliation of this child.
packages/react-reconciler/src/ReactPortal.js
+18 -6
@@ -7,25 +7,37 @@
7 * @flow
8 */
9
10 -import {REACT_PORTAL_TYPE} from 'shared/ReactSymbols';
10 +import {REACT_PORTAL_TYPE, REACT_OPTIMISTIC_KEY} from 'shared/ReactSymbols';
11 import {checkKeyStringCoercion} from 'shared/CheckStringCoercion';
12
13 -import type {ReactNodeList, ReactPortal} from 'shared/ReactTypes';
13 +import type {
14 + ReactNodeList,
15 + ReactPortal,
16 + ReactOptimisticKey,
17 +} from 'shared/ReactTypes';
18
19 export function createPortal(
20 children: ReactNodeList,
21 containerInfo: any,
22 // TODO: figure out the API for cross-renderer implementation.
23 implementation: any,
20 - key: ?string = null,
24 + key: ?string | ReactOptimisticKey = null,
25 ): ReactPortal {
22 - if (__DEV__) {
23 - checkKeyStringCoercion(key);
26 + let resolvedKey;
27 + if (key == null) {
28 + resolvedKey = null;
29 + } else if (key === REACT_OPTIMISTIC_KEY) {
30 + resolvedKey = REACT_OPTIMISTIC_KEY;
31 + } else {
32 + if (__DEV__) {
33 + checkKeyStringCoercion(key);
34 + }
35 + resolvedKey = '' + key;
36 }
37 return {
38 // This tag allow us to uniquely identify this as a React Portal
39 $$typeof: REACT_PORTAL_TYPE,
28 - key: key == null ? null : '' + key,
40 + key: resolvedKey,
41 children,
42 containerInfo,
43 implementation,
packages/react-reconciler/src/__tests__/ReactAsyncActions-test.js
+79
@@ -1789,4 +1789,83 @@ describe('ReactAsyncActions', () => {
1789 });
1790 assertLog(['reportError: Oops']);
1791 });
1792 +
1793 + // @gate enableOptimisticKey
1794 + it('reconciles against new items when optimisticKey is used', async () => {
1795 + const startTransition = React.startTransition;
1796 +
1797 + function Item({text}) {
1798 + const [initialText] = React.useState(text);
1799 + return <span>{initialText + '-' + text}</span>;
1800 + }
1801 +
1802 + let addOptimisticItem;
1803 + function App({items}) {
1804 + const [optimisticItems, _addOptimisticItem] = useOptimistic(
1805 + items,
1806 + (canonicalItems, optimisticText) =>
1807 + canonicalItems.concat({
1808 + id: React.optimisticKey,
1809 + text: optimisticText,
1810 + }),
1811 + );
1812 + addOptimisticItem = _addOptimisticItem;
1813 + return (
1814 + <div>
1815 + {optimisticItems.map(item => (
1816 + <Item key={item.id} text={item.text} />
1817 + ))}
1818 + </div>
1819 + );
1820 + }
1821 +
1822 + const A = {
1823 + id: 'a',
1824 + text: 'A',
1825 + };
1826 +
1827 + const B = {
1828 + id: 'b',
1829 + text: 'B',
1830 + };
1831 +
1832 + const root = ReactNoop.createRoot();
1833 + await act(() => {
1834 + root.render(<App items={[A]} />);
1835 + });
1836 + expect(root).toMatchRenderedOutput(
1837 + <div>
1838 + <span>A-A</span>
1839 + </div>,
1840 + );
1841 +
1842 + // Start an async action using the non-hook form of startTransition. The
1843 + // action includes an optimistic update.
1844 + await act(() => {
1845 + startTransition(async () => {
1846 + addOptimisticItem('b');
1847 + await getText('Yield before updating');
1848 + startTransition(() => root.render(<App items={[A, B]} />));
1849 + });
1850 + });
1851 + // Because the action hasn't finished yet, the optimistic UI is shown.
1852 + expect(root).toMatchRenderedOutput(
1853 + <div>
1854 + <span>A-A</span>
1855 + <span>b-b</span>
1856 + </div>,
1857 + );
1858 +
1859 + // Finish the async action. The optimistic state is reverted and replaced by
1860 + // the canonical state. The state is transferred to the new row.
1861 + await act(() => {
1862 + resolveText('Yield before updating');
1863 + });
1864 + expect(root).toMatchRenderedOutput(
1865 + <div>
1866 + <span>A-A</span>
1867 + <span>b-B</span>
1868 + </div>,
1869 + );
1870 + });
1871 });
packages/react-server/src/ReactFizzServer.js
+8 -2
@@ -27,6 +27,7 @@ import type {
27 SuspenseProps,
28 SuspenseListProps,
29 SuspenseListRevealOrder,
30 + ReactKey,
31 } from 'shared/ReactTypes';
32 import type {LazyComponent as LazyComponentType} from 'react/src/ReactLazy';
33 import type {
@@ -170,6 +171,7 @@ import {
171 REACT_SCOPE_TYPE,
172 REACT_VIEW_TRANSITION_TYPE,
173 REACT_ACTIVITY_TYPE,
174 + REACT_OPTIMISTIC_KEY,
175 } from 'shared/ReactSymbols';
176 import ReactSharedInternals from 'shared/ReactSharedInternals';
177 import {
@@ -3253,7 +3255,7 @@ function retryNode(request: Request, task: Task): void {
3255 case REACT_ELEMENT_TYPE: {
3256 const element: any = node;
3257 const type = element.type;
3256 - const key = element.key;
3258 + const key: ReactKey = element.key;
3259 const props = element.props;
3260
3261 // TODO: We should get the ref off the props object right before using
@@ -3265,7 +3267,11 @@ function retryNode(request: Request, task: Task): void {
3267
3268 const name = getComponentNameFromType(type);
3269 const keyOrIndex =
3268 - key == null ? (childIndex === -1 ? 0 : childIndex) : key;
3270 + key == null || key === REACT_OPTIMISTIC_KEY
3271 + ? childIndex === -1
3272 + ? 0
3273 + : childIndex
3274 + : key;
3275 const keyPath = [task.keyPath, name, keyOrIndex];
3276 if (task.replay !== null) {
3277 if (debugTask) {
packages/react-server/src/ReactFlightServer.js
+22 -10
@@ -65,6 +65,7 @@ import type {
65 ReactFunctionLocation,
66 ReactErrorInfo,
67 ReactErrorInfoDev,
68 + ReactKey,
69 } from 'shared/ReactTypes';
70 import type {ReactElement} from 'shared/ReactElementType';
71 import type {LazyComponent} from 'react/src/ReactLazy';
@@ -136,6 +137,7 @@ import {
137 REACT_LAZY_TYPE,
138 REACT_MEMO_TYPE,
139 ASYNC_ITERATOR,
140 + REACT_OPTIMISTIC_KEY,
141 } from 'shared/ReactSymbols';
142
143 import {
@@ -534,7 +536,7 @@ type Task = {
536 model: ReactClientValue,
537 ping: () => void,
538 toJSON: (key: string, value: ReactClientValue) => ReactJSONValue,
537 - keyPath: null | string, // parent server component keys
539 + keyPath: ReactKey, // parent server component keys
540 implicitSlot: boolean, // true if the root server component of this sequence had a null key
541 formatContext: FormatContext, // an approximate parent context from host components
542 thenableState: ThenableState | null,
@@ -1643,7 +1645,7 @@ function processServerComponentReturnValue(
1645 function renderFunctionComponent<Props>(
1646 request: Request,
1647 task: Task,
1646 - key: null | string,
1648 + key: ReactKey,
1649 Component: (p: Props, arg: void) => any,
1650 props: Props,
1651 validated: number, // DEV-only
@@ -1814,7 +1816,12 @@ function renderFunctionComponent<Props>(
1816 if (key !== null) {
1817 // Append the key to the path. Technically a null key should really add the child
1818 // index. We don't do that to hold the payload small and implementation simple.
1817 - task.keyPath = prevKeyPath === null ? key : prevKeyPath + ',' + key;
1819 + if (key === REACT_OPTIMISTIC_KEY || prevKeyPath === REACT_OPTIMISTIC_KEY) {
1820 + // The optimistic key is viral. It turns the whole key into optimistic if any part is.
1821 + task.keyPath = REACT_OPTIMISTIC_KEY;
1822 + } else {
1823 + task.keyPath = prevKeyPath === null ? key : prevKeyPath + ',' + key;
1824 + }
1825 } else if (prevKeyPath === null) {
1826 // This sequence of Server Components has no keys. This means that it was rendered
1827 // in a slot that needs to assign an implicit key. Even if children below have
@@ -1830,7 +1837,7 @@ function renderFunctionComponent<Props>(
1837
1838 function warnForMissingKey(
1839 request: Request,
1833 - key: null | string,
1840 + key: ReactKey,
1841 componentDebugInfo: ReactComponentInfo,
1842 debugTask: null | ConsoleTask,
1843 ): void {
@@ -2024,7 +2031,7 @@ function renderClientElement(
2031 request: Request,
2032 task: Task,
2033 type: any,
2027 - key: null | string,
2034 + key: ReactKey,
2035 props: any,
2036 validated: number, // DEV-only
2037 ): ReactJSONValue {
@@ -2034,7 +2041,12 @@ function renderClientElement(
2041 if (key === null) {
2042 key = keyPath;
2043 } else if (keyPath !== null) {
2037 - key = keyPath + ',' + key;
2044 + if (keyPath === REACT_OPTIMISTIC_KEY || key === REACT_OPTIMISTIC_KEY) {
2045 + // Optimistic key is viral and turns the whole key optimistic.
2046 + key = REACT_OPTIMISTIC_KEY;
2047 + } else {
2048 + key = keyPath + ',' + key;
2049 + }
2050 }
2051 let debugOwner = null;
2052 let debugStack = null;
@@ -2161,7 +2173,7 @@ function renderElement(
2173 request: Request,
2174 task: Task,
2175 type: any,
2164 - key: null | string,
2176 + key: ReactKey,
2177 ref: mixed,
2178 props: any,
2179 validated: number, // DEV only
@@ -2667,7 +2679,7 @@ function pingTask(request: Request, task: Task): void {
2679 function createTask(
2680 request: Request,
2681 model: ReactClientValue,
2670 - keyPath: null | string,
2682 + keyPath: ReactKey,
2683 implicitSlot: boolean,
2684 formatContext: FormatContext,
2685 abortSet: Set<Task>,
@@ -3521,7 +3533,7 @@ function renderModelDestructive(
3533 element._debugTask === undefined
3534 ) {
3535 let key = '';
3524 - if (element.key !== null) {
3536 + if (element.key !== null && element.key !== REACT_OPTIMISTIC_KEY) {
3537 key = ' key="' + element.key + '"';
3538 }
3539
@@ -3547,7 +3559,7 @@ function renderModelDestructive(
3559 request,
3560 task,
3561 element.type,
3550 - // $FlowFixMe[incompatible-call] the key of an element is null | string
3562 + // $FlowFixMe[incompatible-call] the key of an element is null | string | ReactOptimisticKey
3563 element.key,
3564 ref,
3565 props,
packages/react/index.experimental.development.js
+1
@@ -29,6 +29,7 @@ export {
29 cache,
30 cacheSignal,
31 startTransition,
32 + optimisticKey,
33 Activity,
34 unstable_getCacheForType,
35 unstable_SuspenseList,
packages/react/index.experimental.js
+1
@@ -29,6 +29,7 @@ export {
29 cache,
30 cacheSignal,
31 startTransition,
32 + optimisticKey,
33 Activity,
34 Activity as unstable_Activity,
35 unstable_getCacheForType,
packages/react/src/ReactChildren.js
+9
@@ -22,7 +22,9 @@ import {
22 REACT_ELEMENT_TYPE,
23 REACT_LAZY_TYPE,
24 REACT_PORTAL_TYPE,
25 + REACT_OPTIMISTIC_KEY,
26 } from 'shared/ReactSymbols';
27 +import {enableOptimisticKey} from 'shared/ReactFeatureFlags';
28 import {checkKeyStringCoercion} from 'shared/CheckStringCoercion';
29
30 import {isValidElement, cloneAndReplaceKey} from './jsx/ReactJSXElement';
@@ -73,6 +75,13 @@ function getElementKey(element: any, index: number): string {
75 // Do some typechecking here since we call this blindly. We want to ensure
76 // that we don't block potential future ES APIs.
77 if (typeof element === 'object' && element !== null && element.key != null) {
78 + if (enableOptimisticKey && element.key === REACT_OPTIMISTIC_KEY) {
79 + // For React.Children purposes this is treated as just null.
80 + if (__DEV__) {
81 + console.error("React.Children helpers don't support optimisticKey.");
82 + }
83 + return index.toString(36);
84 + }
85 // Explicit key
86 if (__DEV__) {
87 checkKeyStringCoercion(element.key);
packages/react/src/ReactClient.js
+3
@@ -19,6 +19,7 @@ import {
19 REACT_SCOPE_TYPE,
20 REACT_TRACING_MARKER_TYPE,
21 REACT_VIEW_TRANSITION_TYPE,
22 + REACT_OPTIMISTIC_KEY,
23 } from 'shared/ReactSymbols';
24
25 import {Component, PureComponent} from './ReactBaseClasses';
@@ -127,6 +128,8 @@ export {
128 addTransitionType as addTransitionType,
129 // enableGestureTransition
130 startGestureTransition as unstable_startGestureTransition,
131 + // enableOptimisticKey
132 + REACT_OPTIMISTIC_KEY as optimisticKey,
133 // DEV-only
134 useId,
135 act,
packages/react/src/ReactServer.experimental.development.js
+3
@@ -18,6 +18,7 @@ import {
18 REACT_SUSPENSE_LIST_TYPE,
19 REACT_VIEW_TRANSITION_TYPE,
20 REACT_ACTIVITY_TYPE,
21 + REACT_OPTIMISTIC_KEY,
22 } from 'shared/ReactSymbols';
23 import {
24 cloneElement,
@@ -82,5 +83,7 @@ export {
83 version,
84 // Experimental
85 REACT_SUSPENSE_LIST_TYPE as unstable_SuspenseList,
86 + // enableOptimisticKey
87 + REACT_OPTIMISTIC_KEY as optimisticKey,
88 captureOwnerStack, // DEV-only
89 };
packages/react/src/ReactServer.experimental.js
+3
@@ -18,6 +18,7 @@ import {
18 REACT_SUSPENSE_LIST_TYPE,
19 REACT_VIEW_TRANSITION_TYPE,
20 REACT_ACTIVITY_TYPE,
21 + REACT_OPTIMISTIC_KEY,
22 } from 'shared/ReactSymbols';
23 import {
24 cloneElement,
@@ -81,4 +82,6 @@ export {
82 version,
83 // Experimental
84 REACT_SUSPENSE_LIST_TYPE as unstable_SuspenseList,
85 + // enableOptimisticKey
86 + REACT_OPTIMISTIC_KEY as optimisticKey,
87 };
packages/react/src/jsx/ReactJSXElement.js
+44 -19
@@ -13,10 +13,11 @@ import {
13 REACT_ELEMENT_TYPE,
14 REACT_FRAGMENT_TYPE,
15 REACT_LAZY_TYPE,
16 + REACT_OPTIMISTIC_KEY,
17 } from 'shared/ReactSymbols';
18 import {checkKeyStringCoercion} from 'shared/CheckStringCoercion';
19 import isArray from 'shared/isArray';
19 -import {ownerStackLimit} from 'shared/ReactFeatureFlags';
20 +import {ownerStackLimit, enableOptimisticKey} from 'shared/ReactFeatureFlags';
21
22 const createTask =
23 // eslint-disable-next-line react-internal/no-production-logging
@@ -297,17 +298,25 @@ export function jsxProd(type, config, maybeKey) {
298 // <div {...props} key="Hi" />, because we aren't currently able to tell if
299 // key is explicitly declared to be undefined or not.
300 if (maybeKey !== undefined) {
300 - if (__DEV__) {
301 - checkKeyStringCoercion(maybeKey);
301 + if (enableOptimisticKey && maybeKey === REACT_OPTIMISTIC_KEY) {
302 + key = REACT_OPTIMISTIC_KEY;
303 + } else {
304 + if (__DEV__) {
305 + checkKeyStringCoercion(maybeKey);
306 + }
307 + key = '' + maybeKey;
308 }
303 - key = '' + maybeKey;
309 }
310
311 if (hasValidKey(config)) {
307 - if (__DEV__) {
308 - checkKeyStringCoercion(config.key);
312 + if (enableOptimisticKey && maybeKey === REACT_OPTIMISTIC_KEY) {
313 + key = REACT_OPTIMISTIC_KEY;
314 + } else {
315 + if (__DEV__) {
316 + checkKeyStringCoercion(config.key);
317 + }
318 + key = '' + config.key;
319 }
310 - key = '' + config.key;
320 }
321
322 let props;
@@ -536,17 +545,25 @@ function jsxDEVImpl(
545 // <div {...props} key="Hi" />, because we aren't currently able to tell if
546 // key is explicitly declared to be undefined or not.
547 if (maybeKey !== undefined) {
539 - if (__DEV__) {
540 - checkKeyStringCoercion(maybeKey);
548 + if (enableOptimisticKey && maybeKey === REACT_OPTIMISTIC_KEY) {
549 + key = REACT_OPTIMISTIC_KEY;
550 + } else {
551 + if (__DEV__) {
552 + checkKeyStringCoercion(maybeKey);
553 + }
554 + key = '' + maybeKey;
555 }
542 - key = '' + maybeKey;
556 }
557
558 if (hasValidKey(config)) {
546 - if (__DEV__) {
547 - checkKeyStringCoercion(config.key);
559 + if (enableOptimisticKey && config.key === REACT_OPTIMISTIC_KEY) {
560 + key = REACT_OPTIMISTIC_KEY;
561 + } else {
562 + if (__DEV__) {
563 + checkKeyStringCoercion(config.key);
564 + }
565 + key = '' + config.key;
566 }
549 - key = '' + config.key;
567 }
568
569 let props;
@@ -637,10 +654,14 @@ export function createElement(type, config, children) {
654 }
655
656 if (hasValidKey(config)) {
640 - if (__DEV__) {
641 - checkKeyStringCoercion(config.key);
657 + if (enableOptimisticKey && config.key === REACT_OPTIMISTIC_KEY) {
658 + key = REACT_OPTIMISTIC_KEY;
659 + } else {
660 + if (__DEV__) {
661 + checkKeyStringCoercion(config.key);
662 + }
663 + key = '' + config.key;
664 }
643 - key = '' + config.key;
665 }
666
667 // Remaining properties are added to a new props object
@@ -769,10 +790,14 @@ export function cloneElement(element, config, children) {
790 owner = __DEV__ ? getOwner() : undefined;
791 }
792 if (hasValidKey(config)) {
772 - if (__DEV__) {
773 - checkKeyStringCoercion(config.key);
793 + if (enableOptimisticKey && config.key === REACT_OPTIMISTIC_KEY) {
794 + key = REACT_OPTIMISTIC_KEY;
795 + } else {
796 + if (__DEV__) {
797 + checkKeyStringCoercion(config.key);
798 + }
799 + key = '' + config.key;
800 }
775 - key = '' + config.key;
801 }
802
803 // Remaining properties override existing props
packages/shared/ReactFeatureFlags.js
+2
@@ -98,6 +98,8 @@ export const enableHydrationChangeEvent = __EXPERIMENTAL__;
98
99 export const enableDefaultTransitionIndicator = __EXPERIMENTAL__;
100
101 +export const enableOptimisticKey = __EXPERIMENTAL__;
102 +
103 /**
104 * Switches Fiber creation to a simple object instead of a constructor.
105 */
packages/shared/ReactSymbols.js
+9
@@ -65,3 +65,12 @@ export function getIteratorFn(maybeIterable: ?any): ?() => ?Iterator<any> {
65 }
66
67 export const ASYNC_ITERATOR = Symbol.asyncIterator;
68 +
69 +export const REACT_OPTIMISTIC_KEY: ReactOptimisticKey = (Symbol.for(
70 + 'react.optimistic_key',
71 +): any);
72 +
73 +// This is actually a symbol but Flow doesn't support comparison of symbols to refine.
74 +// We use a boolean since in our code we often expect string (key) or number (index),
75 +// so by pretending to be a boolean we cover a lot of cases that don't consider this case.
76 +export type ReactOptimisticKey = true;
packages/shared/ReactTypes.js
+10 -4
@@ -7,6 +7,12 @@
7 * @flow
8 */
9
10 +import type {ReactOptimisticKey} from './ReactSymbols';
11 +
12 +export type {ReactOptimisticKey};
13 +
14 +export type ReactKey = null | string | ReactOptimisticKey;
15 +
16 export type ReactNode =
17 | React$Element<any>
18 | ReactPortal
@@ -26,7 +32,7 @@ export type ReactText = string | number;
32 export type ReactProvider<T> = {
33 $$typeof: symbol | number,
34 type: ReactContext<T>,
29 - key: null | string,
35 + key: ReactKey,
36 ref: null,
37 props: {
38 value: T,
@@ -42,7 +48,7 @@ export type ReactConsumerType<T> = {
48 export type ReactConsumer<T> = {
49 $$typeof: symbol | number,
50 type: ReactConsumerType<T>,
45 - key: null | string,
51 + key: ReactKey,
52 ref: null,
53 props: {
54 children: (value: T) => ReactNodeList,
@@ -66,7 +72,7 @@ export type ReactContext<T> = {
72
73 export type ReactPortal = {
74 $$typeof: symbol | number,
69 - key: null | string,
75 + key: ReactKey,
76 containerInfo: any,
77 children: ReactNodeList,
78 // TODO: figure out the API for cross-renderer implementation.
@@ -204,7 +210,7 @@ export type ReactFunctionLocation = [
210 export type ReactComponentInfo = {
211 +name: string,
212 +env?: string,
207 - +key?: null | string,
213 + +key?: ReactKey,
214 +owner?: null | ReactComponentInfo,
215 +stack?: null | ReactStackTrace,
216 +props?: null | {[name: string]: mixed},
packages/shared/forks/ReactFeatureFlags.native-fb.js
+1
@@ -85,6 +85,7 @@ export const enableComponentPerformanceTrack: boolean =
85 export const enablePerformanceIssueReporting: boolean =
86 enableComponentPerformanceTrack;
87 export const enableInternalInstanceMap: boolean = false;
88 +export const enableOptimisticKey: boolean = false;
89
90 // Flow magic to verify the exports of this file match the original version.
91 ((((null: any): ExportsType): FeatureFlagsType): ExportsType);
packages/shared/forks/ReactFeatureFlags.native-oss.js
+2
@@ -78,6 +78,8 @@ export const enableFragmentRefsInstanceHandles: boolean = false;
78
79 export const enableInternalInstanceMap: boolean = false;
80
81 +export const enableOptimisticKey: boolean = false;
82 +
83 // Profiling Only
84 export const enableProfilerTimer: boolean = __PROFILE__;
85 export const enableProfilerCommitHooks: boolean = __PROFILE__;
packages/shared/forks/ReactFeatureFlags.test-renderer.js
+2
@@ -93,5 +93,7 @@ export const enableReactTestRendererWarning: boolean = true;
93
94 export const enableObjectFiber: boolean = false;
95
96 +export const enableOptimisticKey: boolean = false;
97 +
98 // Flow magic to verify the exports of this file match the original version.
99 ((((null: any): ExportsType): FeatureFlagsType): ExportsType);
packages/shared/forks/ReactFeatureFlags.test-renderer.native-fb.js
+1
@@ -69,6 +69,7 @@ export const enableDefaultTransitionIndicator = true;
69 export const enableFragmentRefs = false;
70 export const enableFragmentRefsScrollIntoView = false;
71 export const ownerStackLimit = 1e4;
72 +export const enableOptimisticKey = false;
73
74 // Flow magic to verify the exports of this file match the original version.
75 ((((null: any): ExportsType): FeatureFlagsType): ExportsType);
packages/shared/forks/ReactFeatureFlags.test-renderer.www.js
+2
@@ -87,5 +87,7 @@ export const ownerStackLimit = 1e4;
87
88 export const enableInternalInstanceMap: boolean = false;
89
90 +export const enableOptimisticKey: boolean = false;
91 +
92 // Flow magic to verify the exports of this file match the original version.
93 ((((null: any): ExportsType): FeatureFlagsType): ExportsType);
packages/shared/forks/ReactFeatureFlags.www.js
+2
@@ -114,5 +114,7 @@ export const ownerStackLimit = 1e4;
114
115 export const enableFragmentRefsInstanceHandles: boolean = true;
116
117 +export const enableOptimisticKey: boolean = false;
118 +
119 // Flow magic to verify the exports of this file match the original version.
120 ((((null: any): ExportsType): FeatureFlagsType): ExportsType);