@samitouri / QOS-React / commits / e49335e961

[DevTools] Display `React.optimisticKey` in key positions (#35760)

Sebastian "Sebbie" Silbermann committed Feb 11, 2026 at 00:35 UTC e49335e961a77a45be32ad6c9c1877cbe2fe91f4
2 files changed +148 -4
packages/react-devtools-shared/src/__tests__/optimisticKeyDevToolsIntegration.js new
+131
@@ -0,0 +1,131 @@
1 +/**
2 + * Copyright (c) Meta Platforms, Inc. and affiliates.
3 + *
4 + * This source code is licensed under the MIT license found in the
5 + * LICENSE file in the root directory of this source tree.
6 + *
7 + * @flow
8 + */
9 +import {getVersionedRenderImplementation} from './utils';
10 +
11 +describe('Store React.optimisticKey', () => {
12 + let act;
13 + let actAsync;
14 + let React;
15 + let TestRenderer;
16 + let bridge;
17 + let store;
18 +
19 + let BridgeContext;
20 + let StoreContext;
21 + let TreeContext;
22 +
23 + let dispatch;
24 + let state;
25 +
26 + beforeAll(() => {
27 + // JSDDOM doesn't implement getClientRects so we're just faking one for testing purposes
28 + Element.prototype.getClientRects = function (this: Element) {
29 + const textContent = this.textContent;
30 + return [
31 + new DOMRect(1, 2, textContent.length, textContent.split('\n').length),
32 + ];
33 + };
34 + });
35 +
36 + beforeEach(() => {
37 + global.IS_REACT_ACT_ENVIRONMENT = true;
38 +
39 + store = global.store;
40 + bridge = global.bridge;
41 +
42 + React = require('react');
43 +
44 + const utils = require('./utils');
45 + act = utils.act;
46 + actAsync = utils.actAsync;
47 + TestRenderer = utils.requireTestRenderer();
48 +
49 + BridgeContext =
50 + require('react-devtools-shared/src/devtools/views/context').BridgeContext;
51 + StoreContext =
52 + require('react-devtools-shared/src/devtools/views/context').StoreContext;
53 + TreeContext = require('react-devtools-shared/src/devtools/views/Components/TreeContext');
54 + });
55 +
56 + const {render} = getVersionedRenderImplementation();
57 +
58 + const Capture = () => {
59 + dispatch = React.useContext(TreeContext.TreeDispatcherContext);
60 + state = React.useContext(TreeContext.TreeStateContext);
61 + return null;
62 + };
63 +
64 + const Contexts = () => {
65 + return (
66 + <BridgeContext.Provider value={bridge}>
67 + <StoreContext.Provider value={store}>
68 + <TreeContext.TreeContextController>
69 + <Capture />
70 + </TreeContext.TreeContextController>
71 + </StoreContext.Provider>
72 + </BridgeContext.Provider>
73 + );
74 + };
75 +
76 + // @reactVersion >= 19.3
77 + it('is included in the tree', async () => {
78 + if (React.optimisticKey === undefined) {
79 + return;
80 + }
81 +
82 + function Component() {
83 + return null;
84 + }
85 +
86 + await actAsync(() => {
87 + render(<Component key={React.optimisticKey} />);
88 + });
89 +
90 + expect(store).toMatchInlineSnapshot(`
91 + [root]
92 + <Component key="React.optimisticKey">
93 + `);
94 + expect(store.getElementAtIndex(0)).toEqual(
95 + expect.objectContaining({key: 'React.optimisticKey'}),
96 + );
97 + });
98 +
99 + // @reactVersion >= 19.3
100 + it('is searchable', async () => {
101 + if (React.optimisticKey === undefined) {
102 + return;
103 + }
104 + await actAsync(() => {
105 + render(<React.Fragment key={React.optimisticKey} />);
106 + });
107 + let renderer;
108 + act(() => (renderer = TestRenderer.create(<Contexts />)));
109 +
110 + expect(state).toMatchInlineSnapshot(`
111 + [root]
112 + <Fragment key="React.optimisticKey">
113 + `);
114 +
115 + act(() => dispatch({type: 'SET_SEARCH_TEXT', payload: 'optimistic'}));
116 + act(() => renderer.update(<Contexts />));
117 +
118 + expect(state).toMatchInlineSnapshot(`
119 + [root]
120 + <Fragment key="React.optimisticKey">
121 + `);
122 +
123 + act(() => dispatch({type: 'SET_SEARCH_TEXT', payload: 'react'}));
124 + act(() => renderer.update(<Contexts />));
125 +
126 + expect(state).toMatchInlineSnapshot(`
127 + [root]
128 + → <Fragment key="React.optimisticKey">
129 + `);
130 + });
131 +});
packages/react-devtools-shared/src/backend/fiber/renderer.js
+17 -4
@@ -2606,7 +2606,12 @@ export function attach(
2606
2607 // This check is a guard to handle a React element that has been modified
2608 // in such a way as to bypass the default stringification of the "key" property.
2609 - const keyString = key === null ? null : String(key);
2609 + const keyString =
2610 + key === null
2611 + ? null
2612 + : key === REACT_OPTIMISTIC_KEY
2613 + ? 'React.optimisticKey'
2614 + : String(key);
2615 const keyStringID = getStringID(keyString);
2616
2617 const nameProp =
@@ -6180,7 +6185,10 @@ export function attach(
6185 return {
6186 displayName: getDisplayNameForFiber(fiber) || 'Anonymous',
6187 id: instance.id,
6183 - key: fiber.key === REACT_OPTIMISTIC_KEY ? null : fiber.key,
6188 + key:
6189 + fiber.key === REACT_OPTIMISTIC_KEY
6190 + ? 'React.optimisticKey'
6191 + : fiber.key,
6192 env: null,
6193 stack:
6194 fiber._debugOwner == null || fiber._debugStack == null
@@ -6196,7 +6204,7 @@ export function attach(
6204 key:
6205 componentInfo.key == null ||
6206 componentInfo.key === REACT_OPTIMISTIC_KEY
6199 - ? null
6207 + ? 'React.optimisticKey'
6208 : componentInfo.key,
6209 env: componentInfo.env == null ? null : componentInfo.env,
6210 stack:
@@ -7123,7 +7131,12 @@ export function attach(
7131 // Does the component have legacy context attached to it.
7132 hasLegacyContext,
7133
7126 - key: key != null && key !== REACT_OPTIMISTIC_KEY ? key : null,
7134 + key:
7135 + key != null
7136 + ? key === REACT_OPTIMISTIC_KEY
7137 + ? 'React.optimisticKey'
7138 + : key
7139 + : null,
7140
7141 type: elementType,
7142