[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