@samitouri / QOS-React-2 / commits / 3b3daf5573

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.

Brian Vaughn committed Dec 10, 2021 at 11:05 UTC 3b3daf5573efe801fa3dc659020625b4023d3a9f
20 files changed +370 -23
packages/react-devtools-shared/src/__tests__/__snapshots__/profilingCache-test.js.snap
+14
@@ -719,6 +719,8 @@ Object {
719 1,
720 1,
721 11,
722 + 0,
723 + 1,
724 1,
725 1,
726 4,
@@ -1183,6 +1185,8 @@ Object {
1185 1,
1186 1,
1187 11,
1188 + 0,
1189 + 1,
1190 1,
1191 1,
1192 4,
@@ -1658,6 +1662,8 @@ Object {
1662 1,
1663 13,
1664 11,
1665 + 0,
1666 + 1,
1667 1,
1668 1,
1669 4,
@@ -2202,6 +2208,8 @@ Object {
2208 1,
2209 13,
2210 11,
2211 + 0,
2212 + 1,
2213 1,
2214 1,
2215 4,
@@ -2295,6 +2303,8 @@ Object {
2303 1,
2304 1,
2305 11,
2306 + 0,
2307 + 1,
2308 1,
2309 1,
2310 1,
@@ -2943,6 +2953,8 @@ Object {
2953 1,
2954 1,
2955 11,
2956 + 0,
2957 + 1,
2958 1,
2959 1,
2960 1,
@@ -4214,6 +4226,8 @@ Object {
4226 1,
4227 1,
4228 11,
4229 + 0,
4230 + 1,
4231 1,
4232 1,
4233 1,
packages/react-devtools-shared/src/__tests__/legacy/storeLegacy-v15-test.js
+11
@@ -509,4 +509,15 @@ describe('Store (legacy)', () => {
509 expect(store).toMatchSnapshot('5: collapse root');
510 });
511 });
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 + });
523 });
packages/react-devtools-shared/src/__tests__/store-test.js
+48
@@ -114,6 +114,54 @@ describe('Store', () => {
114 `);
115 });
116
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 +
165 describe('collapseNodesByDefault:false', () => {
166 beforeEach(() => {
167 store.collapseNodesByDefault = false;
packages/react-devtools-shared/src/backend/legacy/renderer.js
+3 -1
@@ -386,7 +386,9 @@ export function attach(
386 pushOperation(TREE_OPERATION_ADD);
387 pushOperation(id);
388 pushOperation(ElementTypeRoot);
389 - pushOperation(0); // isProfilingSupported?
389 + pushOperation(0); // StrictMode compliant?
390 + pushOperation(0); // Profiling supported?
391 + pushOperation(0); // StrictMode supported?
392 pushOperation(hasOwnerMetadata ? 1 : 0);
393 } else {
394 const type = getElementType(internalInstance);
packages/react-devtools-shared/src/backend/renderer.js
+29
@@ -24,6 +24,7 @@ import {
24 ElementTypeRoot,
25 ElementTypeSuspense,
26 ElementTypeSuspenseList,
27 + StrictMode,
28 } from 'react-devtools-shared/src/types';
29 import {
30 deletePathInObject,
@@ -52,6 +53,7 @@ import {
53 TREE_OPERATION_REMOVE,
54 TREE_OPERATION_REMOVE_ROOT,
55 TREE_OPERATION_REORDER_CHILDREN,
56 + TREE_OPERATION_SET_SUBTREE_MODE,
57 TREE_OPERATION_UPDATE_ERRORS_OR_WARNINGS,
58 TREE_OPERATION_UPDATE_TREE_BASE_DURATION,
59 } from '../constants';
@@ -155,6 +157,7 @@ export function getInternalReactConstants(
157 ReactPriorityLevels: ReactPriorityLevelsType,
158 ReactTypeOfSideEffect: ReactTypeOfSideEffectType,
159 ReactTypeOfWork: WorkTagMap,
160 + StrictModeBits: number,
161 |} {
162 const ReactTypeOfSideEffect: ReactTypeOfSideEffectType = {
163 DidCapture: 0b10000000,
@@ -192,6 +195,18 @@ export function getInternalReactConstants(
195 };
196 }
197
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 +
210 let ReactTypeOfWork: WorkTagMap = ((null: any): WorkTagMap);
211
212 // **********************************************************
@@ -513,6 +528,7 @@ export function getInternalReactConstants(
528 ReactPriorityLevels,
529 ReactTypeOfWork,
530 ReactTypeOfSideEffect,
531 + StrictModeBits,
532 };
533 }
534
@@ -534,6 +550,7 @@ export function attach(
550 ReactPriorityLevels,
551 ReactTypeOfWork,
552 ReactTypeOfSideEffect,
553 + StrictModeBits,
554 } = getInternalReactConstants(version);
555 const {
556 DidCapture,
@@ -1876,7 +1893,9 @@ export function attach(
1893 pushOperation(TREE_OPERATION_ADD);
1894 pushOperation(id);
1895 pushOperation(ElementTypeRoot);
1896 + pushOperation((fiber.mode & StrictModeBits) !== 0 ? 1 : 0);
1897 pushOperation(isProfilingSupported ? 1 : 0);
1898 + pushOperation(StrictModeBits !== 0 ? 1 : 0);
1899 pushOperation(hasOwnerMetadata ? 1 : 0);
1900
1901 if (isProfiling) {
@@ -1913,6 +1932,16 @@ export function attach(
1932 pushOperation(ownerID);
1933 pushOperation(displayNameStringID);
1934 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 + }
1945 }
1946
1947 if (isProfilingSupported) {
packages/react-devtools-shared/src/bridge.js
+6
@@ -57,6 +57,12 @@ export const BRIDGE_PROTOCOL: Array<BridgeProtocol> = [
57 {
58 version: 1,
59 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',
66 maxNpmVersion: null,
67 },
68 ];
packages/react-devtools-shared/src/constants.js
+1
@@ -23,6 +23,7 @@ export const TREE_OPERATION_REORDER_CHILDREN = 3;
23 export const TREE_OPERATION_UPDATE_TREE_BASE_DURATION = 4;
24 export const TREE_OPERATION_UPDATE_ERRORS_OR_WARNINGS = 5;
25 export const TREE_OPERATION_REMOVE_ROOT = 6;
26 +export const TREE_OPERATION_SET_SUBTREE_MODE = 7;
27
28 export const LOCAL_STORAGE_DEFAULT_TAB_KEY = 'React::DevTools::defaultTab';
29
packages/react-devtools-shared/src/devtools/store.js
+55 -1
@@ -14,6 +14,7 @@ import {
14 TREE_OPERATION_REMOVE,
15 TREE_OPERATION_REMOVE_ROOT,
16 TREE_OPERATION_REORDER_CHILDREN,
17 + TREE_OPERATION_SET_SUBTREE_MODE,
18 TREE_OPERATION_UPDATE_ERRORS_OR_WARNINGS,
19 TREE_OPERATION_UPDATE_TREE_BASE_DURATION,
20 } from '../constants';
@@ -33,6 +34,7 @@ import {
34 BRIDGE_PROTOCOL,
35 currentBridgeProtocol,
36 } from 'react-devtools-shared/src/bridge';
37 +import {StrictMode} from 'react-devtools-shared/src/types';
38
39 import type {Element} from './views/Components/types';
40 import type {ComponentFilter, ElementType} from '../types';
@@ -72,6 +74,7 @@ type Config = {|
74 export type Capabilities = {|
75 hasOwnerMetadata: boolean,
76 supportsProfiling: boolean,
77 + supportsStrictMode: boolean,
78 |};
79
80 /**
@@ -812,6 +815,20 @@ export default class Store extends EventEmitter<{|
815 }
816 };
817
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 +
832 onBridgeNativeStyleEditorSupported = ({
833 isSupported,
834 validAttributes,
@@ -883,9 +900,15 @@ export default class Store extends EventEmitter<{|
900 debug('Add', `new root node ${id}`);
901 }
902
903 + const isStrictModeCompliant = operations[i] > 0;
904 + i++;
905 +
906 const supportsProfiling = operations[i] > 0;
907 i++;
908
909 + const supportsStrictMode = operations[i] > 0;
910 + i++;
911 +
912 const hasOwnerMetadata = operations[i] > 0;
913 i++;
914
@@ -894,8 +917,14 @@ export default class Store extends EventEmitter<{|
917 this._rootIDToCapabilities.set(id, {
918 hasOwnerMetadata,
919 supportsProfiling,
920 + supportsStrictMode,
921 });
922
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 +
928 this._idToElement.set(id, {
929 children: [],
930 depth: -1,
@@ -903,6 +932,7 @@ export default class Store extends EventEmitter<{|
932 hocDisplayNames: null,
933 id,
934 isCollapsed: false, // Never collapse roots; it would hide the entire tree.
935 + isStrictModeNonCompliant,
936 key: null,
937 ownerID: 0,
938 parentID: 0,
@@ -958,9 +988,10 @@ export default class Store extends EventEmitter<{|
988 hocDisplayNames,
989 id,
990 isCollapsed: this._collapseNodesByDefault,
991 + isStrictModeNonCompliant: parentElement.isStrictModeNonCompliant,
992 key,
993 ownerID,
963 - parentID: parentElement.id,
994 + parentID,
995 type,
996 weight: 1,
997 };
@@ -1050,6 +1081,7 @@ export default class Store extends EventEmitter<{|
1081 haveErrorsOrWarningsChanged = true;
1082 }
1083 }
1084 +
1085 break;
1086 }
1087 case TREE_OPERATION_REMOVE_ROOT: {
@@ -1124,6 +1156,28 @@ export default class Store extends EventEmitter<{|
1156 }
1157 break;
1158 }
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 + }
1181 case TREE_OPERATION_UPDATE_TREE_BASE_DURATION:
1182 // Base duration updates are only sent while profiling is in progress.
1183 // We can ignore them at this point.
packages/react-devtools-shared/src/devtools/views/Components/Element.css
+8
@@ -76,6 +76,8 @@
76
77 .ErrorIcon,
78 .ErrorIconContrast,
79 +.StrictMode,
80 +.StrictModeContrast,
81 .WarningIcon,
82 .WarningIconContrast {
83 height: 0.75rem !important;
@@ -85,9 +87,15 @@
87 .ErrorIcon {
88 color: var(--color-console-error-icon);
89 }
90 +.StrictMode {
91 + color: var(--color-dimmer);
92 +}
93 .WarningIcon {
94 color: var(--color-console-warning-icon);
95 }
96 .ErrorIconContrast, .WarningIconContrast {
97 color: var(--color-component-name);
98 }
99 +.StrictModeContrast {
100 + color: var(--color-text);
101 +}
packages/react-devtools-shared/src/devtools/views/Components/Element.js
+14
@@ -113,6 +113,7 @@ export default function Element({data, index, style}: Props) {
113 depth,
114 displayName,
115 hocDisplayNames,
116 + isStrictModeNonCompliant,
117 key,
118 type,
119 } = ((element: any): ElementType);
@@ -146,7 +147,9 @@ export default function Element({data, index, style}: Props) {
147 {ownerID === null ? (
148 <ExpandCollapseToggle element={element} store={store} />
149 ) : null}
150 +
151 <DisplayName displayName={displayName} id={((id: any): number)} />
152 +
153 {key && (
154 <Fragment>
155 &nbsp;<span className={styles.KeyName}>key</span>="
@@ -190,6 +193,17 @@ export default function Element({data, index, style}: Props) {
193 }
194 />
195 )}
196 + {isStrictModeNonCompliant && (
197 + <Icon
198 + className={
199 + isSelected && treeFocused
200 + ? styles.StrictModeContrast
201 + : styles.StrictMode
202 + }
203 + title="This component is not running in StrictMode."
204 + type="strict-mode-non-compliant"
205 + />
206 + )}
207 </div>
208 </div>
209 );
packages/react-devtools-shared/src/devtools/views/Components/InspectedElement.css
+5
@@ -66,3 +66,8 @@
66 font-style: italic;
67 border-left: 1px solid var(--color-border);
68 }
69 +
70 +.StrictModeNonCompliant {
71 + margin-right: 0.25rem;
72 + color: var(--color-console-error-icon);
73 +}
\ No newline at end of file
packages/react-devtools-shared/src/devtools/views/Components/InspectedElement.js
+24 -1
@@ -13,6 +13,7 @@ import {TreeDispatcherContext, TreeStateContext} from './TreeContext';
13 import {BridgeContext, StoreContext, OptionsContext} from '../context';
14 import Button from '../Button';
15 import ButtonIcon from '../ButtonIcon';
16 +import Icon from '../Icon';
17 import {ModalDialogContext} from '../ModalDialog';
18 import ViewElementSourceContext from './ViewElementSourceContext';
19 import Toggle from '../Toggle';
@@ -235,9 +236,25 @@ export default function InspectedElementWrapper(_: Props) {
236 );
237 }
238
239 + let strictModeBadge = null;
240 + if (element.isStrictModeNonCompliant) {
241 + strictModeBadge = (
242 + <a
243 + className={styles.StrictModeNonCompliant}
244 + href="https://fb.me/devtools-strict-mode"
245 + rel="noopener noreferrer"
246 + target="_blank"
247 + title="This component is not running in StrictMode. Click to learn more.">
248 + <Icon type="strict-mode-non-compliant" />
249 + </a>
250 + );
251 + }
252 +
253 return (
254 <div className={styles.InspectedElement}>
255 <div className={styles.TitleRow}>
256 + {strictModeBadge}
257 +
258 {element.key && (
259 <>
260 <div className={styles.Key} title={`key "${element.key}"`}>
@@ -248,7 +265,13 @@ export default function InspectedElementWrapper(_: Props) {
265 )}
266
267 <div className={styles.SelectedComponentName}>
251 - <div className={styles.Component} title={element.displayName}>
268 + <div
269 + className={
270 + element.isStrictModeNonCompliant
271 + ? styles.StrictModeNonCompliantComponent
272 + : styles.Component
273 + }
274 + title={element.displayName}>
275 {element.displayName}
276 </div>
277 </div>
packages/react-devtools-shared/src/devtools/views/Components/types.js
+4
@@ -42,6 +42,10 @@ export type Element = {|
42 // This property is used to quickly determine the total number of Elements,
43 // and the Element at any given index (for windowing purposes).
44 weight: number,
45 +
46 + // This element is not in a StrictMode compliant subtree.
47 + // Only true for React versions supporting StrictMode.
48 + isStrictModeNonCompliant: boolean,
49 |};
50
51 export type SerializedElement = {|
packages/react-devtools-shared/src/devtools/views/Icon.js
+12 -1
@@ -25,14 +25,16 @@ export type IconType =
25 | 'search'
26 | 'settings'
27 | 'store-as-global-variable'
28 + | 'strict-mode-non-compliant'
29 | 'warning';
30
31 type Props = {|
32 className?: string,
33 + title?: string,
34 type: IconType,
35 |};
36
35 -export default function Icon({className = '', type}: Props) {
37 +export default function Icon({className = '', title = '', type}: Props) {
38 let pathData = null;
39 switch (type) {
40 case 'arrow':
@@ -77,6 +79,9 @@ export default function Icon({className = '', type}: Props) {
79 case 'store-as-global-variable':
80 pathData = PATH_STORE_AS_GLOBAL_VARIABLE;
81 break;
82 + case 'strict-mode-non-compliant':
83 + pathData = PATH_STRICT_MODE_NON_COMPLIANT;
84 + break;
85 case 'warning':
86 pathData = PATH_WARNING;
87 break;
@@ -92,6 +97,7 @@ export default function Icon({className = '', type}: Props) {
97 width="24"
98 height="24"
99 viewBox="0 0 24 24">
100 + {title && <title>{title}</title>}
101 <path d="M0 0h24v24H0z" fill="none" />
102 <path fill="currentColor" d={pathData} />
103 </svg>
@@ -170,4 +176,9 @@ const PATH_STORE_AS_GLOBAL_VARIABLE = `
176 8h-4v-2h4v2zm0-4h-4v-2h4v2z
177 `;
178
179 +const PATH_STRICT_MODE_NON_COMPLIANT = `
180 + M4.47 21h15.06c1.54 0 2.5-1.67 1.73-3L13.73 4.99c-.77-1.33-2.69-1.33-3.46 0L2.74 18c-.77 1.33.19 3 1.73 3zM12
181 + 14c-.55 0-1-.45-1-1v-2c0-.55.45-1 1-1s1 .45 1 1v2c0 .55-.45 1-1 1zm1 4h-2v-2h2v2z
182 +`;
183 +
184 const PATH_WARNING = `M12 1l-12 22h24l-12-22zm-1 8h2v7h-2v-7zm1 11.25c-.69 0-1.25-.56-1.25-1.25s.56-1.25 1.25-1.25 1.25.56 1.25 1.25-.56 1.25-1.25 1.25z`;
packages/react-devtools-shared/src/devtools/views/Profiler/CommitTreeBuilder.js
+18 -2
@@ -13,6 +13,7 @@ import {
13 TREE_OPERATION_REMOVE,
14 TREE_OPERATION_REMOVE_ROOT,
15 TREE_OPERATION_REORDER_CHILDREN,
16 + TREE_OPERATION_SET_SUBTREE_MODE,
17 TREE_OPERATION_UPDATE_TREE_BASE_DURATION,
18 TREE_OPERATION_UPDATE_ERRORS_OR_WARNINGS,
19 } from 'react-devtools-shared/src/constants';
@@ -179,7 +180,7 @@ function updateTree(
180 const operation = operations[i];
181
182 switch (operation) {
182 - case TREE_OPERATION_ADD:
183 + case TREE_OPERATION_ADD: {
184 id = ((operations[i + 1]: any): number);
185 const type = ((operations[i + 2]: any): ElementType);
186
@@ -192,7 +193,9 @@ function updateTree(
193 }
194
195 if (type === ElementTypeRoot) {
196 + i++; // isStrictModeCompliant
197 i++; // supportsProfiling flag
198 + i++; // supportsStrictMode flag
199 i++; // hasOwnerMetadata flag
200
201 if (__DEBUG__) {
@@ -250,6 +253,7 @@ function updateTree(
253 }
254
255 break;
256 + }
257 case TREE_OPERATION_REMOVE: {
258 const removeLength = ((operations[i + 1]: any): number);
259 i += 2;
@@ -307,6 +311,17 @@ function updateTree(
311
312 break;
313 }
314 + case TREE_OPERATION_SET_SUBTREE_MODE: {
315 + id = operations[i + 1];
316 + const mode = operations[i + 1];
317 +
318 + i += 3;
319 +
320 + if (__DEBUG__) {
321 + debug('Subtree mode', `Subtree with root ${id} set to mode ${mode}`);
322 + }
323 + break;
324 + }
325 case TREE_OPERATION_UPDATE_TREE_BASE_DURATION: {
326 id = operations[i + 1];
327
@@ -323,7 +338,7 @@ function updateTree(
338 i += 3;
339 break;
340 }
326 - case TREE_OPERATION_UPDATE_ERRORS_OR_WARNINGS:
341 + case TREE_OPERATION_UPDATE_ERRORS_OR_WARNINGS: {
342 id = operations[i + 1];
343 const numErrors = operations[i + 2];
344 const numWarnings = operations[i + 3];
@@ -337,6 +352,7 @@ function updateTree(
352 );
353 }
354 break;
355 + }
356
357 default:
358 throw Error(`Unsupported Bridge operation "${operation}"`);
packages/react-devtools-shared/src/types.js
+2
@@ -100,3 +100,5 @@ export type StyleXPlugin = {|
100 export type Plugins = {|
101 stylex: StyleXPlugin | null,
102 |};
103 +
104 +export const StrictMode = 1;
packages/react-devtools-shared/src/utils.js
+12
@@ -28,6 +28,7 @@ import {
28 TREE_OPERATION_REMOVE,
29 TREE_OPERATION_REMOVE_ROOT,
30 TREE_OPERATION_REORDER_CHILDREN,
31 + TREE_OPERATION_SET_SUBTREE_MODE,
32 TREE_OPERATION_UPDATE_ERRORS_OR_WARNINGS,
33 TREE_OPERATION_UPDATE_TREE_BASE_DURATION,
34 } from './constants';
@@ -211,7 +212,9 @@ export function printOperationsArray(operations: Array<number>) {
212 if (type === ElementTypeRoot) {
213 logs.push(`Add new root node ${id}`);
214
215 + i++; // isStrictModeCompliant
216 i++; // supportsProfiling
217 + i++; // supportsStrictMode
218 i++; // hasOwnerMetadata
219 } else {
220 const parentID = ((operations[i]: any): number);
@@ -249,6 +252,15 @@ export function printOperationsArray(operations: Array<number>) {
252 logs.push(`Remove root ${rootID}`);
253 break;
254 }
255 + case TREE_OPERATION_SET_SUBTREE_MODE: {
256 + const id = operations[i + 1];
257 + const mode = operations[i + 1];
258 +
259 + i += 3;
260 +
261 + logs.push(`Mode ${mode} set for subtree with root ${id}`);
262 + break;
263 + }
264 case TREE_OPERATION_REORDER_CHILDREN: {
265 const id = ((operations[i + 1]: any): number);
266 const numChildren = ((operations[i + 2]: any): number);
packages/react-devtools-shell/src/app/PartiallyStrictApp/index.js new
+34
@@ -0,0 +1,34 @@
1 +/**
2 + * Copyright (c) Facebook, Inc. and its 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 +
10 +import * as React from 'react';
11 +import {StrictMode} from 'react';
12 +
13 +export default function PartiallyStrictApp() {
14 + return (
15 + <>
16 + <Child />
17 + <StrictMode>
18 + <StrictChild />
19 + </StrictMode>
20 + </>
21 + );
22 +}
23 +
24 +function Child() {
25 + return <Grandchild />;
26 +}
27 +
28 +function StrictChild() {
29 + return <Grandchild />;
30 +}
31 +
32 +function Grandchild() {
33 + return null;
34 +}
packages/react-devtools-shell/src/app/index.js
+51 -16
@@ -6,6 +6,8 @@ import {createElement} from 'react';
6 import {
7 // $FlowFixMe Flow does not yet know about createRoot()
8 createRoot,
9 + render,
10 + unmountComponentAtNode,
11 } from 'react-dom';
12 import DeeplyNestedComponents from './DeeplyNestedComponents';
13 import Iframe from './Iframe';
@@ -18,6 +20,7 @@ import ReactNativeWeb from './ReactNativeWeb';
20 import ToDoList from './ToDoList';
21 import Toggle from './Toggle';
22 import ErrorBoundaries from './ErrorBoundaries';
23 +import PartiallyStrictApp from './PartiallyStrictApp';
24 import SuspenseTree from './SuspenseTree';
25 import {ignoreErrors, ignoreLogs, ignoreWarnings} from './console';
26
@@ -34,36 +37,68 @@ ignoreErrors([
37 ignoreWarnings(['Warning: componentWillReceiveProps has been renamed']);
38 ignoreLogs([]);
39
37 -const roots = [];
40 +const unmountFunctions = [];
41
39 -function mountHelper(App) {
42 +function createContainer() {
43 const container = document.createElement('div');
44
45 ((document.body: any): HTMLBodyElement).appendChild(container);
46
47 + return container;
48 +}
49 +
50 +function mountApp(App) {
51 + const container = createContainer();
52 +
53 const root = createRoot(container);
54 root.render(createElement(App));
55
47 - roots.push(root);
56 + unmountFunctions.push(() => root.unmount());
57 +}
58 +
59 +function mountStrictApp(App) {
60 + function StrictRoot() {
61 + return createElement(App);
62 + }
63 +
64 + const container = createContainer();
65 +
66 + const root = createRoot(container, {unstable_strictMode: true});
67 + root.render(createElement(StrictRoot));
68 +
69 + unmountFunctions.push(() => root.unmount());
70 +}
71 +
72 +function mountLegacyApp(App) {
73 + function LegacyRender() {
74 + return createElement(App);
75 + }
76 +
77 + const container = createContainer();
78 +
79 + render(createElement(LegacyRender), container);
80 +
81 + unmountFunctions.push(() => unmountComponentAtNode(container));
82 }
83
84 function mountTestApp() {
51 - mountHelper(ToDoList);
52 - mountHelper(InspectableElements);
53 - mountHelper(Hydration);
54 - mountHelper(ElementTypes);
55 - mountHelper(EditableProps);
56 - mountHelper(InlineWarnings);
57 - mountHelper(ReactNativeWeb);
58 - mountHelper(Toggle);
59 - mountHelper(ErrorBoundaries);
60 - mountHelper(SuspenseTree);
61 - mountHelper(DeeplyNestedComponents);
62 - mountHelper(Iframe);
85 + mountStrictApp(ToDoList);
86 + mountApp(InspectableElements);
87 + mountApp(Hydration);
88 + mountApp(ElementTypes);
89 + mountApp(EditableProps);
90 + mountApp(InlineWarnings);
91 + mountApp(ReactNativeWeb);
92 + mountApp(Toggle);
93 + mountApp(ErrorBoundaries);
94 + mountApp(SuspenseTree);
95 + mountApp(DeeplyNestedComponents);
96 + mountApp(Iframe);
97 + mountLegacyApp(PartiallyStrictApp);
98 }
99
100 function unmountTestApp() {
66 - roots.forEach(root => root.unmount());
101 + unmountFunctions.forEach(fn => fn());
102 }
103
104 mountTestApp();
packages/react-devtools/OVERVIEW.md
+19 -1
@@ -54,7 +54,9 @@ Adding a root to the tree requires sending 5 numbers:
54 1. add operation constant (`1`)
55 1. fiber id
56 1. element type constant (`11 === ElementTypeRoot`)
57 -1. profiling supported flag
57 +1. root has `StrictMode` enabled
58 +1. supports profiling flag
59 +1. supports `StrictMode` flag
60 1. owner metadata flag
61
62 For example, adding a root fiber with an id of 1:
@@ -63,7 +65,9 @@ For example, adding a root fiber with an id of 1:
65 1, // add operation
66 1, // fiber id
67 11, // ElementTypeRoot
68 + 1, // this root is StrictMode enabled
69 1, // this root's renderer supports profiling
70 + 1, // this root's renderer supports StrictMode
71 1, // this root has owner metadata
72 ]
73 ```
@@ -176,6 +180,20 @@ Special case of unmounting an entire root (include its descendants). This specia
180
181 This operation has no additional payload because renderer and root ids are already sent at the beginning of every operations payload.
182
183 +#### Setting the mode for a subtree
184 +
185 +This message specifies that a subtree operates under a specific mode (e.g. `StrictMode`).
186 +
187 +```js
188 +[
189 + 7, // set subtree mode
190 + 1, // subtree root fiber id
191 + 0b01 // mode bitmask
192 +]
193 +```
194 +
195 +Modes are constant meaning that the modes a subtree mounts with will never change.
196 +
197 ## Reconstructing the tree
198
199 The frontend stores its information about the tree in a map of id to objects with the following keys: