diff --git a/packages/react-devtools-extensions/src/main.js b/packages/react-devtools-extensions/src/main.js index b2cb6758e669..ad75317a348c 100644 --- a/packages/react-devtools-extensions/src/main.js +++ b/packages/react-devtools-extensions/src/main.js @@ -161,7 +161,7 @@ function createPanelIfReactLoaded() { overrideTab, profilerPortalContainer, showTabBar: false, - showWelcomeToTheNewDevToolsDialog: true, + warnIfUnsupportedVersionDetected: true, store, viewElementSourceFunction, }), diff --git a/packages/react-devtools-shared/src/backend/agent.js b/packages/react-devtools-shared/src/backend/agent.js index 3bbec8416ab3..3e73c0f750e6 100644 --- a/packages/react-devtools-shared/src/backend/agent.js +++ b/packages/react-devtools-shared/src/backend/agent.js @@ -474,6 +474,10 @@ export default class Agent extends EventEmitter<{| } }; + onUnsupportedRenderer(rendererID: number) { + this._bridge.send('unsupportedRendererVersion', rendererID); + } + _throttledPersistSelection = throttle((rendererID: number, id: number) => { // This is throttled, so both renderer and selected ID // might not be available by the time we read them. diff --git a/packages/react-devtools-shared/src/backend/index.js b/packages/react-devtools-shared/src/backend/index.js index 2c57627f3111..73e6c9f8f251 100644 --- a/packages/react-devtools-shared/src/backend/index.js +++ b/packages/react-devtools-shared/src/backend/index.js @@ -39,6 +39,10 @@ export function initBackend( }, ), + hook.sub('unsupported-renderer-version', (id: number) => { + agent.onUnsupportedRenderer(id); + }), + hook.sub('operations', agent.onHookOperations), // TODO Add additional subscriptions required for profiling mode @@ -48,23 +52,33 @@ export function initBackend( let rendererInterface = hook.rendererInterfaces.get(id); // Inject any not-yet-injected renderers (if we didn't reload-and-profile) - if (!rendererInterface) { + if (rendererInterface == null) { if (typeof renderer.findFiberByHostInstance === 'function') { + // react-reconciler v16+ rendererInterface = attach(hook, id, renderer, global); - } else { + } else if (renderer.ComponentTree) { + // react-dom v15 rendererInterface = attachLegacy(hook, id, renderer, global); + } else { + // Older react-dom or other unsupported renderer version } - hook.rendererInterfaces.set(id, rendererInterface); + if (rendererInterface != null) { + hook.rendererInterfaces.set(id, rendererInterface); + } } // Notify the DevTools frontend about new renderers. // This includes any that were attached early (via __REACT_DEVTOOLS_ATTACH__). - hook.emit('renderer-attached', { - id, - renderer, - rendererInterface, - }); + if (rendererInterface != null) { + hook.emit('renderer-attached', { + id, + renderer, + rendererInterface, + }); + } else { + hook.emit('unsupported-renderer-version', id); + } }; // Connect renderers that have already injected themselves. diff --git a/packages/react-devtools-shared/src/backend/types.js b/packages/react-devtools-shared/src/backend/types.js index 245817b5c660..bf6a4c728671 100644 --- a/packages/react-devtools-shared/src/backend/types.js +++ b/packages/react-devtools-shared/src/backend/types.js @@ -81,7 +81,10 @@ export type ReactRenderer = { // Enables DevTools to append owners-only component stack to error messages. getCurrentFiber?: () => Fiber | null, - // <= 15 + // Uniquely identifies React DOM v15. + ComponentTree?: any, + + // Present for React DOM v12 (possibly earlier) through v15. Mount?: any, }; diff --git a/packages/react-devtools-shared/src/bridge.js b/packages/react-devtools-shared/src/bridge.js index 4d88747992f6..695121d08c25 100644 --- a/packages/react-devtools-shared/src/bridge.js +++ b/packages/react-devtools-shared/src/bridge.js @@ -83,6 +83,7 @@ type BackendEvents = {| stopInspectingNative: [boolean], syncSelectionFromNativeElementsPanel: [], syncSelectionToNativeElementsPanel: [], + unsupportedRendererVersion: [RendererID], // React Native style editor plug-in. isNativeStyleEditorSupported: [ diff --git a/packages/react-devtools-shared/src/constants.js b/packages/react-devtools-shared/src/constants.js index 00956d28ff8f..d0f0a89807f9 100644 --- a/packages/react-devtools-shared/src/constants.js +++ b/packages/react-devtools-shared/src/constants.js @@ -35,6 +35,9 @@ export const PROFILER_EXPORT_VERSION = 4; export const CHANGE_LOG_URL = 'https://github.com/facebook/react/blob/master/packages/react-devtools/CHANGELOG.md'; +export const UNSUPPORTED_VERSION_URL = + 'https://reactjs.org/blog/2019/08/15/new-react-devtools.html#how-do-i-get-the-old-version-back'; + // HACK // // Extracting during build time avoids a temporarily invalid state for the inline target. diff --git a/packages/react-devtools-shared/src/devtools/store.js b/packages/react-devtools-shared/src/devtools/store.js index 68264ddeb464..efbcf0219688 100644 --- a/packages/react-devtools-shared/src/devtools/store.js +++ b/packages/react-devtools-shared/src/devtools/store.js @@ -73,6 +73,7 @@ export default class Store extends EventEmitter<{| supportsNativeStyleEditor: [], supportsProfiling: [], supportsReloadAndProfile: [], + unsupportedRendererVersionDetected: [], |}> { _bridge: FrontendBridge; @@ -125,6 +126,8 @@ export default class Store extends EventEmitter<{| _supportsProfiling: boolean = false; _supportsReloadAndProfile: boolean = false; + _unsupportedRendererVersionDetected: boolean = false; + // Total number of visible elements (within all roots). // Used for windowing purposes. _weightAcrossRoots: number = 0; @@ -179,6 +182,10 @@ export default class Store extends EventEmitter<{| 'isNativeStyleEditorSupported', this.onBridgeNativeStyleEditorSupported, ); + bridge.addListener( + 'unsupportedRendererVersion', + this.onBridgeUnsupportedRendererVersion, + ); this._profilerStore = new ProfilerStore(bridge, this, isProfiling); } @@ -337,6 +344,10 @@ export default class Store extends EventEmitter<{| return this._supportsReloadAndProfile && this._isBackendStorageAPISupported; } + get unsupportedRendererVersionDetected(): boolean { + return this._unsupportedRendererVersionDetected; + } + containsElement(id: number): boolean { return this._idToElement.get(id) != null; } @@ -1009,4 +1020,10 @@ export default class Store extends EventEmitter<{| this.emit('supportsReloadAndProfile'); }; + + onBridgeUnsupportedRendererVersion = () => { + this._unsupportedRendererVersionDetected = true; + + this.emit('unsupportedRendererVersionDetected'); + }; } diff --git a/packages/react-devtools-shared/src/devtools/views/DevTools.js b/packages/react-devtools-shared/src/devtools/views/DevTools.js index 89b275cffd83..229a750adb7b 100644 --- a/packages/react-devtools-shared/src/devtools/views/DevTools.js +++ b/packages/react-devtools-shared/src/devtools/views/DevTools.js @@ -24,6 +24,7 @@ import ViewElementSourceContext from './Components/ViewElementSourceContext'; import {ProfilerContextController} from './Profiler/ProfilerContext'; import {ModalDialogContextController} from './ModalDialog'; import ReactLogo from './ReactLogo'; +import UnsupportedVersionDialog from './UnsupportedVersionDialog'; import WarnIfLegacyBackendDetected from './WarnIfLegacyBackendDetected'; import styles from './DevTools.css'; @@ -51,6 +52,7 @@ export type Props = {| showTabBar?: boolean, store: Store, warnIfLegacyBackendDetected?: boolean, + warnIfUnsupportedVersionDetected?: boolean, viewElementSourceFunction?: ?ViewElementSource, // This property is used only by the web extension target. @@ -92,6 +94,7 @@ export default function DevTools({ showTabBar = false, store, warnIfLegacyBackendDetected = false, + warnIfUnsupportedVersionDetected = false, viewElementSourceFunction, }: Props) { const [tab, setTab] = useState(defaultTab); @@ -164,6 +167,7 @@ export default function DevTools({ {warnIfLegacyBackendDetected && } + {warnIfUnsupportedVersionDetected && } diff --git a/packages/react-devtools-shared/src/devtools/views/UnsupportedVersionDialog.css b/packages/react-devtools-shared/src/devtools/views/UnsupportedVersionDialog.css new file mode 100644 index 000000000000..5c183f1fe60f --- /dev/null +++ b/packages/react-devtools-shared/src/devtools/views/UnsupportedVersionDialog.css @@ -0,0 +1,20 @@ +.Row { + display: flex; + flex-direction: row; + align-items: center; +} + +.Column { + display: flex; + flex-direction: column; + align-items: center; +} + +.Title { + font-size: var(--font-size-sans-large); + margin-bottom: 0.5rem; +} + +.ReleaseNotesLink { + color: var(--color-button-active); +} \ No newline at end of file diff --git a/packages/react-devtools-shared/src/devtools/views/UnsupportedVersionDialog.js b/packages/react-devtools-shared/src/devtools/views/UnsupportedVersionDialog.js new file mode 100644 index 000000000000..652c4904c224 --- /dev/null +++ b/packages/react-devtools-shared/src/devtools/views/UnsupportedVersionDialog.js @@ -0,0 +1,83 @@ +/** + * 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 React, {Fragment, useContext, useEffect, useState} from 'react'; +import {unstable_batchedUpdates as batchedUpdates} from 'react-dom'; +import {ModalDialogContext} from './ModalDialog'; +import {StoreContext} from './context'; +import {UNSUPPORTED_VERSION_URL} from 'react-devtools-shared/src/constants'; + +import styles from './UnsupportedVersionDialog.css'; + +type DAILOG_STATE = 'dialog-not-shown' | 'show-dialog' | 'dialog-shown'; + +export default function UnsupportedVersionDialog(_: {||}) { + const {dispatch} = useContext(ModalDialogContext); + const store = useContext(StoreContext); + const [state, setState] = useState('dialog-not-shown'); + + useEffect( + () => { + if (state === 'dialog-not-shown') { + const showDialog = () => { + batchedUpdates(() => { + setState('show-dialog'); + dispatch({ + canBeDismissed: true, + type: 'SHOW', + content: , + }); + }); + }; + + if (store.unsupportedRendererVersionDetected) { + showDialog(); + } else { + store.addListener('unsupportedRendererVersionDetected', showDialog); + return () => { + store.removeListener( + 'unsupportedRendererVersionDetected', + showDialog, + ); + }; + } + } + }, + [state, store], + ); + + return null; +} + +function DialogContent(_: {||}) { + return ( + +
+
+
Unsupported React version detected
+

+ This version of React DevTools supports React DOM v15+ and React + Native v61+. +

+

+ In order to use DevTools with an older version of React, you'll need + to{' '} + + install an older version of the extension + . +

+
+
+
+ ); +} diff --git a/packages/react-devtools-shell/src/devtools.js b/packages/react-devtools-shell/src/devtools.js index ce623793a70c..5ecd250161bd 100644 --- a/packages/react-devtools-shell/src/devtools.js +++ b/packages/react-devtools-shell/src/devtools.js @@ -57,6 +57,7 @@ inject('dist/app.js', () => { browserTheme: 'light', showTabBar: true, warnIfLegacyBackendDetected: true, + warnIfUnsupportedVersionDetected: true, }), ); }, diff --git a/packages/react-devtools/CHANGELOG.md b/packages/react-devtools/CHANGELOG.md index 97a3061ab2b2..9eade1cfafa9 100644 --- a/packages/react-devtools/CHANGELOG.md +++ b/packages/react-devtools/CHANGELOG.md @@ -12,6 +12,7 @@ * Fixed bug where Components panel was always empty for certain users. ([linshunghuang](https://github.com/linshunghuang) in [#16900](https://github.com/facebook/react/pull/16900)) * Fixed regression in DevTools editable hooks interface that caused primitive values to be shown as `undefined`. ([bvaughn](https://github.com/bvaughn) in [#16867](https://github.com/facebook/react/pull/16867)) * Fixed bug where DevTools showed stale values in props/state/hooks editing interface. ([bvaughn](https://github.com/bvaughn) in [#16878](https://github.com/facebook/react/pull/16878)) +* Show unsupported version dialog with downgrade instructions. ([bvaughn](https://github.com/bvaughn) in [#16897](https://github.com/facebook/react/pull/16897)) ## 4.1.0 (September 19, 2019)