@samitouri / QOS-React / commits / a7b829524b

[DevTools] Show component names while highlighting renders (#31577)

## Summary This PR improves the Trace Updates feature by letting developers see component names directly on the update overlay. Before this change, the overlay only highlighted updated regions, leaving it unclear which components were involved. With this update, you can now match visual updates to their corresponding components, making it much easier to debug rendering performance. ### New Feature: Show component names while highlighting When the new **"Show component names while highlighting"** setting is enabled, the update overlay display the names of affected components above the rectangles, along with the update count. This gives immediate context about what’s rendering and why. The preference is stored in local storage and synced with the backend, so it’s remembered across sessions. ### Improvements to Drawing Logic The drawing logic has been updated to make the overlay sharper and easier to read. Overlay now respect device pixel ratios, so they look great on high-DPI screens. Outlines have also been made crisper, which makes it easier to spot exactly where updates are happening. > [!NOTE] > **Grouping Logic and Limitations** > Updates are grouped by their screen position `(left, top coordinates)` to combine overlapping or nearby regions into a single group. Groups are sorted by the highest update count within each group, making the most frequently updated components stand out. > Overlapping labels may still occur when multiple updates involve components that overlap but are not in the exact same position. This is intentional, as the logic aims to maintain a straightforward mapping between update regions and component names without introducing unnecessary complexity. ### Testing This PR also adds tests for the new `groupAndSortNodes` utility, which handles the logic for grouping and sorting updates. The tests ensure the behavior is reliable across different scenarios. ## Before & After https://github.com/user-attachments/assets/6ea0fe3e-9354-44fa-95f3-9a867554f74c https://github.com/user-attachments/assets/32af4d98-92a5-47dd-a732-f05c2293e41b --------- Co-authored-by: Ruslan Lesiutin <rdlesyutin@gmail.com>

Piotr Tomczewski committed Dec 13, 2024 at 12:53 UTC a7b829524b295bb114b112c7fc2375bbcd4c65e3
10 files changed +505 -40
packages/react-devtools-shared/src/__tests__/traceUpdates-test.js new
+269
@@ -0,0 +1,269 @@
1 +import {groupAndSortNodes} from 'react-devtools-shared/src/backend/views/TraceUpdates/canvas';
2 +
3 +describe('Trace updates group and sort nodes', () => {
4 + test('should group nodes by position without changing order within group', () => {
5 + const nodeToData = new Map([
6 + [
7 + {id: 1},
8 + {
9 + rect: {left: 0, top: 0, width: 100, height: 100},
10 + color: '#80b393',
11 + displayName: 'Node1',
12 + count: 3,
13 + },
14 + ],
15 + [
16 + {id: 2},
17 + {
18 + rect: {left: 0, top: 0, width: 100, height: 100},
19 + color: '#63b19e',
20 + displayName: 'Node2',
21 + count: 2,
22 + },
23 + ],
24 + ]);
25 +
26 + const result = groupAndSortNodes(nodeToData);
27 +
28 + expect(result).toEqual([
29 + [
30 + {
31 + rect: {left: 0, top: 0, width: 100, height: 100},
32 + color: '#80b393',
33 + displayName: 'Node1',
34 + count: 3,
35 + },
36 + {
37 + rect: {left: 0, top: 0, width: 100, height: 100},
38 + color: '#63b19e',
39 + displayName: 'Node2',
40 + count: 2,
41 + },
42 + ],
43 + ]);
44 + });
45 +
46 + test('should sort groups by lowest count in each group', () => {
47 + const nodeToData = new Map([
48 + [
49 + {id: 1},
50 + {
51 + rect: {left: 0, top: 0, width: 100, height: 100},
52 + color: '#97b488',
53 + displayName: 'Group1',
54 + count: 4,
55 + },
56 + ],
57 + [
58 + {id: 2},
59 + {
60 + rect: {left: 100, top: 0, width: 100, height: 100},
61 + color: '#37afa9',
62 + displayName: 'Group2',
63 + count: 1,
64 + },
65 + ],
66 + [
67 + {id: 3},
68 + {
69 + rect: {left: 200, top: 0, width: 100, height: 100},
70 + color: '#63b19e',
71 + displayName: 'Group3',
72 + count: 2,
73 + },
74 + ],
75 + ]);
76 +
77 + const result = groupAndSortNodes(nodeToData);
78 +
79 + expect(result).toEqual([
80 + [
81 + {
82 + rect: {left: 100, top: 0, width: 100, height: 100},
83 + color: '#37afa9',
84 + displayName: 'Group2',
85 + count: 1,
86 + },
87 + ],
88 + [
89 + {
90 + rect: {left: 200, top: 0, width: 100, height: 100},
91 + color: '#63b19e',
92 + displayName: 'Group3',
93 + count: 2,
94 + },
95 + ],
96 + [
97 + {
98 + rect: {left: 0, top: 0, width: 100, height: 100},
99 + color: '#97b488',
100 + displayName: 'Group1',
101 + count: 4,
102 + },
103 + ],
104 + ]);
105 + });
106 +
107 + test('should maintain order within groups while sorting groups by lowest count', () => {
108 + const nodeToData = new Map([
109 + [
110 + {id: 1},
111 + {
112 + rect: {left: 0, top: 0, width: 50, height: 50},
113 + color: '#97b488',
114 + displayName: 'Pos1Node1',
115 + count: 4,
116 + },
117 + ],
118 + [
119 + {id: 2},
120 + {
121 + rect: {left: 0, top: 0, width: 60, height: 60},
122 + color: '#63b19e',
123 + displayName: 'Pos1Node2',
124 + count: 2,
125 + },
126 + ],
127 + [
128 + {id: 3},
129 + {
130 + rect: {left: 100, top: 0, width: 70, height: 70},
131 + color: '#80b393',
132 + displayName: 'Pos2Node1',
133 + count: 3,
134 + },
135 + ],
136 + [
137 + {id: 4},
138 + {
139 + rect: {left: 100, top: 0, width: 80, height: 80},
140 + color: '#37afa9',
141 + displayName: 'Pos2Node2',
142 + count: 1,
143 + },
144 + ],
145 + ]);
146 +
147 + const result = groupAndSortNodes(nodeToData);
148 +
149 + expect(result).toEqual([
150 + [
151 + {
152 + rect: {left: 100, top: 0, width: 70, height: 70},
153 + color: '#80b393',
154 + displayName: 'Pos2Node1',
155 + count: 3,
156 + },
157 + {
158 + rect: {left: 100, top: 0, width: 80, height: 80},
159 + color: '#37afa9',
160 + displayName: 'Pos2Node2',
161 + count: 1,
162 + },
163 + ],
164 + [
165 + {
166 + rect: {left: 0, top: 0, width: 50, height: 50},
167 + color: '#97b488',
168 + displayName: 'Pos1Node1',
169 + count: 4,
170 + },
171 + {
172 + rect: {left: 0, top: 0, width: 60, height: 60},
173 + color: '#63b19e',
174 + displayName: 'Pos1Node2',
175 + count: 2,
176 + },
177 + ],
178 + ]);
179 + });
180 +
181 + test('should handle multiple groups with same minimum count', () => {
182 + const nodeToData = new Map([
183 + [
184 + {id: 1},
185 + {
186 + rect: {left: 0, top: 0, width: 100, height: 100},
187 + color: '#37afa9',
188 + displayName: 'Group1Node1',
189 + count: 1,
190 + },
191 + ],
192 + [
193 + {id: 2},
194 + {
195 + rect: {left: 100, top: 0, width: 100, height: 100},
196 + color: '#37afa9',
197 + displayName: 'Group2Node1',
198 + count: 1,
199 + },
200 + ],
201 + ]);
202 +
203 + const result = groupAndSortNodes(nodeToData);
204 +
205 + expect(result).toEqual([
206 + [
207 + {
208 + rect: {left: 0, top: 0, width: 100, height: 100},
209 + color: '#37afa9',
210 + displayName: 'Group1Node1',
211 + count: 1,
212 + },
213 + ],
214 + [
215 + {
216 + rect: {left: 100, top: 0, width: 100, height: 100},
217 + color: '#37afa9',
218 + displayName: 'Group2Node1',
219 + count: 1,
220 + },
221 + ],
222 + ]);
223 + });
224 +
225 + test('should filter out nodes without rect property', () => {
226 + const nodeToData = new Map([
227 + [
228 + {id: 1},
229 + {
230 + rect: null,
231 + color: '#37afa9',
232 + displayName: 'NoRectNode',
233 + count: 1,
234 + },
235 + ],
236 + [
237 + {id: 2},
238 + {
239 + rect: undefined,
240 + color: '#63b19e',
241 + displayName: 'UndefinedRectNode',
242 + count: 2,
243 + },
244 + ],
245 + [
246 + {id: 3},
247 + {
248 + rect: {left: 0, top: 0, width: 100, height: 100},
249 + color: '#80b393',
250 + displayName: 'ValidNode',
251 + count: 3,
252 + },
253 + ],
254 + ]);
255 +
256 + const result = groupAndSortNodes(nodeToData);
257 +
258 + expect(result).toEqual([
259 + [
260 + {
261 + rect: {left: 0, top: 0, width: 100, height: 100},
262 + color: '#80b393',
263 + displayName: 'ValidNode',
264 + count: 3,
265 + },
266 + ],
267 + ]);
268 + });
269 +});
packages/react-devtools-shared/src/backend/agent.js
+14
@@ -28,6 +28,7 @@ import type {
28 DevToolsHookSettings,
29 } from './types';
30 import type {ComponentFilter} from 'react-devtools-shared/src/frontend/types';
31 +import type {GroupItem} from './views/TraceUpdates/canvas';
32 import {isReactNativeEnvironment} from './utils';
33 import {
34 sessionStorageGetItem,
@@ -142,10 +143,12 @@ export default class Agent extends EventEmitter<{
143 shutdown: [],
144 traceUpdates: [Set<HostInstance>],
145 drawTraceUpdates: [Array<HostInstance>],
146 + drawGroupedTraceUpdatesWithNames: [Array<Array<GroupItem>>],
147 disableTraceUpdates: [],
148 getIfHasUnsupportedRendererVersion: [],
149 updateHookSettings: [$ReadOnly<DevToolsHookSettings>],
150 getHookSettings: [],
151 + showNamesWhenTracing: [boolean],
152 }> {
153 _bridge: BackendBridge;
154 _isProfiling: boolean = false;
@@ -156,6 +159,7 @@ export default class Agent extends EventEmitter<{
159 _onReloadAndProfile:
160 | ((recordChangeDescriptions: boolean, recordTimeline: boolean) => void)
161 | void;
162 + _showNamesWhenTracing: boolean = true;
163
164 constructor(
165 bridge: BackendBridge,
@@ -200,6 +204,7 @@ export default class Agent extends EventEmitter<{
204 bridge.addListener('reloadAndProfile', this.reloadAndProfile);
205 bridge.addListener('renamePath', this.renamePath);
206 bridge.addListener('setTraceUpdatesEnabled', this.setTraceUpdatesEnabled);
207 + bridge.addListener('setShowNamesWhenTracing', this.setShowNamesWhenTracing);
208 bridge.addListener('startProfiling', this.startProfiling);
209 bridge.addListener('stopProfiling', this.stopProfiling);
210 bridge.addListener('storeAsGlobal', this.storeAsGlobal);
@@ -722,6 +727,7 @@ export default class Agent extends EventEmitter<{
727 this._traceUpdatesEnabled = traceUpdatesEnabled;
728
729 setTraceUpdatesEnabled(traceUpdatesEnabled);
730 + this.emit('showNamesWhenTracing', this._showNamesWhenTracing);
731
732 for (const rendererID in this._rendererInterfaces) {
733 const renderer = ((this._rendererInterfaces[
@@ -731,6 +737,14 @@ export default class Agent extends EventEmitter<{
737 }
738 };
739
740 + setShowNamesWhenTracing: (show: boolean) => void = show => {
741 + if (this._showNamesWhenTracing === show) {
742 + return;
743 + }
744 + this._showNamesWhenTracing = show;
745 + this.emit('showNamesWhenTracing', show);
746 + };
747 +
748 syncSelectionFromBuiltinElementsPanel: () => void = () => {
749 const target = window.__REACT_DEVTOOLS_GLOBAL_HOOK__.$0;
750 if (target == null) {
packages/react-devtools-shared/src/backend/views/TraceUpdates/canvas.js
+117 -36
@@ -14,8 +14,6 @@ import type Agent from '../../agent';
14
15 import {isReactNativeEnvironment} from 'react-devtools-shared/src/backend/utils';
16
17 -const OUTLINE_COLOR = '#f0f0f0';
18 -
17 // Note these colors are in sync with DevTools Profiler chart colors.
18 const COLORS = [
19 '#37afa9',
@@ -34,11 +32,14 @@ let canvas: HTMLCanvasElement | null = null;
32
33 function drawNative(nodeToData: Map<HostInstance, Data>, agent: Agent) {
34 const nodesToDraw = [];
37 - iterateNodes(nodeToData, (_, color, node) => {
35 + iterateNodes(nodeToData, ({color, node}) => {
36 nodesToDraw.push({node, color});
37 });
38
39 agent.emit('drawTraceUpdates', nodesToDraw);
40 +
41 + const mergedNodes = groupAndSortNodes(nodeToData);
42 + agent.emit('drawGroupedTraceUpdatesWithNames', mergedNodes);
43 }
44
45 function drawWeb(nodeToData: Map<HostInstance, Data>) {
@@ -46,62 +47,142 @@ function drawWeb(nodeToData: Map<HostInstance, Data>) {
47 initialize();
48 }
49
50 + const dpr = window.devicePixelRatio || 1;
51 const canvasFlow: HTMLCanvasElement = ((canvas: any): HTMLCanvasElement);
50 - canvasFlow.width = window.innerWidth;
51 - canvasFlow.height = window.innerHeight;
52 + canvasFlow.width = window.innerWidth * dpr;
53 + canvasFlow.height = window.innerHeight * dpr;
54 + canvasFlow.style.width = `${window.innerWidth}px`;
55 + canvasFlow.style.height = `${window.innerHeight}px`;
56
57 const context = canvasFlow.getContext('2d');
54 - context.clearRect(0, 0, canvasFlow.width, canvasFlow.height);
55 - iterateNodes(nodeToData, (rect, color) => {
56 - if (rect !== null) {
57 - drawBorder(context, rect, color);
58 - }
58 + context.scale(dpr, dpr);
59 +
60 + context.clearRect(0, 0, canvasFlow.width / dpr, canvasFlow.height / dpr);
61 +
62 + const mergedNodes = groupAndSortNodes(nodeToData);
63 +
64 + mergedNodes.forEach(group => {
65 + drawGroupBorders(context, group);
66 + drawGroupLabel(context, group);
67 + });
68 +}
69 +
70 +type GroupItem = {
71 + rect: Rect,
72 + color: string,
73 + displayName: string | null,
74 + count: number,
75 +};
76 +
77 +export type {GroupItem};
78 +
79 +export function groupAndSortNodes(
80 + nodeToData: Map<HostInstance, Data>,
81 +): Array<Array<GroupItem>> {
82 + const positionGroups: Map<string, Array<GroupItem>> = new Map();
83 +
84 + iterateNodes(nodeToData, ({rect, color, displayName, count}) => {
85 + if (!rect) return;
86 + const key = `${rect.left},${rect.top}`;
87 + if (!positionGroups.has(key)) positionGroups.set(key, []);
88 + positionGroups.get(key)?.push({rect, color, displayName, count});
89 + });
90 +
91 + return Array.from(positionGroups.values()).sort((groupA, groupB) => {
92 + const maxCountA = Math.max(...groupA.map(item => item.count));
93 + const maxCountB = Math.max(...groupB.map(item => item.count));
94 + return maxCountA - maxCountB;
95 + });
96 +}
97 +
98 +function drawGroupBorders(
99 + context: CanvasRenderingContext2D,
100 + group: Array<GroupItem>,
101 +) {
102 + group.forEach(({color, rect}) => {
103 + context.beginPath();
104 + context.strokeStyle = color;
105 + context.rect(rect.left, rect.top, rect.width - 1, rect.height - 1);
106 + context.stroke();
107 });
108 }
109
110 +function drawGroupLabel(
111 + context: CanvasRenderingContext2D,
112 + group: Array<GroupItem>,
113 +) {
114 + const mergedName = group
115 + .map(({displayName, count}) =>
116 + displayName ? `${displayName}${count > 1 ? ` x${count}` : ''}` : '',
117 + )
118 + .filter(Boolean)
119 + .join(', ');
120 +
121 + if (mergedName) {
122 + drawLabel(context, group[0].rect, mergedName, group[0].color);
123 + }
124 +}
125 +
126 export function draw(nodeToData: Map<HostInstance, Data>, agent: Agent): void {
127 return isReactNativeEnvironment()
128 ? drawNative(nodeToData, agent)
129 : drawWeb(nodeToData);
130 }
131
132 +type DataWithColorAndNode = {
133 + ...Data,
134 + color: string,
135 + node: HostInstance,
136 +};
137 +
138 function iterateNodes(
139 nodeToData: Map<HostInstance, Data>,
70 - execute: (rect: Rect | null, color: string, node: HostInstance) => void,
140 + execute: (data: DataWithColorAndNode) => void,
141 ) {
72 - nodeToData.forEach(({count, rect}, node) => {
73 - const colorIndex = Math.min(COLORS.length - 1, count - 1);
142 + nodeToData.forEach((data, node) => {
143 + const colorIndex = Math.min(COLORS.length - 1, data.count - 1);
144 const color = COLORS[colorIndex];
75 - execute(rect, color, node);
145 + execute({
146 + color,
147 + node,
148 + count: data.count,
149 + displayName: data.displayName,
150 + expirationTime: data.expirationTime,
151 + lastMeasuredAt: data.lastMeasuredAt,
152 + rect: data.rect,
153 + });
154 });
155 }
156
79 -function drawBorder(
157 +function drawLabel(
158 context: CanvasRenderingContext2D,
159 rect: Rect,
160 + text: string,
161 color: string,
162 ): void {
84 - const {height, left, top, width} = rect;
85 -
86 - // outline
87 - context.lineWidth = 1;
88 - context.strokeStyle = OUTLINE_COLOR;
89 -
90 - context.strokeRect(left - 1, top - 1, width + 2, height + 2);
91 -
92 - // inset
93 - context.lineWidth = 1;
94 - context.strokeStyle = OUTLINE_COLOR;
95 - context.strokeRect(left + 1, top + 1, width - 1, height - 1);
96 - context.strokeStyle = color;
97 -
98 - context.setLineDash([0]);
99 -
100 - // border
101 - context.lineWidth = 1;
102 - context.strokeRect(left, top, width - 1, height - 1);
103 -
104 - context.setLineDash([0]);
163 + const {left, top} = rect;
164 + context.font = '10px monospace';
165 + context.textBaseline = 'middle';
166 + context.textAlign = 'center';
167 +
168 + const padding = 2;
169 + const textHeight = 14;
170 +
171 + const metrics = context.measureText(text);
172 + const backgroundWidth = metrics.width + padding * 2;
173 + const backgroundHeight = textHeight;
174 + const labelX = left;
175 + const labelY = top - backgroundHeight;
176 +
177 + context.fillStyle = color;
178 + context.fillRect(labelX, labelY, backgroundWidth, backgroundHeight);
179 +
180 + context.fillStyle = '#000000';
181 + context.fillText(
182 + text,
183 + labelX + backgroundWidth / 2,
184 + labelY + backgroundHeight / 2,
185 + );
186 }
187
188 function destroyNative(agent: Agent) {
packages/react-devtools-shared/src/backend/views/TraceUpdates/index.js
+35 -4
@@ -9,7 +9,7 @@
9
10 import Agent from 'react-devtools-shared/src/backend/agent';
11 import {destroy as destroyCanvas, draw} from './canvas';
12 -import {getNestedBoundingClientRect} from '../utils';
12 +import {extractHOCNames, getNestedBoundingClientRect} from '../utils';
13
14 import type {HostInstance} from '../../types';
15 import type {Rect} from '../utils';
@@ -24,6 +24,12 @@ const MAX_DISPLAY_DURATION = 3000;
24 // How long should a rect be considered valid for?
25 const REMEASUREMENT_AFTER_DURATION = 250;
26
27 +// Markers for different types of HOCs
28 +const HOC_MARKERS = new Map([
29 + ['Forget', '✨'],
30 + ['Memo', '🧠'],
31 +]);
32 +
33 // Some environments (e.g. React Native / Hermes) don't support the performance API yet.
34 const getCurrentTime =
35 // $FlowFixMe[method-unbinding]
@@ -36,6 +42,7 @@ export type Data = {
42 expirationTime: number,
43 lastMeasuredAt: number,
44 rect: Rect | null,
45 + displayName: string | null,
46 };
47
48 const nodeToData: Map<HostInstance, Data> = new Map();
@@ -43,11 +50,20 @@ const nodeToData: Map<HostInstance, Data> = new Map();
50 let agent: Agent = ((null: any): Agent);
51 let drawAnimationFrameID: AnimationFrameID | null = null;
52 let isEnabled: boolean = false;
53 +let showNames: boolean = false;
54 let redrawTimeoutID: TimeoutID | null = null;
55
56 export function initialize(injectedAgent: Agent): void {
57 agent = injectedAgent;
58 agent.addListener('traceUpdates', traceUpdates);
59 + agent.addListener('showNamesWhenTracing', (shouldShowNames: boolean) => {
60 + showNames = shouldShowNames;
61 + if (isEnabled) {
62 + if (drawAnimationFrameID === null) {
63 + drawAnimationFrameID = requestAnimationFrame(prepareToDraw);
64 + }
65 + }
66 + });
67 }
68
69 export function toggleEnabled(value: boolean): void {
@@ -71,9 +87,7 @@ export function toggleEnabled(value: boolean): void {
87 }
88
89 function traceUpdates(nodes: Set<HostInstance>): void {
74 - if (!isEnabled) {
75 - return;
76 - }
90 + if (!isEnabled) return;
91
92 nodes.forEach(node => {
93 const data = nodeToData.get(node);
@@ -81,11 +95,27 @@ function traceUpdates(nodes: Set<HostInstance>): void {
95
96 let lastMeasuredAt = data != null ? data.lastMeasuredAt : 0;
97 let rect = data != null ? data.rect : null;
98 +
99 if (rect === null || lastMeasuredAt + REMEASUREMENT_AFTER_DURATION < now) {
100 lastMeasuredAt = now;
101 rect = measureNode(node);
102 }
103
104 + let displayName = showNames
105 + ? agent.getComponentNameForHostInstance(node)
106 + : null;
107 + if (displayName) {
108 + const {baseComponentName, hocNames} = extractHOCNames(displayName);
109 +
110 + const markers = hocNames.map(hoc => HOC_MARKERS.get(hoc) || '').join('');
111 +
112 + const enhancedDisplayName = markers
113 + ? `${markers}${baseComponentName}`
114 + : baseComponentName;
115 +
116 + displayName = enhancedDisplayName;
117 + }
118 +
119 nodeToData.set(node, {
120 count: data != null ? data.count + 1 : 1,
121 expirationTime:
@@ -97,6 +127,7 @@ function traceUpdates(nodes: Set<HostInstance>): void {
127 : now + DISPLAY_DURATION,
128 lastMeasuredAt,
129 rect,
130 + displayName: showNames ? displayName : null,
131 });
132 });
133
packages/react-devtools-shared/src/backend/views/utils.js
+25
@@ -138,3 +138,28 @@ export function getElementDimensions(domElement: HTMLElement): {
138 paddingBottom: parseInt(calculatedStyle.paddingBottom, 10),
139 };
140 }
141 +
142 +export function extractHOCNames(displayName: string): {
143 + baseComponentName: string,
144 + hocNames: string[],
145 +} {
146 + if (!displayName) return {baseComponentName: '', hocNames: []};
147 +
148 + const hocRegex = /([A-Z][a-zA-Z0-9]*?)\((.*)\)/g;
149 + const hocNames: string[] = [];
150 + let baseComponentName = displayName;
151 + let match;
152 +
153 + while ((match = hocRegex.exec(baseComponentName)) != null) {
154 + if (Array.isArray(match)) {
155 + const [, hocName, inner] = match;
156 + hocNames.push(hocName);
157 + baseComponentName = inner;
158 + }
159 + }
160 +
161 + return {
162 + baseComponentName,
163 + hocNames,
164 + };
165 +}
packages/react-devtools-shared/src/bridge.js
+1
@@ -234,6 +234,7 @@ type FrontendEvents = {
234 renamePath: [RenamePath],
235 savedPreferences: [SavedPreferencesParams],
236 setTraceUpdatesEnabled: [boolean],
237 + setShowNamesWhenTracing: [boolean],
238 shutdown: [],
239 startInspectingHost: [],
240 startProfiling: [StartProfilingParams],
packages/react-devtools-shared/src/constants.js
+2
@@ -50,6 +50,8 @@ export const LOCAL_STORAGE_TRACE_UPDATES_ENABLED_KEY =
50 'React::DevTools::traceUpdatesEnabled';
51 export const LOCAL_STORAGE_SUPPORTS_PROFILING_KEY =
52 'React::DevTools::supportsProfiling';
53 +export const LOCAL_STORAGE_SHOW_NAMES_WHEN_TRACING_KEY =
54 + 'React::DevTools::showNamesWhenTracing';
55
56 export const PROFILER_EXPORT_VERSION = 5;
57
packages/react-devtools-shared/src/devtools/views/Settings/GeneralSettings.js
+15
@@ -34,8 +34,10 @@ export default function GeneralSettings(_: {}): React.Node {
34 setDisplayDensity,
35 setTheme,
36 setTraceUpdatesEnabled,
37 + setShowNamesWhenTracing,
38 theme,
39 traceUpdatesEnabled,
40 + showNamesWhenTracing,
41 } = useContext(SettingsContext);
42
43 const {backendVersion, supportsTraceUpdates} = useContext(StoreContext);
@@ -83,6 +85,19 @@ export default function GeneralSettings(_: {}): React.Node {
85 />{' '}
86 Highlight updates when components render.
87 </label>
88 + <div className={styles.Setting}>
89 + <label>
90 + <input
91 + type="checkbox"
92 + checked={showNamesWhenTracing}
93 + disabled={!traceUpdatesEnabled}
94 + onChange={({currentTarget}) =>
95 + setShowNamesWhenTracing(currentTarget.checked)
96 + }
97 + />{' '}
98 + Show component names while highlighting.
99 + </label>
100 + </div>
101 </div>
102 )}
103
packages/react-devtools-shared/src/devtools/views/Settings/SettingsContext.js
+16
@@ -21,6 +21,7 @@ import {
21 LOCAL_STORAGE_BROWSER_THEME,
22 LOCAL_STORAGE_PARSE_HOOK_NAMES_KEY,
23 LOCAL_STORAGE_TRACE_UPDATES_ENABLED_KEY,
24 + LOCAL_STORAGE_SHOW_NAMES_WHEN_TRACING_KEY,
25 } from 'react-devtools-shared/src/constants';
26 import {
27 COMFORTABLE_LINE_HEIGHT,
@@ -53,6 +54,9 @@ type Context = {
54
55 traceUpdatesEnabled: boolean,
56 setTraceUpdatesEnabled: (value: boolean) => void,
57 +
58 + showNamesWhenTracing: boolean,
59 + setShowNamesWhenTracing: (showNames: boolean) => void,
60 };
61
62 const SettingsContext: ReactContext<Context> = createContext<Context>(
@@ -111,6 +115,11 @@ function SettingsContextController({
115 LOCAL_STORAGE_TRACE_UPDATES_ENABLED_KEY,
116 false,
117 );
118 + const [showNamesWhenTracing, setShowNamesWhenTracing] =
119 + useLocalStorageWithLog<boolean>(
120 + LOCAL_STORAGE_SHOW_NAMES_WHEN_TRACING_KEY,
121 + true,
122 + );
123
124 const documentElements = useMemo<DocumentElements>(() => {
125 const array: Array<HTMLElement> = [
@@ -164,6 +173,10 @@ function SettingsContextController({
173 bridge.send('setTraceUpdatesEnabled', traceUpdatesEnabled);
174 }, [bridge, traceUpdatesEnabled]);
175
176 + useEffect(() => {
177 + bridge.send('setShowNamesWhenTracing', showNamesWhenTracing);
178 + }, [bridge, showNamesWhenTracing]);
179 +
180 const value: Context = useMemo(
181 () => ({
182 displayDensity,
@@ -179,6 +192,8 @@ function SettingsContextController({
192 theme,
193 browserTheme,
194 traceUpdatesEnabled,
195 + showNamesWhenTracing,
196 + setShowNamesWhenTracing,
197 }),
198 [
199 displayDensity,
@@ -190,6 +205,7 @@ function SettingsContextController({
205 theme,
206 browserTheme,
207 traceUpdatesEnabled,
208 + showNamesWhenTracing,
209 ],
210 );
211
packages/react-devtools-shared/src/devtools/views/Settings/SettingsShared.css
+11
@@ -154,3 +154,14 @@
154 padding: 0;
155 margin: 0;
156 }
157 +
158 +.Setting .Setting {
159 + margin-left: 1rem;
160 + margin-top: 0.5rem;
161 + margin-bottom: 0.5rem;
162 +}
163 +
164 +.Setting label:has(input:disabled) {
165 + opacity: 0.5;
166 + cursor: default;
167 +}