diff --git a/packages/react-devtools-shared/src/__tests__/__snapshots__/profilingCache-test.js.snap b/packages/react-devtools-shared/src/__tests__/__snapshots__/profilingCache-test.js.snap index f07f405a85f2e..dd4717bb5cb6b 100644 --- a/packages/react-devtools-shared/src/__tests__/__snapshots__/profilingCache-test.js.snap +++ b/packages/react-devtools-shared/src/__tests__/__snapshots__/profilingCache-test.js.snap @@ -719,6 +719,8 @@ Object { 1, 1, 11, + 0, + 1, 1, 1, 4, @@ -1183,6 +1185,8 @@ Object { 1, 1, 11, + 0, + 1, 1, 1, 4, @@ -1658,6 +1662,8 @@ Object { 1, 13, 11, + 0, + 1, 1, 1, 4, @@ -2202,6 +2208,8 @@ Object { 1, 13, 11, + 0, + 1, 1, 1, 4, @@ -2295,6 +2303,8 @@ Object { 1, 1, 11, + 0, + 1, 1, 1, 1, @@ -2943,6 +2953,8 @@ Object { 1, 1, 11, + 0, + 1, 1, 1, 1, @@ -4214,6 +4226,8 @@ Object { 1, 1, 11, + 0, + 1, 1, 1, 1, diff --git a/packages/react-devtools-shared/src/backend/legacy/renderer.js b/packages/react-devtools-shared/src/backend/legacy/renderer.js index b757d008ac294..0a1896ee322c4 100644 --- a/packages/react-devtools-shared/src/backend/legacy/renderer.js +++ b/packages/react-devtools-shared/src/backend/legacy/renderer.js @@ -386,7 +386,9 @@ export function attach( pushOperation(TREE_OPERATION_ADD); pushOperation(id); pushOperation(ElementTypeRoot); - pushOperation(0); // isProfilingSupported? + pushOperation(0); // StrictMode compliant? + pushOperation(0); // Profiling supported? + pushOperation(0); // StrictMode supported? pushOperation(hasOwnerMetadata ? 1 : 0); } else { const type = getElementType(internalInstance); diff --git a/packages/react-devtools-shared/src/backend/renderer.js b/packages/react-devtools-shared/src/backend/renderer.js index 7aebe6e507a0f..631521791a549 100644 --- a/packages/react-devtools-shared/src/backend/renderer.js +++ b/packages/react-devtools-shared/src/backend/renderer.js @@ -24,6 +24,7 @@ import { ElementTypeRoot, ElementTypeSuspense, ElementTypeSuspenseList, + StrictMode, } from 'react-devtools-shared/src/types'; import { deletePathInObject, @@ -52,6 +53,7 @@ import { TREE_OPERATION_REMOVE, TREE_OPERATION_REMOVE_ROOT, TREE_OPERATION_REORDER_CHILDREN, + TREE_OPERATION_SET_SUBTREE_MODE, TREE_OPERATION_UPDATE_ERRORS_OR_WARNINGS, TREE_OPERATION_UPDATE_TREE_BASE_DURATION, } from '../constants'; @@ -155,6 +157,7 @@ export function getInternalReactConstants( ReactPriorityLevels: ReactPriorityLevelsType, ReactTypeOfSideEffect: ReactTypeOfSideEffectType, ReactTypeOfWork: WorkTagMap, + StrictModeBits: number, |} { const ReactTypeOfSideEffect: ReactTypeOfSideEffectType = { DidCapture: 0b10000000, @@ -192,6 +195,18 @@ export function getInternalReactConstants( }; } + let StrictModeBits = 0; + if (gte(version, '18.0.0-alpha')) { + // 18+ + StrictModeBits = 0b011000; + } else if (gte(version, '16.9.0')) { + // 16.9 - 17 + StrictModeBits = 0b1; + } else if (gte(version, '16.3.0')) { + // 16.3 - 16.8 + StrictModeBits = 0b10; + } + let ReactTypeOfWork: WorkTagMap = ((null: any): WorkTagMap); // ********************************************************** @@ -513,6 +528,7 @@ export function getInternalReactConstants( ReactPriorityLevels, ReactTypeOfWork, ReactTypeOfSideEffect, + StrictModeBits, }; } @@ -534,6 +550,7 @@ export function attach( ReactPriorityLevels, ReactTypeOfWork, ReactTypeOfSideEffect, + StrictModeBits, } = getInternalReactConstants(version); const { DidCapture, @@ -1876,7 +1893,9 @@ export function attach( pushOperation(TREE_OPERATION_ADD); pushOperation(id); pushOperation(ElementTypeRoot); + pushOperation((fiber.mode & StrictModeBits) !== 0 ? 1 : 0); pushOperation(isProfilingSupported ? 1 : 0); + pushOperation(StrictModeBits !== 0 ? 1 : 0); pushOperation(hasOwnerMetadata ? 1 : 0); if (isProfiling) { @@ -1913,6 +1932,16 @@ export function attach( pushOperation(ownerID); pushOperation(displayNameStringID); pushOperation(keyStringID); + + // If this subtree has a new mode, let the frontend know. + if ( + (fiber.mode & StrictModeBits) !== 0 && + (((parentFiber: any): Fiber).mode & StrictModeBits) === 0 + ) { + pushOperation(TREE_OPERATION_SET_SUBTREE_MODE); + pushOperation(id); + pushOperation(StrictMode); + } } if (isProfilingSupported) { diff --git a/packages/react-devtools-shared/src/bridge.js b/packages/react-devtools-shared/src/bridge.js index e67f6cee95423..68891e8c18d4b 100644 --- a/packages/react-devtools-shared/src/bridge.js +++ b/packages/react-devtools-shared/src/bridge.js @@ -57,6 +57,12 @@ export const BRIDGE_PROTOCOL: Array = [ { version: 1, minNpmVersion: '4.13.0', + maxNpmVersion: '4.21.0', + }, + // Version 2 adds a StrictMode-enabled and supports-StrictMode bits to add-root operation. + { + version: 2, + minNpmVersion: '4.22.0', maxNpmVersion: null, }, ]; diff --git a/packages/react-devtools-shared/src/constants.js b/packages/react-devtools-shared/src/constants.js index 8898c27787f40..6480222e1e414 100644 --- a/packages/react-devtools-shared/src/constants.js +++ b/packages/react-devtools-shared/src/constants.js @@ -23,6 +23,7 @@ export const TREE_OPERATION_REORDER_CHILDREN = 3; export const TREE_OPERATION_UPDATE_TREE_BASE_DURATION = 4; export const TREE_OPERATION_UPDATE_ERRORS_OR_WARNINGS = 5; export const TREE_OPERATION_REMOVE_ROOT = 6; +export const TREE_OPERATION_SET_SUBTREE_MODE = 7; export const LOCAL_STORAGE_DEFAULT_TAB_KEY = 'React::DevTools::defaultTab'; diff --git a/packages/react-devtools-shared/src/devtools/store.js b/packages/react-devtools-shared/src/devtools/store.js index c8b252072dcb4..b68ea3d26f92e 100644 --- a/packages/react-devtools-shared/src/devtools/store.js +++ b/packages/react-devtools-shared/src/devtools/store.js @@ -14,6 +14,7 @@ import { TREE_OPERATION_REMOVE, TREE_OPERATION_REMOVE_ROOT, TREE_OPERATION_REORDER_CHILDREN, + TREE_OPERATION_SET_SUBTREE_MODE, TREE_OPERATION_UPDATE_ERRORS_OR_WARNINGS, TREE_OPERATION_UPDATE_TREE_BASE_DURATION, } from '../constants'; @@ -33,6 +34,7 @@ import { BRIDGE_PROTOCOL, currentBridgeProtocol, } from 'react-devtools-shared/src/bridge'; +import {StrictMode} from 'react-devtools-shared/src/types'; import type {Element} from './views/Components/types'; import type {ComponentFilter, ElementType} from '../types'; @@ -72,6 +74,7 @@ type Config = {| export type Capabilities = {| hasOwnerMetadata: boolean, supportsProfiling: boolean, + supportsStrictMode: boolean, |}; /** @@ -812,6 +815,20 @@ export default class Store extends EventEmitter<{| } }; + _recursivelyUpdateSubtree( + id: number, + callback: (element: Element) => void, + ): void { + const element = this._idToElement.get(id); + if (element) { + callback(element); + + element.children.forEach(child => + this._recursivelyUpdateSubtree(child, callback), + ); + } + } + onBridgeNativeStyleEditorSupported = ({ isSupported, validAttributes, @@ -883,9 +900,15 @@ export default class Store extends EventEmitter<{| debug('Add', `new root node ${id}`); } + const isStrictModeCompliant = operations[i] > 0; + i++; + const supportsProfiling = operations[i] > 0; i++; + const supportsStrictMode = operations[i] > 0; + i++; + const hasOwnerMetadata = operations[i] > 0; i++; @@ -894,8 +917,14 @@ export default class Store extends EventEmitter<{| this._rootIDToCapabilities.set(id, { hasOwnerMetadata, supportsProfiling, + supportsStrictMode, }); + // Not all roots support StrictMode; + // don't flag a root as non-compliant unless it also supports StrictMode. + const isStrictModeNonCompliant = + !isStrictModeCompliant && supportsStrictMode; + this._idToElement.set(id, { children: [], depth: -1, @@ -903,6 +932,7 @@ export default class Store extends EventEmitter<{| hocDisplayNames: null, id, isCollapsed: false, // Never collapse roots; it would hide the entire tree. + isStrictModeNonCompliant, key: null, ownerID: 0, parentID: 0, @@ -958,9 +988,10 @@ export default class Store extends EventEmitter<{| hocDisplayNames, id, isCollapsed: this._collapseNodesByDefault, + isStrictModeNonCompliant: parentElement.isStrictModeNonCompliant, key, ownerID, - parentID: parentElement.id, + parentID, type, weight: 1, }; @@ -1050,6 +1081,7 @@ export default class Store extends EventEmitter<{| haveErrorsOrWarningsChanged = true; } } + break; } case TREE_OPERATION_REMOVE_ROOT: { @@ -1124,6 +1156,28 @@ export default class Store extends EventEmitter<{| } break; } + case TREE_OPERATION_SET_SUBTREE_MODE: { + const id = operations[i + 1]; + const mode = operations[i + 2]; + + i += 3; + + // If elements have already been mounted in this subtree, update them. + // (In practice, this likely only applies to the root element.) + if (mode === StrictMode) { + this._recursivelyUpdateSubtree(id, element => { + element.isStrictModeNonCompliant = false; + }); + } + + if (__DEBUG__) { + debug( + 'Subtree mode', + `Subtree with root ${id} set to mode ${mode}`, + ); + } + break; + } case TREE_OPERATION_UPDATE_TREE_BASE_DURATION: // Base duration updates are only sent while profiling is in progress. // We can ignore them at this point. diff --git a/packages/react-devtools-shared/src/devtools/views/ButtonIcon.js b/packages/react-devtools-shared/src/devtools/views/ButtonIcon.js index b384018671d92..a5478dbd6dc0d 100644 --- a/packages/react-devtools-shared/src/devtools/views/ButtonIcon.js +++ b/packages/react-devtools-shared/src/devtools/views/ButtonIcon.js @@ -35,6 +35,7 @@ export type IconType = | 'search' | 'settings' | 'error' + | 'strict-mode-non-compliant' | 'suspend' | 'undo' | 'up' @@ -121,6 +122,9 @@ export default function ButtonIcon({className = '', type}: Props) { case 'error': pathData = PATH_ERROR; break; + case 'strict-mode-non-compliant': + pathData = PATH_STRICT_MODE_NON_COMPLIANT; + break; case 'suspend': pathData = PATH_SUSPEND; break; @@ -269,6 +273,10 @@ const PATH_VIEW_DOM = ` 3-1.34 3-3-1.34-3-3-3z `; +const PATH_STRICT_MODE_NON_COMPLIANT = ` + 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 +`; + const PATH_VIEW_SOURCE = ` 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 `; diff --git a/packages/react-devtools-shared/src/devtools/views/Components/Element.css b/packages/react-devtools-shared/src/devtools/views/Components/Element.css index a21b303a3504a..1fa165c32cc08 100644 --- a/packages/react-devtools-shared/src/devtools/views/Components/Element.css +++ b/packages/react-devtools-shared/src/devtools/views/Components/Element.css @@ -43,6 +43,15 @@ --color-expand-collapse-toggle: var(--color-component-name-inverted); } +.SelectedElement.StrictModeNonCompliantElement { + background-color: var(--color-warning-background); + color: var(--color-text-selected); +} +.InactiveSelectedElement.StrictModeNonCompliantElement { + background-color: var(--color-error-background); + color: var(--color-text-selected); +} + .KeyName { color: var(--color-attribute-name); } diff --git a/packages/react-devtools-shared/src/devtools/views/Components/Element.js b/packages/react-devtools-shared/src/devtools/views/Components/Element.js index 73789a9bd62ff..ee5a20b5aaaf0 100644 --- a/packages/react-devtools-shared/src/devtools/views/Components/Element.js +++ b/packages/react-devtools-shared/src/devtools/views/Components/Element.js @@ -113,6 +113,7 @@ export default function Element({data, index, style}: Props) { depth, displayName, hocDisplayNames, + isStrictModeNonCompliant, key, type, } = ((element: any): ElementType); @@ -126,6 +127,10 @@ export default function Element({data, index, style}: Props) { className = styles.HoveredElement; } + if (isStrictModeNonCompliant) { + className += ' ' + styles.StrictModeNonCompliantElement; + } + return (
) : null} + {key && ( diff --git a/packages/react-devtools-shared/src/devtools/views/Components/InspectedElement.css b/packages/react-devtools-shared/src/devtools/views/Components/InspectedElement.css index b6ad3b9983f22..6cd4b86a868b5 100644 --- a/packages/react-devtools-shared/src/devtools/views/Components/InspectedElement.css +++ b/packages/react-devtools-shared/src/devtools/views/Components/InspectedElement.css @@ -66,3 +66,8 @@ font-style: italic; border-left: 1px solid var(--color-border); } + +.StrictModeNonCompliant { + margin-right: 0.125rem; + color: var(--color-warning-background); +} \ No newline at end of file diff --git a/packages/react-devtools-shared/src/devtools/views/Components/InspectedElement.js b/packages/react-devtools-shared/src/devtools/views/Components/InspectedElement.js index 9ebd30a6ec0b3..62a9e4a8d7cb3 100644 --- a/packages/react-devtools-shared/src/devtools/views/Components/InspectedElement.js +++ b/packages/react-devtools-shared/src/devtools/views/Components/InspectedElement.js @@ -235,9 +235,22 @@ export default function InspectedElementWrapper(_: Props) { ); } + let strictModeBadge = null; + if (element.isStrictModeNonCompliant) { + strictModeBadge = ( + + ); + } + return (
+ {strictModeBadge} + {element.key && ( <>
@@ -248,7 +261,13 @@ export default function InspectedElementWrapper(_: Props) { )}
-
+
{element.displayName}
diff --git a/packages/react-devtools-shared/src/devtools/views/Components/types.js b/packages/react-devtools-shared/src/devtools/views/Components/types.js index 372cdd5b9087d..c6789fa1e9ec1 100644 --- a/packages/react-devtools-shared/src/devtools/views/Components/types.js +++ b/packages/react-devtools-shared/src/devtools/views/Components/types.js @@ -42,6 +42,10 @@ export type Element = {| // This property is used to quickly determine the total number of Elements, // and the Element at any given index (for windowing purposes). weight: number, + + // This element is not in a StrictMode compliant subtree. + // Only true for React versions supporting StrictMode. + isStrictModeNonCompliant: boolean, |}; export type SerializedElement = {| diff --git a/packages/react-devtools-shared/src/devtools/views/Profiler/CommitTreeBuilder.js b/packages/react-devtools-shared/src/devtools/views/Profiler/CommitTreeBuilder.js index 1d16aba7fc292..286048a172bf5 100644 --- a/packages/react-devtools-shared/src/devtools/views/Profiler/CommitTreeBuilder.js +++ b/packages/react-devtools-shared/src/devtools/views/Profiler/CommitTreeBuilder.js @@ -13,6 +13,7 @@ import { TREE_OPERATION_REMOVE, TREE_OPERATION_REMOVE_ROOT, TREE_OPERATION_REORDER_CHILDREN, + TREE_OPERATION_SET_SUBTREE_MODE, TREE_OPERATION_UPDATE_TREE_BASE_DURATION, TREE_OPERATION_UPDATE_ERRORS_OR_WARNINGS, } from 'react-devtools-shared/src/constants'; @@ -179,7 +180,7 @@ function updateTree( const operation = operations[i]; switch (operation) { - case TREE_OPERATION_ADD: + case TREE_OPERATION_ADD: { id = ((operations[i + 1]: any): number); const type = ((operations[i + 2]: any): ElementType); @@ -192,7 +193,9 @@ function updateTree( } if (type === ElementTypeRoot) { + i++; // isStrictModeCompliant i++; // supportsProfiling flag + i++; // supportsStrictMode flag i++; // hasOwnerMetadata flag if (__DEBUG__) { @@ -250,6 +253,7 @@ function updateTree( } break; + } case TREE_OPERATION_REMOVE: { const removeLength = ((operations[i + 1]: any): number); i += 2; @@ -307,6 +311,17 @@ function updateTree( break; } + case TREE_OPERATION_SET_SUBTREE_MODE: { + id = operations[i + 1]; + const mode = operations[i + 1]; + + i += 3; + + if (__DEBUG__) { + debug('Subtree mode', `Subtree with root ${id} set to mode ${mode}`); + } + break; + } case TREE_OPERATION_UPDATE_TREE_BASE_DURATION: { id = operations[i + 1]; @@ -323,7 +338,7 @@ function updateTree( i += 3; break; } - case TREE_OPERATION_UPDATE_ERRORS_OR_WARNINGS: + case TREE_OPERATION_UPDATE_ERRORS_OR_WARNINGS: { id = operations[i + 1]; const numErrors = operations[i + 2]; const numWarnings = operations[i + 3]; @@ -337,6 +352,7 @@ function updateTree( ); } break; + } default: throw Error(`Unsupported Bridge operation "${operation}"`); diff --git a/packages/react-devtools-shared/src/types.js b/packages/react-devtools-shared/src/types.js index 5a54979b63b19..859fb9bd8ff72 100644 --- a/packages/react-devtools-shared/src/types.js +++ b/packages/react-devtools-shared/src/types.js @@ -100,3 +100,5 @@ export type StyleXPlugin = {| export type Plugins = {| stylex: StyleXPlugin | null, |}; + +export const StrictMode = 1; diff --git a/packages/react-devtools-shared/src/utils.js b/packages/react-devtools-shared/src/utils.js index 0c50374ae30b6..c022cd4650ed7 100644 --- a/packages/react-devtools-shared/src/utils.js +++ b/packages/react-devtools-shared/src/utils.js @@ -28,6 +28,7 @@ import { TREE_OPERATION_REMOVE, TREE_OPERATION_REMOVE_ROOT, TREE_OPERATION_REORDER_CHILDREN, + TREE_OPERATION_SET_SUBTREE_MODE, TREE_OPERATION_UPDATE_ERRORS_OR_WARNINGS, TREE_OPERATION_UPDATE_TREE_BASE_DURATION, } from './constants'; @@ -211,7 +212,9 @@ export function printOperationsArray(operations: Array) { if (type === ElementTypeRoot) { logs.push(`Add new root node ${id}`); + i++; // isStrictModeCompliant i++; // supportsProfiling + i++; // supportsStrictMode i++; // hasOwnerMetadata } else { const parentID = ((operations[i]: any): number); @@ -249,6 +252,15 @@ export function printOperationsArray(operations: Array) { logs.push(`Remove root ${rootID}`); break; } + case TREE_OPERATION_SET_SUBTREE_MODE: { + const id = operations[i + 1]; + const mode = operations[i + 1]; + + i += 3; + + logs.push(`Mode ${mode} set for subtree with root ${id}`); + break; + } case TREE_OPERATION_REORDER_CHILDREN: { const id = ((operations[i + 1]: any): number); const numChildren = ((operations[i + 2]: any): number); diff --git a/packages/react-devtools-shell/src/app/PartiallyStrictApp/index.js b/packages/react-devtools-shell/src/app/PartiallyStrictApp/index.js new file mode 100644 index 0000000000000..068e11935bce6 --- /dev/null +++ b/packages/react-devtools-shell/src/app/PartiallyStrictApp/index.js @@ -0,0 +1,34 @@ +/** + * Copyright (c) Facebook, Inc. and its affiliates. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + * + * @flow + */ + +import * as React from 'react'; +import {StrictMode} from 'react'; + +export default function PartiallyStrictApp() { + return ( + <> + + + + + + ); +} + +function Child() { + return ; +} + +function StrictChild() { + return ; +} + +function Grandchild() { + return null; +} diff --git a/packages/react-devtools-shell/src/app/index.js b/packages/react-devtools-shell/src/app/index.js index 805cd0a2a707e..6d49d9d871f61 100644 --- a/packages/react-devtools-shell/src/app/index.js +++ b/packages/react-devtools-shell/src/app/index.js @@ -6,6 +6,8 @@ import {createElement} from 'react'; import { // $FlowFixMe Flow does not yet know about createRoot() createRoot, + render, + unmountComponentAtNode, } from 'react-dom'; import DeeplyNestedComponents from './DeeplyNestedComponents'; import Iframe from './Iframe'; @@ -18,6 +20,7 @@ import ReactNativeWeb from './ReactNativeWeb'; import ToDoList from './ToDoList'; import Toggle from './Toggle'; import ErrorBoundaries from './ErrorBoundaries'; +import PartiallyStrictApp from './PartiallyStrictApp'; import SuspenseTree from './SuspenseTree'; import {ignoreErrors, ignoreLogs, ignoreWarnings} from './console'; @@ -34,20 +37,34 @@ ignoreErrors([ ignoreWarnings(['Warning: componentWillReceiveProps has been renamed']); ignoreLogs([]); -const roots = []; +const unmountFunctions = []; -function mountHelper(App) { +function mountHelper(App, useLegacyRender = false) { const container = document.createElement('div'); ((document.body: any): HTMLBodyElement).appendChild(container); - const root = createRoot(container); - root.render(createElement(App)); + if (useLegacyRender) { + render(createElement(App), container); - roots.push(root); + unmountFunctions.push(() => unmountComponentAtNode(container)); + } else { + const root = createRoot(container); + root.render(createElement(App)); + + unmountFunctions.push(() => root.unmount()); + } +} + +function createLegacyWrapper(App) { + return function LegacyApp() { + return createElement(App); + }; } function mountTestApp() { + mountHelper(PartiallyStrictApp); + mountHelper(createLegacyWrapper(PartiallyStrictApp), true); mountHelper(ToDoList); mountHelper(InspectableElements); mountHelper(Hydration); @@ -63,7 +80,7 @@ function mountTestApp() { } function unmountTestApp() { - roots.forEach(root => root.unmount()); + unmountFunctions.forEach(fn => fn()); } mountTestApp(); diff --git a/packages/react-devtools/OVERVIEW.md b/packages/react-devtools/OVERVIEW.md index 70b2c9ba095b0..46612fcbf2e03 100644 --- a/packages/react-devtools/OVERVIEW.md +++ b/packages/react-devtools/OVERVIEW.md @@ -63,7 +63,9 @@ For example, adding a root fiber with an id of 1: 1, // add operation 1, // fiber id 11, // ElementTypeRoot + 1, // this root is StrictMode enabled 1, // this root's renderer supports profiling + 1, // this root's renderer supports StrictMode 1, // this root has owner metadata ] ```