Skip to content

Commit 3b3daf5

Browse files
author
Brian Vaughn
authored
Advocate for StrictMode usage within Components tree (#22886)
Adds the concept of subtree modes to DevTools to bridge protocol as follows: 1. Add-root messages get two new attributes: one specifying whether the root is running in strict mode and another specifying whether the root (really the root's renderer) supports the concept of strict mode. 2. A new backend message type (TREE_OPERATION_SET_SUBTREE_MODE). This type specifies a subtree root (id) and a mode (bitmask). For now, the only mode this message deals with is strict mode. The DevTools frontend has been updated as well to highlight non-StrictMode compliant components. The changes to the bridge protocol require incrementing the bridge protocol version number, which will also require updating the version of react-devtools-core backend that is shipped with React Native.
1 parent 2c1cf56 commit 3b3daf5

File tree

20 files changed

+370
-23
lines changed

20 files changed

+370
-23
lines changed

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

+14
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/__tests__/legacy/storeLegacy-v15-test.js

+11
Original file line numberDiff line numberDiff line change
@@ -509,4 +509,15 @@ describe('Store (legacy)', () => {
509509
expect(store).toMatchSnapshot('5: collapse root');
510510
});
511511
});
512+
513+
describe('StrictMode compliance', () => {
514+
it('should mark all elements as strict mode compliant', () => {
515+
const App = () => null;
516+
517+
const container = document.createElement('div');
518+
act(() => ReactDOM.render(<App />, container));
519+
520+
expect(store.getElementAtIndex(0).isStrictModeNonCompliant).toBe(false);
521+
});
522+
});
512523
});

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

+48
Original file line numberDiff line numberDiff line change
@@ -114,6 +114,54 @@ describe('Store', () => {
114114
`);
115115
});
116116

117+
describe('StrictMode compliance', () => {
118+
it('should mark strict root elements as strict', () => {
119+
const App = () => <Component />;
120+
const Component = () => null;
121+
122+
const container = document.createElement('div');
123+
const root = ReactDOM.createRoot(container, {unstable_strictMode: true});
124+
act(() => {
125+
root.render(<App />);
126+
});
127+
128+
expect(store.getElementAtIndex(0).isStrictModeNonCompliant).toBe(false);
129+
expect(store.getElementAtIndex(1).isStrictModeNonCompliant).toBe(false);
130+
});
131+
132+
it('should mark non strict root elements as not strict', () => {
133+
const App = () => <Component />;
134+
const Component = () => null;
135+
136+
const container = document.createElement('div');
137+
const root = ReactDOM.createRoot(container);
138+
act(() => {
139+
root.render(<App />);
140+
});
141+
142+
expect(store.getElementAtIndex(0).isStrictModeNonCompliant).toBe(true);
143+
expect(store.getElementAtIndex(1).isStrictModeNonCompliant).toBe(true);
144+
});
145+
146+
it('should mark StrictMode subtree elements as strict', () => {
147+
const App = () => (
148+
<React.StrictMode>
149+
<Component />
150+
</React.StrictMode>
151+
);
152+
const Component = () => null;
153+
154+
const container = document.createElement('div');
155+
const root = ReactDOM.createRoot(container);
156+
act(() => {
157+
root.render(<App />);
158+
});
159+
160+
expect(store.getElementAtIndex(0).isStrictModeNonCompliant).toBe(true);
161+
expect(store.getElementAtIndex(1).isStrictModeNonCompliant).toBe(false);
162+
});
163+
});
164+
117165
describe('collapseNodesByDefault:false', () => {
118166
beforeEach(() => {
119167
store.collapseNodesByDefault = false;

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

+3-1
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

+29
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

+6
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

+1
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

+55-1
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.

0 commit comments

Comments
 (0)