Skip to content

Commit a9ee0aa

Browse files
author
Brian Vaughn
committed
Advocate for StrictMode usage within Components tree
Adds concept of subtree modes to DevTools frontend and visually styles non-StrictMode compliant components.
1 parent ad60746 commit a9ee0aa

File tree

18 files changed

+251
-11
lines changed

18 files changed

+251
-11
lines changed

packages/react-devtools-shared/src/__tests__/__snapshots__/profilingCache-test.js.snap

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -719,6 +719,8 @@ Object {
719719
1,
720720
1,
721721
11,
722+
0,
723+
1,
722724
1,
723725
1,
724726
4,
@@ -1183,6 +1185,8 @@ Object {
11831185
1,
11841186
1,
11851187
11,
1188+
0,
1189+
1,
11861190
1,
11871191
1,
11881192
4,
@@ -1658,6 +1662,8 @@ Object {
16581662
1,
16591663
13,
16601664
11,
1665+
0,
1666+
1,
16611667
1,
16621668
1,
16631669
4,
@@ -2202,6 +2208,8 @@ Object {
22022208
1,
22032209
13,
22042210
11,
2211+
0,
2212+
1,
22052213
1,
22062214
1,
22072215
4,
@@ -2295,6 +2303,8 @@ Object {
22952303
1,
22962304
1,
22972305
11,
2306+
0,
2307+
1,
22982308
1,
22992309
1,
23002310
1,
@@ -2943,6 +2953,8 @@ Object {
29432953
1,
29442954
1,
29452955
11,
2956+
0,
2957+
1,
29462958
1,
29472959
1,
29482960
1,
@@ -4214,6 +4226,8 @@ Object {
42144226
1,
42154227
1,
42164228
11,
4229+
0,
4230+
1,
42174231
1,
42184232
1,
42194233
1,

packages/react-devtools-shared/src/backend/legacy/renderer.js

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -386,7 +386,9 @@ export function attach(
386386
pushOperation(TREE_OPERATION_ADD);
387387
pushOperation(id);
388388
pushOperation(ElementTypeRoot);
389-
pushOperation(0); // isProfilingSupported?
389+
pushOperation(0); // StrictMode compliant?
390+
pushOperation(0); // Profiling supported?
391+
pushOperation(0); // StrictMode supported?
390392
pushOperation(hasOwnerMetadata ? 1 : 0);
391393
} else {
392394
const type = getElementType(internalInstance);

packages/react-devtools-shared/src/backend/renderer.js

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,7 @@ import {
2424
ElementTypeRoot,
2525
ElementTypeSuspense,
2626
ElementTypeSuspenseList,
27+
StrictMode,
2728
} from 'react-devtools-shared/src/types';
2829
import {
2930
deletePathInObject,
@@ -52,6 +53,7 @@ import {
5253
TREE_OPERATION_REMOVE,
5354
TREE_OPERATION_REMOVE_ROOT,
5455
TREE_OPERATION_REORDER_CHILDREN,
56+
TREE_OPERATION_SET_SUBTREE_MODE,
5557
TREE_OPERATION_UPDATE_ERRORS_OR_WARNINGS,
5658
TREE_OPERATION_UPDATE_TREE_BASE_DURATION,
5759
} from '../constants';
@@ -155,6 +157,7 @@ export function getInternalReactConstants(
155157
ReactPriorityLevels: ReactPriorityLevelsType,
156158
ReactTypeOfSideEffect: ReactTypeOfSideEffectType,
157159
ReactTypeOfWork: WorkTagMap,
160+
StrictModeBits: number,
158161
|} {
159162
const ReactTypeOfSideEffect: ReactTypeOfSideEffectType = {
160163
DidCapture: 0b10000000,
@@ -192,6 +195,18 @@ export function getInternalReactConstants(
192195
};
193196
}
194197

198+
let StrictModeBits = 0;
199+
if (gte(version, '18.0.0-alpha')) {
200+
// 18+
201+
StrictModeBits = 0b011000;
202+
} else if (gte(version, '16.9.0')) {
203+
// 16.9 - 17
204+
StrictModeBits = 0b1;
205+
} else if (gte(version, '16.3.0')) {
206+
// 16.3 - 16.8
207+
StrictModeBits = 0b10;
208+
}
209+
195210
let ReactTypeOfWork: WorkTagMap = ((null: any): WorkTagMap);
196211

197212
// **********************************************************
@@ -513,6 +528,7 @@ export function getInternalReactConstants(
513528
ReactPriorityLevels,
514529
ReactTypeOfWork,
515530
ReactTypeOfSideEffect,
531+
StrictModeBits,
516532
};
517533
}
518534

@@ -534,6 +550,7 @@ export function attach(
534550
ReactPriorityLevels,
535551
ReactTypeOfWork,
536552
ReactTypeOfSideEffect,
553+
StrictModeBits,
537554
} = getInternalReactConstants(version);
538555
const {
539556
DidCapture,
@@ -1876,7 +1893,9 @@ export function attach(
18761893
pushOperation(TREE_OPERATION_ADD);
18771894
pushOperation(id);
18781895
pushOperation(ElementTypeRoot);
1896+
pushOperation((fiber.mode & StrictModeBits) !== 0 ? 1 : 0);
18791897
pushOperation(isProfilingSupported ? 1 : 0);
1898+
pushOperation(StrictModeBits !== 0 ? 1 : 0);
18801899
pushOperation(hasOwnerMetadata ? 1 : 0);
18811900

18821901
if (isProfiling) {
@@ -1913,6 +1932,16 @@ export function attach(
19131932
pushOperation(ownerID);
19141933
pushOperation(displayNameStringID);
19151934
pushOperation(keyStringID);
1935+
1936+
// If this subtree has a new mode, let the frontend know.
1937+
if (
1938+
(fiber.mode & StrictModeBits) !== 0 &&
1939+
(((parentFiber: any): Fiber).mode & StrictModeBits) === 0
1940+
) {
1941+
pushOperation(TREE_OPERATION_SET_SUBTREE_MODE);
1942+
pushOperation(id);
1943+
pushOperation(StrictMode);
1944+
}
19161945
}
19171946

19181947
if (isProfilingSupported) {

packages/react-devtools-shared/src/bridge.js

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -57,6 +57,12 @@ export const BRIDGE_PROTOCOL: Array<BridgeProtocol> = [
5757
{
5858
version: 1,
5959
minNpmVersion: '4.13.0',
60+
maxNpmVersion: '4.21.0',
61+
},
62+
// Version 2 adds a StrictMode-enabled and supports-StrictMode bits to add-root operation.
63+
{
64+
version: 2,
65+
minNpmVersion: '4.22.0',
6066
maxNpmVersion: null,
6167
},
6268
];

packages/react-devtools-shared/src/constants.js

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,7 @@ export const TREE_OPERATION_REORDER_CHILDREN = 3;
2323
export const TREE_OPERATION_UPDATE_TREE_BASE_DURATION = 4;
2424
export const TREE_OPERATION_UPDATE_ERRORS_OR_WARNINGS = 5;
2525
export const TREE_OPERATION_REMOVE_ROOT = 6;
26+
export const TREE_OPERATION_SET_SUBTREE_MODE = 7;
2627

2728
export const LOCAL_STORAGE_DEFAULT_TAB_KEY = 'React::DevTools::defaultTab';
2829

packages/react-devtools-shared/src/devtools/store.js

Lines changed: 55 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@ import {
1414
TREE_OPERATION_REMOVE,
1515
TREE_OPERATION_REMOVE_ROOT,
1616
TREE_OPERATION_REORDER_CHILDREN,
17+
TREE_OPERATION_SET_SUBTREE_MODE,
1718
TREE_OPERATION_UPDATE_ERRORS_OR_WARNINGS,
1819
TREE_OPERATION_UPDATE_TREE_BASE_DURATION,
1920
} from '../constants';
@@ -33,6 +34,7 @@ import {
3334
BRIDGE_PROTOCOL,
3435
currentBridgeProtocol,
3536
} from 'react-devtools-shared/src/bridge';
37+
import {StrictMode} from 'react-devtools-shared/src/types';
3638

3739
import type {Element} from './views/Components/types';
3840
import type {ComponentFilter, ElementType} from '../types';
@@ -72,6 +74,7 @@ type Config = {|
7274
export type Capabilities = {|
7375
hasOwnerMetadata: boolean,
7476
supportsProfiling: boolean,
77+
supportsStrictMode: boolean,
7578
|};
7679

7780
/**
@@ -812,6 +815,20 @@ export default class Store extends EventEmitter<{|
812815
}
813816
};
814817

818+
_recursivelyUpdateSubtree(
819+
id: number,
820+
callback: (element: Element) => void,
821+
): void {
822+
const element = this._idToElement.get(id);
823+
if (element) {
824+
callback(element);
825+
826+
element.children.forEach(child =>
827+
this._recursivelyUpdateSubtree(child, callback),
828+
);
829+
}
830+
}
831+
815832
onBridgeNativeStyleEditorSupported = ({
816833
isSupported,
817834
validAttributes,
@@ -883,9 +900,15 @@ export default class Store extends EventEmitter<{|
883900
debug('Add', `new root node ${id}`);
884901
}
885902

903+
const isStrictModeCompliant = operations[i] > 0;
904+
i++;
905+
886906
const supportsProfiling = operations[i] > 0;
887907
i++;
888908

909+
const supportsStrictMode = operations[i] > 0;
910+
i++;
911+
889912
const hasOwnerMetadata = operations[i] > 0;
890913
i++;
891914

@@ -894,15 +917,22 @@ export default class Store extends EventEmitter<{|
894917
this._rootIDToCapabilities.set(id, {
895918
hasOwnerMetadata,
896919
supportsProfiling,
920+
supportsStrictMode,
897921
});
898922

923+
// Not all roots support StrictMode;
924+
// don't flag a root as non-compliant unless it also supports StrictMode.
925+
const isStrictModeNonCompliant =
926+
!isStrictModeCompliant && supportsStrictMode;
927+
899928
this._idToElement.set(id, {
900929
children: [],
901930
depth: -1,
902931
displayName: null,
903932
hocDisplayNames: null,
904933
id,
905934
isCollapsed: false, // Never collapse roots; it would hide the entire tree.
935+
isStrictModeNonCompliant,
906936
key: null,
907937
ownerID: 0,
908938
parentID: 0,
@@ -958,9 +988,10 @@ export default class Store extends EventEmitter<{|
958988
hocDisplayNames,
959989
id,
960990
isCollapsed: this._collapseNodesByDefault,
991+
isStrictModeNonCompliant: parentElement.isStrictModeNonCompliant,
961992
key,
962993
ownerID,
963-
parentID: parentElement.id,
994+
parentID,
964995
type,
965996
weight: 1,
966997
};
@@ -1050,6 +1081,7 @@ export default class Store extends EventEmitter<{|
10501081
haveErrorsOrWarningsChanged = true;
10511082
}
10521083
}
1084+
10531085
break;
10541086
}
10551087
case TREE_OPERATION_REMOVE_ROOT: {
@@ -1124,6 +1156,28 @@ export default class Store extends EventEmitter<{|
11241156
}
11251157
break;
11261158
}
1159+
case TREE_OPERATION_SET_SUBTREE_MODE: {
1160+
const id = operations[i + 1];
1161+
const mode = operations[i + 2];
1162+
1163+
i += 3;
1164+
1165+
// If elements have already been mounted in this subtree, update them.
1166+
// (In practice, this likely only applies to the root element.)
1167+
if (mode === StrictMode) {
1168+
this._recursivelyUpdateSubtree(id, element => {
1169+
element.isStrictModeNonCompliant = false;
1170+
});
1171+
}
1172+
1173+
if (__DEBUG__) {
1174+
debug(
1175+
'Subtree mode',
1176+
`Subtree with root ${id} set to mode ${mode}`,
1177+
);
1178+
}
1179+
break;
1180+
}
11271181
case TREE_OPERATION_UPDATE_TREE_BASE_DURATION:
11281182
// Base duration updates are only sent while profiling is in progress.
11291183
// We can ignore them at this point.

packages/react-devtools-shared/src/devtools/views/ButtonIcon.js

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,7 @@ export type IconType =
3535
| 'search'
3636
| 'settings'
3737
| 'error'
38+
| 'strict-mode-non-compliant'
3839
| 'suspend'
3940
| 'undo'
4041
| 'up'
@@ -121,6 +122,9 @@ export default function ButtonIcon({className = '', type}: Props) {
121122
case 'error':
122123
pathData = PATH_ERROR;
123124
break;
125+
case 'strict-mode-non-compliant':
126+
pathData = PATH_STRICT_MODE_NON_COMPLIANT;
127+
break;
124128
case 'suspend':
125129
pathData = PATH_SUSPEND;
126130
break;
@@ -269,6 +273,10 @@ const PATH_VIEW_DOM = `
269273
3-1.34 3-3-1.34-3-3-3z
270274
`;
271275

276+
const PATH_STRICT_MODE_NON_COMPLIANT = `
277+
M12 2C6.48 2 2 6.48 2 12s4.48 10 10 10 10-4.48 10-10S17.52 2 12 2zm1 15h-2v-2h2v2zm0-4h-2V7h2v6z
278+
`;
279+
272280
const PATH_VIEW_SOURCE = `
273281
M9.4 16.6L4.8 12l4.6-4.6L8 6l-6 6 6 6 1.4-1.4zm5.2 0l4.6-4.6-4.6-4.6L16 6l6 6-6 6-1.4-1.4z
274282
`;

packages/react-devtools-shared/src/devtools/views/Components/Element.css

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -43,6 +43,15 @@
4343
--color-expand-collapse-toggle: var(--color-component-name-inverted);
4444
}
4545

46+
.SelectedElement.StrictModeNonCompliantElement {
47+
background-color: var(--color-warning-background);
48+
color: var(--color-text-selected);
49+
}
50+
.InactiveSelectedElement.StrictModeNonCompliantElement {
51+
background-color: var(--color-error-background);
52+
color: var(--color-text-selected);
53+
}
54+
4655
.KeyName {
4756
color: var(--color-attribute-name);
4857
}

packages/react-devtools-shared/src/devtools/views/Components/Element.js

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -113,6 +113,7 @@ export default function Element({data, index, style}: Props) {
113113
depth,
114114
displayName,
115115
hocDisplayNames,
116+
isStrictModeNonCompliant,
116117
key,
117118
type,
118119
} = ((element: any): ElementType);
@@ -126,6 +127,10 @@ export default function Element({data, index, style}: Props) {
126127
className = styles.HoveredElement;
127128
}
128129

130+
if (isStrictModeNonCompliant) {
131+
className += ' ' + styles.StrictModeNonCompliantElement;
132+
}
133+
129134
return (
130135
<div
131136
className={className}
@@ -146,6 +151,7 @@ export default function Element({data, index, style}: Props) {
146151
{ownerID === null ? (
147152
<ExpandCollapseToggle element={element} store={store} />
148153
) : null}
154+
149155
<DisplayName displayName={displayName} id={((id: any): number)} />
150156
{key && (
151157
<Fragment>

packages/react-devtools-shared/src/devtools/views/Components/InspectedElement.css

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -66,3 +66,8 @@
6666
font-style: italic;
6767
border-left: 1px solid var(--color-border);
6868
}
69+
70+
.StrictModeNonCompliant {
71+
margin-right: 0.125rem;
72+
color: var(--color-warning-background);
73+
}

0 commit comments

Comments
 (0)