@@ -368,7 +372,7 @@ export default function Tree(props: Props) {
diff --git a/packages/react-devtools-shared/src/devtools/views/ErrorBoundary.js b/packages/react-devtools-shared/src/devtools/views/ErrorBoundary.js
index bd932cff395f9..7b777115d3b5e 100644
--- a/packages/react-devtools-shared/src/devtools/views/ErrorBoundary.js
+++ b/packages/react-devtools-shared/src/devtools/views/ErrorBoundary.js
@@ -38,14 +38,25 @@ const InitialState: State = {
export default class ErrorBoundary extends Component
{
state: State = InitialState;
- componentDidCatch(error: any, {componentStack}: any) {
+ static getDerivedStateFromError(error: any) {
const errorMessage =
- typeof error === 'object' && error.hasOwnProperty('message')
+ typeof error === 'object' &&
+ error !== null &&
+ error.hasOwnProperty('message')
? error.message
: error;
+ return {
+ errorMessage,
+ hasError: true,
+ };
+ }
+
+ componentDidCatch(error: any, {componentStack}: any) {
const callStack =
- typeof error === 'object' && error.hasOwnProperty('stack')
+ typeof error === 'object' &&
+ error !== null &&
+ error.hasOwnProperty('stack')
? error.stack
.split('\n')
.slice(1)
@@ -55,8 +66,6 @@ export default class ErrorBoundary extends Component {
this.setState({
callStack,
componentStack,
- errorMessage,
- hasError: true,
});
}
diff --git a/packages/react-devtools-shared/src/inspectedElementCache.js b/packages/react-devtools-shared/src/inspectedElementCache.js
new file mode 100644
index 0000000000000..6169207df0dbe
--- /dev/null
+++ b/packages/react-devtools-shared/src/inspectedElementCache.js
@@ -0,0 +1,220 @@
+/**
+ * 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 {
+ unstable_getCacheForType,
+ unstable_startTransition as startTransition,
+} from 'react';
+import Store from './devtools/store';
+import {
+ convertInspectedElementBackendToFrontend,
+ inspectElement as inspectElementAPI,
+} from './backendAPI';
+
+import type {FrontendBridge} from 'react-devtools-shared/src/bridge';
+import type {Wakeable} from 'shared/ReactTypes';
+import type {
+ InspectedElement as InspectedElementBackend,
+ InspectedElementPayload,
+} from 'react-devtools-shared/src/backend/types';
+import type {
+ Element,
+ InspectedElement as InspectedElementFrontend,
+} from 'react-devtools-shared/src/devtools/views/Components/types';
+
+const Pending = 0;
+const Resolved = 1;
+const Rejected = 2;
+
+type PendingRecord = {|
+ status: 0,
+ value: Wakeable,
+|};
+
+type ResolvedRecord = {|
+ status: 1,
+ value: T,
+|};
+
+type RejectedRecord = {|
+ status: 2,
+ value: string,
+|};
+
+type Record = PendingRecord | ResolvedRecord | RejectedRecord;
+
+function readRecord(record: Record): ResolvedRecord {
+ if (record.status === Resolved) {
+ // This is just a type refinement.
+ return record;
+ } else {
+ throw record.value;
+ }
+}
+
+type InspectedElementMap = WeakMap>;
+type CacheSeedKey = () => InspectedElementMap;
+
+function createMap(): InspectedElementMap {
+ return new WeakMap();
+}
+
+function getRecordMap(): WeakMap> {
+ return unstable_getCacheForType(createMap);
+}
+
+function createCacheSeed(
+ element: Element,
+ inspectedElement: InspectedElementFrontend,
+): [CacheSeedKey, InspectedElementMap] {
+ const newRecord: Record = {
+ status: Resolved,
+ value: inspectedElement,
+ };
+ const map = createMap();
+ map.set(element, newRecord);
+ return [createMap, map];
+}
+
+/**
+ * Fetches element props and state from the backend for inspection.
+ * This method should be called during render; it will suspend if data has not yet been fetched.
+ */
+export function inspectElement(
+ element: Element,
+ inspectedPaths: Object,
+ forceUpdate: boolean,
+ store: Store,
+ bridge: FrontendBridge,
+): InspectedElementFrontend | null {
+ const map = getRecordMap();
+ let record = map.get(element);
+ if (!record) {
+ const callbacks = new Set();
+ const wakeable: Wakeable = {
+ then(callback) {
+ callbacks.add(callback);
+ },
+ };
+ const wake = () => {
+ // This assumes they won't throw.
+ callbacks.forEach(callback => callback());
+ callbacks.clear();
+ };
+ const newRecord: Record = (record = {
+ status: Pending,
+ value: wakeable,
+ });
+
+ const rendererID = store.getRendererIDForElement(element.id);
+ if (rendererID == null) {
+ const rejectedRecord = ((newRecord: any): RejectedRecord);
+ rejectedRecord.status = Rejected;
+ rejectedRecord.value = 'Inspected element not found.';
+ return null;
+ }
+
+ inspectElementAPI({
+ bridge,
+ forceUpdate: true,
+ id: element.id,
+ inspectedPaths,
+ rendererID: ((rendererID: any): number),
+ }).then(
+ (data: InspectedElementPayload) => {
+ if (newRecord.status === Pending) {
+ switch (data.type) {
+ case 'no-change':
+ // This response type should never be received.
+ // We always send forceUpdate:true when we have a cache miss.
+ break;
+
+ case 'not-found':
+ const notFoundRecord = ((newRecord: any): RejectedRecord);
+ notFoundRecord.status = Rejected;
+ notFoundRecord.value = 'Inspected element not found.';
+ wake();
+ break;
+
+ case 'full-data':
+ const resolvedRecord = ((newRecord: any): ResolvedRecord);
+ resolvedRecord.status = Resolved;
+ resolvedRecord.value = convertInspectedElementBackendToFrontend(
+ ((data.value: any): InspectedElementBackend),
+ );
+ wake();
+ break;
+ }
+ }
+ },
+
+ () => {
+ // Timed out without receiving a response.
+ if (newRecord.status === Pending) {
+ const timedOutRecord = ((newRecord: any): RejectedRecord);
+ timedOutRecord.status = Rejected;
+ timedOutRecord.value = 'Inspected element timed out.';
+ wake();
+ }
+ },
+ );
+ map.set(element, record);
+ }
+
+ const response = readRecord(record).value;
+ return response;
+}
+
+type RefreshFunction = (
+ seedKey: CacheSeedKey,
+ cacheMap: InspectedElementMap,
+) => void;
+
+/**
+ * Asks the backend for updated props and state from an expected element.
+ * This method should never be called during render; call it from an effect or event handler.
+ * This method will schedule an update if updated information is returned.
+ */
+export function checkForUpdate({
+ bridge,
+ element,
+ inspectedPaths,
+ refresh,
+ store,
+}: {
+ bridge: FrontendBridge,
+ element: Element,
+ inspectedPaths: Object,
+ refresh: RefreshFunction,
+ store: Store,
+}): void {
+ const {id} = element;
+ const rendererID = store.getRendererIDForElement(id);
+ if (rendererID != null) {
+ inspectElementAPI({
+ bridge,
+ forceUpdate: false,
+ id,
+ inspectedPaths,
+ rendererID,
+ }).then((data: InspectedElementPayload) => {
+ switch (data.type) {
+ case 'full-data':
+ const inspectedElement = convertInspectedElementBackendToFrontend(
+ ((data.value: any): InspectedElementBackend),
+ );
+ startTransition(() => {
+ const [key, value] = createCacheSeed(element, inspectedElement);
+ refresh(key, value);
+ });
+ break;
+ }
+ });
+ }
+}
diff --git a/packages/shared/forks/ReactFeatureFlags.test-renderer.js b/packages/shared/forks/ReactFeatureFlags.test-renderer.js
index 16e488409e0c4..c6ffacb27bab2 100644
--- a/packages/shared/forks/ReactFeatureFlags.test-renderer.js
+++ b/packages/shared/forks/ReactFeatureFlags.test-renderer.js
@@ -23,7 +23,7 @@ export const enableSchedulerTracing = __PROFILE__;
export const enableSuspenseServerRenderer = false;
export const enableSelectiveHydration = false;
export const enableLazyElements = false;
-export const enableCache = false;
+export const enableCache = __EXPERIMENTAL__;
export const disableJavaScriptURLs = false;
export const disableInputAttributeSyncing = false;
export const enableSchedulerDebugging = false;
diff --git a/scripts/jest/config.build-devtools.js b/scripts/jest/config.build-devtools.js
index 0d5a6039e414d..761479ce45c50 100644
--- a/scripts/jest/config.build-devtools.js
+++ b/scripts/jest/config.build-devtools.js
@@ -58,6 +58,9 @@ module.exports = Object.assign({}, baseConfig, {
transformIgnorePatterns: ['/node_modules/', '/build2/'],
testRegex: 'packages/react-devtools-shared/src/__tests__/[^]+.test.js$',
snapshotSerializers: [
+ require.resolve(
+ '../../packages/react-devtools-shared/src/__tests__/dehydratedValueSerializer.js'
+ ),
require.resolve(
'../../packages/react-devtools-shared/src/__tests__/inspectedElementSerializer.js'
),
diff --git a/scripts/jest/preprocessor.js b/scripts/jest/preprocessor.js
index f57005c9407fa..072071be07fbd 100644
--- a/scripts/jest/preprocessor.js
+++ b/scripts/jest/preprocessor.js
@@ -59,6 +59,10 @@ const babelOptions = {
module.exports = {
process: function(src, filePath) {
+ if (filePath.match(/\.css$/)) {
+ // Don't try to parse CSS modules; they aren't needed for tests anyway.
+ return '';
+ }
if (filePath.match(/\.coffee$/)) {
return coffee.compile(src, {bare: true});
}