diff --git a/package.json b/package.json
index deb7b24882da6..bca998508f1dc 100644
--- a/package.json
+++ b/package.json
@@ -107,7 +107,7 @@
   "scripts": {
     "build": "node ./scripts/rollup/build.js",
     "build-combined": "node ./scripts/rollup/build-all-release-channels.js",
-    "build-for-devtools": "cross-env RELEASE_CHANNEL=experimental yarn build react/index,react-dom,react-is,react-debug-tools,scheduler,react-test-renderer,react-refresh",
+    "build-for-devtools": "cross-env RELEASE_CHANNEL=experimental yarn build-combined react/index,react-dom,react-is,react-debug-tools,scheduler,react-test-renderer,react-refresh",
     "build-for-devtools-dev": "yarn build-for-devtools --type=NODE_DEV",
     "build-for-devtools-prod": "yarn build-for-devtools --type=NODE_PROD",
     "linc": "node ./scripts/tasks/linc.js",
diff --git a/packages/react-devtools-shared/src/__tests__/__snapshots__/inspectedElementContext-test.js.snap b/packages/react-devtools-shared/src/__tests__/__snapshots__/inspectedElementContext-test.js.snap
deleted file mode 100644
index 8270d0c5b5f1d..0000000000000
--- a/packages/react-devtools-shared/src/__tests__/__snapshots__/inspectedElementContext-test.js.snap
+++ /dev/null
@@ -1,670 +0,0 @@
-// Jest Snapshot v1, https://goo.gl/fbAQLP
-
-exports[`InspectedElementContext should dehydrate complex nested values when requested: 1: Initially inspect element 1`] = `
-{
-  "id": 2,
-  "owners": null,
-  "context": null,
-  "hooks": null,
-  "props": {
-    "set_of_sets": {
-      "0": {},
-      "1": {}
-    }
-  },
-  "state": null
-}
-`;
-
-exports[`InspectedElementContext should dehydrate complex nested values when requested: 2: Inspect props.set_of_sets.0 1`] = `
-{
-  "id": 2,
-  "owners": null,
-  "context": null,
-  "hooks": null,
-  "props": {
-    "set_of_sets": {
-      "0": {
-        "0": 1,
-        "1": 2,
-        "2": 3
-      },
-      "1": {}
-    }
-  },
-  "state": null
-}
-`;
-
-exports[`InspectedElementContext should display complex values of useDebugValue: DisplayedComplexValue 1`] = `
-{
-  "id": 2,
-  "owners": null,
-  "context": null,
-  "hooks": [
-    {
-      "id": null,
-      "isStateEditable": false,
-      "name": "DebuggableHook",
-      "value": {
-        "foo": 2
-      },
-      "subHooks": [
-        {
-          "id": 0,
-          "isStateEditable": true,
-          "name": "State",
-          "value": 1,
-          "subHooks": []
-        }
-      ]
-    }
-  ],
-  "props": {},
-  "state": null
-}
-`;
-
-exports[`InspectedElementContext should include updates for nested values that were previously hydrated: 1: Initially inspect element 1`] = `
-{
-  "id": 2,
-  "owners": null,
-  "context": null,
-  "hooks": null,
-  "props": {
-    "nestedObject": {
-      "a": {},
-      "c": {}
-    }
-  },
-  "state": null
-}
-`;
-
-exports[`InspectedElementContext should include updates for nested values that were previously hydrated: 2: Inspect props.nestedObject.a 1`] = `
-{
-  "id": 2,
-  "owners": null,
-  "context": null,
-  "hooks": null,
-  "props": {
-    "nestedObject": {
-      "a": {
-        "value": 1,
-        "b": {
-          "value": 1
-        }
-      },
-      "c": {}
-    }
-  },
-  "state": null
-}
-`;
-
-exports[`InspectedElementContext should include updates for nested values that were previously hydrated: 3: Inspect props.nestedObject.c 1`] = `
-{
-  "id": 2,
-  "owners": null,
-  "context": null,
-  "hooks": null,
-  "props": {
-    "nestedObject": {
-      "a": {
-        "value": 1,
-        "b": {
-          "value": 1
-        }
-      },
-      "c": {
-        "value": 1,
-        "d": {
-          "value": 1,
-          "e": {}
-        }
-      }
-    }
-  },
-  "state": null
-}
-`;
-
-exports[`InspectedElementContext should include updates for nested values that were previously hydrated: 4: update inspected element 1`] = `
-{
-  "id": 2,
-  "owners": null,
-  "context": null,
-  "hooks": null,
-  "props": {
-    "nestedObject": {
-      "a": {
-        "value": 2,
-        "b": {
-          "value": 2
-        }
-      },
-      "c": {
-        "value": 2,
-        "d": {
-          "value": 2,
-          "e": {}
-        }
-      }
-    }
-  },
-  "state": null
-}
-`;
-
-exports[`InspectedElementContext should inspect hooks for components that only use context: 1: Inspected element 2 1`] = `
-{
-  "id": 2,
-  "owners": null,
-  "context": null,
-  "hooks": [
-    {
-      "id": null,
-      "isStateEditable": false,
-      "name": "Context",
-      "value": true,
-      "subHooks": []
-    }
-  ],
-  "props": {
-    "a": 1,
-    "b": "abc"
-  },
-  "state": null
-}
-`;
-
-exports[`InspectedElementContext should inspect the currently selected element: 1: Inspected element 2 1`] = `
-{
-  "id": 2,
-  "owners": null,
-  "context": null,
-  "hooks": [
-    {
-      "id": 0,
-      "isStateEditable": true,
-      "name": "State",
-      "value": 1,
-      "subHooks": []
-    }
-  ],
-  "props": {
-    "a": 1,
-    "b": "abc"
-  },
-  "state": null
-}
-`;
-
-exports[`InspectedElementContext should not consume iterables while inspecting: 1: Inspected element 2 1`] = `
-{
-  "id": 2,
-  "owners": null,
-  "context": null,
-  "hooks": null,
-  "props": {
-    "prop": {}
-  },
-  "state": null
-}
-`;
-
-exports[`InspectedElementContext should not dehydrate nested values until explicitly requested: 1: Initially inspect element 1`] = `
-{
-  "id": 2,
-  "owners": null,
-  "context": null,
-  "hooks": [
-    {
-      "id": 0,
-      "isStateEditable": true,
-      "name": "State",
-      "value": {
-        "foo": {}
-      },
-      "subHooks": []
-    }
-  ],
-  "props": {
-    "nestedObject": {
-      "a": {}
-    }
-  },
-  "state": null
-}
-`;
-
-exports[`InspectedElementContext should not dehydrate nested values until explicitly requested: 2: Inspect props.nestedObject.a 1`] = `
-{
-  "id": 2,
-  "owners": null,
-  "context": null,
-  "hooks": [
-    {
-      "id": 0,
-      "isStateEditable": true,
-      "name": "State",
-      "value": {
-        "foo": {}
-      },
-      "subHooks": []
-    }
-  ],
-  "props": {
-    "nestedObject": {
-      "a": {
-        "b": {
-          "c": {}
-        }
-      }
-    }
-  },
-  "state": null
-}
-`;
-
-exports[`InspectedElementContext should not dehydrate nested values until explicitly requested: 3: Inspect props.nestedObject.a.b.c 1`] = `
-{
-  "id": 2,
-  "owners": null,
-  "context": null,
-  "hooks": [
-    {
-      "id": 0,
-      "isStateEditable": true,
-      "name": "State",
-      "value": {
-        "foo": {}
-      },
-      "subHooks": []
-    }
-  ],
-  "props": {
-    "nestedObject": {
-      "a": {
-        "b": {
-          "c": [
-            {
-              "d": {}
-            }
-          ]
-        }
-      }
-    }
-  },
-  "state": null
-}
-`;
-
-exports[`InspectedElementContext should not dehydrate nested values until explicitly requested: 4: Inspect props.nestedObject.a.b.c.0.d 1`] = `
-{
-  "id": 2,
-  "owners": null,
-  "context": null,
-  "hooks": [
-    {
-      "id": 0,
-      "isStateEditable": true,
-      "name": "State",
-      "value": {
-        "foo": {}
-      },
-      "subHooks": []
-    }
-  ],
-  "props": {
-    "nestedObject": {
-      "a": {
-        "b": {
-          "c": [
-            {
-              "d": {
-                "e": {}
-              }
-            }
-          ]
-        }
-      }
-    }
-  },
-  "state": null
-}
-`;
-
-exports[`InspectedElementContext should not dehydrate nested values until explicitly requested: 5: Inspect hooks.0.value 1`] = `
-{
-  "id": 2,
-  "owners": null,
-  "context": null,
-  "hooks": [
-    {
-      "id": 0,
-      "isStateEditable": true,
-      "name": "State",
-      "value": {
-        "foo": {
-          "bar": {}
-        }
-      },
-      "subHooks": []
-    }
-  ],
-  "props": {
-    "nestedObject": {
-      "a": {
-        "b": {
-          "c": [
-            {
-              "d": {
-                "e": {}
-              }
-            }
-          ]
-        }
-      }
-    }
-  },
-  "state": null
-}
-`;
-
-exports[`InspectedElementContext should not dehydrate nested values until explicitly requested: 6: Inspect hooks.0.value.foo.bar 1`] = `
-{
-  "id": 2,
-  "owners": null,
-  "context": null,
-  "hooks": [
-    {
-      "id": 0,
-      "isStateEditable": true,
-      "name": "State",
-      "value": {
-        "foo": {
-          "bar": {
-            "baz": "hi"
-          }
-        }
-      },
-      "subHooks": []
-    }
-  ],
-  "props": {
-    "nestedObject": {
-      "a": {
-        "b": {
-          "c": [
-            {
-              "d": {
-                "e": {}
-              }
-            }
-          ]
-        }
-      }
-    }
-  },
-  "state": null
-}
-`;
-
-exports[`InspectedElementContext should not re-render a function with hooks if it did not update since it was last inspected: 1: initial render 1`] = `
-{
-  "id": 3,
-  "owners": null,
-  "context": null,
-  "hooks": [
-    {
-      "id": 0,
-      "isStateEditable": true,
-      "name": "State",
-      "value": 0,
-      "subHooks": []
-    }
-  ],
-  "props": {
-    "a": 1,
-    "b": "abc"
-  },
-  "state": null
-}
-`;
-
-exports[`InspectedElementContext should not re-render a function with hooks if it did not update since it was last inspected: 2: updated state 1`] = `
-{
-  "id": 3,
-  "owners": null,
-  "context": null,
-  "hooks": [
-    {
-      "id": 0,
-      "isStateEditable": true,
-      "name": "State",
-      "value": 0,
-      "subHooks": []
-    }
-  ],
-  "props": {
-    "a": 2,
-    "b": "def"
-  },
-  "state": null
-}
-`;
-
-exports[`InspectedElementContext should not tear if hydration is requested after an update: 1: Initially inspect element 1`] = `
-{
-  "id": 2,
-  "owners": null,
-  "context": null,
-  "hooks": null,
-  "props": {
-    "nestedObject": {
-      "value": 1,
-      "a": {}
-    }
-  },
-  "state": null
-}
-`;
-
-exports[`InspectedElementContext should not tear if hydration is requested after an update: 2: Inspect props.nestedObject.a 1`] = `
-{
-  "id": 2,
-  "owners": null,
-  "context": null,
-  "hooks": null,
-  "props": {
-    "nestedObject": {
-      "value": 2,
-      "a": {
-        "value": 2,
-        "b": {
-          "value": 2
-        }
-      }
-    }
-  },
-  "state": null
-}
-`;
-
-exports[`InspectedElementContext should poll for updates for the currently selected element: 1: initial render 1`] = `
-{
-  "id": 2,
-  "owners": null,
-  "context": null,
-  "hooks": null,
-  "props": {
-    "a": 1,
-    "b": "abc"
-  },
-  "state": null
-}
-`;
-
-exports[`InspectedElementContext should poll for updates for the currently selected element: 2: updated state 1`] = `
-{
-  "id": 2,
-  "owners": null,
-  "context": null,
-  "hooks": null,
-  "props": {
-    "a": 2,
-    "b": "def"
-  },
-  "state": null
-}
-`;
-
-exports[`InspectedElementContext should support complex data types: 1: Inspected element 2 1`] = `
-{
-  "id": 2,
-  "owners": null,
-  "context": null,
-  "hooks": null,
-  "props": {
-    "anonymous_fn": {},
-    "array_buffer": {},
-    "array_of_arrays": [
-      {}
-    ],
-    "big_int": {},
-    "bound_fn": {},
-    "data_view": {},
-    "date": {},
-    "fn": {},
-    "html_element": {},
-    "immutable": {
-      "0": {},
-      "1": {},
-      "2": {}
-    },
-    "map": {
-      "0": {},
-      "1": {}
-    },
-    "map_of_maps": {
-      "0": {},
-      "1": {}
-    },
-    "object_of_objects": {
-      "inner": {}
-    },
-    "object_with_symbol": {
-      "Symbol(name)": "hello"
-    },
-    "proxy": {},
-    "react_element": {},
-    "regexp": {},
-    "set": {
-      "0": "abc",
-      "1": 123
-    },
-    "set_of_sets": {
-      "0": {},
-      "1": {}
-    },
-    "symbol": {},
-    "typed_array": {
-      "0": 100,
-      "1": -100,
-      "2": 0
-    }
-  },
-  "state": null
-}
-`;
-
-exports[`InspectedElementContext should support custom objects with enumerable properties and getters: 1: Inspected element 2 1`] = `
-{
-  "id": 2,
-  "owners": null,
-  "context": null,
-  "hooks": null,
-  "props": {
-    "data": {
-      "_number": 42,
-      "number": 42
-    }
-  },
-  "state": null
-}
-`;
-
-exports[`InspectedElementContext should support objects with no prototype: 1: Inspected element 2 1`] = `
-{
-  "id": 2,
-  "owners": null,
-  "context": null,
-  "hooks": null,
-  "props": {
-    "object": {
-      "string": "abc",
-      "number": 123,
-      "boolean": true
-    }
-  },
-  "state": null
-}
-`;
-
-exports[`InspectedElementContext should support objects with overridden hasOwnProperty: 1: Inspected element 2 1`] = `
-{
-  "id": 2,
-  "owners": null,
-  "context": null,
-  "hooks": null,
-  "props": {
-    "object": {
-      "name": "blah",
-      "hasOwnProperty": true
-    }
-  },
-  "state": null
-}
-`;
-
-exports[`InspectedElementContext should support objects with with inherited keys: 1: Inspected element 2 1`] = `
-{
-  "id": 2,
-  "owners": null,
-  "context": null,
-  "hooks": null,
-  "props": {
-    "object": {
-      "123": 3,
-      "enumerableString": 2,
-      "Symbol(enumerableSymbol)": 3,
-      "enumerableStringBase": 1,
-      "Symbol(enumerableSymbolBase)": 1
-    }
-  },
-  "state": null
-}
-`;
-
-exports[`InspectedElementContext should support simple data types: 1: Initial inspection 1`] = `
-{
-  "id": 2,
-  "owners": null,
-  "context": null,
-  "hooks": null,
-  "props": {
-    "boolean_false": false,
-    "boolean_true": true,
-    "infinity": null,
-    "integer_zero": 0,
-    "integer_one": 1,
-    "float": 1.23,
-    "string": "abc",
-    "string_empty": "",
-    "nan": null,
-    "value_null": null
-  },
-  "state": null
-}
-`;
diff --git a/packages/react-devtools-shared/src/__tests__/dehydratedValueSerializer.js b/packages/react-devtools-shared/src/__tests__/dehydratedValueSerializer.js
new file mode 100644
index 0000000000000..d8eac57df035c
--- /dev/null
+++ b/packages/react-devtools-shared/src/__tests__/dehydratedValueSerializer.js
@@ -0,0 +1,39 @@
+/**
+ * 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
+ */
+
+// test() is part of Jest's serializer API
+export function test(maybeDehydratedValue) {
+  const {meta} = require('react-devtools-shared/src/hydration');
+  return (
+    maybeDehydratedValue !== null &&
+    typeof maybeDehydratedValue === 'object' &&
+    maybeDehydratedValue.hasOwnProperty(meta.inspectable) &&
+    maybeDehydratedValue[meta.inspected] !== true
+  );
+}
+
+// print() is part of Jest's serializer API
+export function print(dehydratedValue, serialize, indent) {
+  const {meta} = require('react-devtools-shared/src/hydration');
+  const indentation = Math.max(indent('.').indexOf('.') - 2, 0);
+  const paddingLeft = ' '.repeat(indentation);
+  return (
+    'Dehydrated {\n' +
+    paddingLeft +
+    '  "preview_short": ' +
+    dehydratedValue[meta.preview_short] +
+    ',\n' +
+    paddingLeft +
+    '  "preview_long": ' +
+    dehydratedValue[meta.preview_long] +
+    ',\n' +
+    paddingLeft +
+    '}'
+  );
+}
diff --git a/packages/react-devtools-shared/src/__tests__/inspectedElementContext-test.js b/packages/react-devtools-shared/src/__tests__/inspectedElement-test.js
similarity index 75%
rename from packages/react-devtools-shared/src/__tests__/inspectedElementContext-test.js
rename to packages/react-devtools-shared/src/__tests__/inspectedElement-test.js
index c60560302f55f..6a596a8b4a624 100644
--- a/packages/react-devtools-shared/src/__tests__/inspectedElementContext-test.js
+++ b/packages/react-devtools-shared/src/__tests__/inspectedElement-test.js
@@ -8,16 +8,12 @@
  */
 
 import typeof ReactTestRenderer from 'react-test-renderer';
-import type {
-  CopyInspectedElementPath,
-  GetInspectedElementPath,
-  StoreAsGlobal,
-} from 'react-devtools-shared/src/devtools/views/Components/InspectedElementContext';
+import {withErrorsOrWarningsIgnored} from 'react-devtools-shared/src/__tests__/utils';
+
 import type {FrontendBridge} from 'react-devtools-shared/src/bridge';
 import type Store from 'react-devtools-shared/src/devtools/store';
-import {withErrorsOrWarningsIgnored} from 'react-devtools-shared/src/__tests__/utils';
 
-describe('InspectedElementContext', () => {
+describe('InspectedElement', () => {
   let React;
   let ReactDOM;
   let PropTypes;
@@ -81,14 +77,26 @@ describe('InspectedElementContext', () => {
         <TreeContextController
           defaultSelectedElementID={defaultSelectedElementID}
           defaultSelectedElementIndex={defaultSelectedElementIndex}>
-          <InspectedElementContextController>
-            {children}
-          </InspectedElementContextController>
+          <React.Suspense fallback="Loading...">
+            <InspectedElementContextController>
+              {children}
+            </InspectedElementContextController>
+          </React.Suspense>
         </TreeContextController>
       </StoreContext.Provider>
     </BridgeContext.Provider>
   );
 
+  function useInspectedElement(id: number) {
+    const {inspectedElement} = React.useContext(InspectedElementContext);
+    return inspectedElement;
+  }
+
+  function useInspectElementPath(id: number) {
+    const {inspectPaths} = React.useContext(InspectedElementContext);
+    return inspectPaths;
+  }
+
   it('should inspect the currently selected element', async done => {
     const Example = () => {
       const [count] = React.useState(1);
@@ -105,9 +113,29 @@ describe('InspectedElementContext', () => {
     let didFinish = false;
 
     function Suspender({target}) {
-      const {getInspectedElement} = React.useContext(InspectedElementContext);
-      const inspectedElement = getInspectedElement(id);
-      expect(inspectedElement).toMatchSnapshot(`1: Inspected element ${id}`);
+      const inspectedElement = useInspectedElement(id);
+      expect(inspectedElement).toMatchInlineSnapshot(`
+        Object {
+          "context": null,
+          "events": undefined,
+          "hooks": Array [
+            Object {
+              "id": 0,
+              "isStateEditable": true,
+              "name": "State",
+              "subHooks": Array [],
+              "value": 1,
+            },
+          ],
+          "id": 2,
+          "owners": null,
+          "props": Object {
+            "a": 1,
+            "b": "abc",
+          },
+          "state": null,
+        }
+      `);
       didFinish = true;
       return null;
     }
@@ -211,8 +239,7 @@ describe('InspectedElementContext', () => {
     ];
 
     function Suspender({target, shouldHaveLegacyContext}) {
-      const {getInspectedElement} = React.useContext(InspectedElementContext);
-      const inspectedElement = getInspectedElement(target);
+      const inspectedElement = useInspectedElement(target);
 
       expect(inspectedElement.context).not.toBe(null);
       expect(inspectedElement.hasLegacyContext).toBe(shouldHaveLegacyContext);
@@ -257,8 +284,7 @@ describe('InspectedElementContext', () => {
     let inspectedElement = null;
 
     function Suspender({target}) {
-      const {getInspectedElement} = React.useContext(InspectedElementContext);
-      inspectedElement = getInspectedElement(id);
+      inspectedElement = useInspectedElement(id);
       return null;
     }
 
@@ -273,14 +299,28 @@ describe('InspectedElementContext', () => {
         </Contexts>,
       );
     }, false);
-    expect(inspectedElement).toMatchSnapshot('1: initial render');
+    expect(inspectedElement.props).toMatchInlineSnapshot(`
+      Object {
+        "a": 1,
+        "b": "abc",
+      }
+    `);
 
     await utils.actAsync(
       () => ReactDOM.render(<Example a={2} b="def" />, container),
       false,
     );
 
-    inspectedElement = null;
+    // TODO (cache)
+    // This test only passes if both the check-for-updates poll AND the test renderer.update() call are included below.
+    // It seems like either one of the two should be sufficient but:
+    // 1. Running only check-for-updates schedules a transition that React never renders.
+    // 2. Running only renderer.update() loads stale data (first props)
+
+    // Wait for our check-for-updates poll to get the new data.
+    jest.runOnlyPendingTimers();
+    await Promise.resolve();
+
     await utils.actAsync(
       () =>
         renderer.update(
@@ -294,7 +334,12 @@ describe('InspectedElementContext', () => {
         ),
       false,
     );
-    expect(inspectedElement).toMatchSnapshot('2: updated state');
+    expect(inspectedElement.props).toMatchInlineSnapshot(`
+      Object {
+        "a": 2,
+        "b": "def",
+      }
+    `);
 
     done();
   });
@@ -305,6 +350,7 @@ describe('InspectedElementContext', () => {
     const Wrapper = ({children}) => children;
     const Target = React.memo(props => {
       targetRenderCount++;
+      // Even though his hook isn't referenced, it's used to observe backend rendering.
       React.useState(0);
       return null;
     });
@@ -324,8 +370,7 @@ describe('InspectedElementContext', () => {
     let inspectedElement = null;
 
     function Suspender({target}) {
-      const {getInspectedElement} = React.useContext(InspectedElementContext);
-      inspectedElement = getInspectedElement(target);
+      inspectedElement = useInspectedElement(target);
       return null;
     }
 
@@ -346,7 +391,12 @@ describe('InspectedElementContext', () => {
       false,
     );
     expect(targetRenderCount).toBe(1);
-    expect(inspectedElement).toMatchSnapshot('1: initial render');
+    expect(inspectedElement.props).toMatchInlineSnapshot(`
+      Object {
+        "a": 1,
+        "b": "abc",
+      }
+    `);
 
     const initialInspectedElement = inspectedElement;
 
@@ -369,6 +419,7 @@ describe('InspectedElementContext', () => {
     expect(inspectedElement).toEqual(initialInspectedElement);
 
     targetRenderCount = 0;
+    inspectedElement = null;
 
     await utils.actAsync(
       () =>
@@ -382,8 +433,26 @@ describe('InspectedElementContext', () => {
     );
 
     // Target should have been rendered once (by ReactDOM) and once by DevTools for inspection.
+    await utils.actAsync(
+      () =>
+        renderer.update(
+          <Contexts
+            defaultSelectedElementID={id}
+            defaultSelectedElementIndex={1}>
+            <React.Suspense fallback={null}>
+              <Suspender target={id} />
+            </React.Suspense>
+          </Contexts>,
+        ),
+      false,
+    );
     expect(targetRenderCount).toBe(2);
-    expect(inspectedElement).toMatchSnapshot('2: updated state');
+    expect(inspectedElement.props).toMatchInlineSnapshot(`
+      Object {
+        "a": 2,
+        "b": "def",
+      }
+    `);
 
     done();
   });
@@ -426,8 +495,7 @@ describe('InspectedElementContext', () => {
     let inspectedElement = null;
 
     function Suspender({target}) {
-      const {getInspectedElement} = React.useContext(InspectedElementContext);
-      inspectedElement = getInspectedElement(target);
+      inspectedElement = useInspectedElement(target);
       return null;
     }
 
@@ -482,8 +550,7 @@ describe('InspectedElementContext', () => {
     let inspectedElement = null;
 
     function Suspender({target}) {
-      const {getInspectedElement} = React.useContext(InspectedElementContext);
-      inspectedElement = getInspectedElement(id);
+      inspectedElement = useInspectedElement(id);
       return null;
     }
 
@@ -501,9 +568,6 @@ describe('InspectedElementContext', () => {
       false,
     );
 
-    expect(inspectedElement).not.toBeNull();
-    expect(inspectedElement).toMatchSnapshot('1: Initial inspection');
-
     const {props} = (inspectedElement: any);
     expect(props.boolean_false).toBe(false);
     expect(props.boolean_true).toBe(true);
@@ -606,8 +670,7 @@ describe('InspectedElementContext', () => {
     let inspectedElement = null;
 
     function Suspender({target}) {
-      const {getInspectedElement} = React.useContext(InspectedElementContext);
-      inspectedElement = getInspectedElement(id);
+      inspectedElement = useInspectedElement(id);
       return null;
     }
 
@@ -625,9 +688,6 @@ describe('InspectedElementContext', () => {
       false,
     );
 
-    expect(inspectedElement).not.toBeNull();
-    expect(inspectedElement).toMatchSnapshot(`1: Inspected element ${id}`);
-
     const {
       anonymous_fn,
       array_buffer,
@@ -820,8 +880,7 @@ describe('InspectedElementContext', () => {
     let inspectedElement = null;
 
     function Suspender({target}) {
-      const {getInspectedElement} = React.useContext(InspectedElementContext);
-      inspectedElement = getInspectedElement(id);
+      inspectedElement = useInspectedElement(id);
       return null;
     }
 
@@ -839,9 +898,6 @@ describe('InspectedElementContext', () => {
       false,
     );
 
-    expect(inspectedElement).not.toBeNull();
-    expect(inspectedElement).toMatchSnapshot(`1: Inspected element ${id}`);
-
     const {prop} = (inspectedElement: any).props;
     expect(prop[meta.inspectable]).toBe(false);
     expect(prop[meta.name]).toBe('Generator');
@@ -870,8 +926,7 @@ describe('InspectedElementContext', () => {
     let inspectedElement = null;
 
     function Suspender({target}) {
-      const {getInspectedElement} = React.useContext(InspectedElementContext);
-      inspectedElement = getInspectedElement(id);
+      inspectedElement = useInspectedElement(id);
       return null;
     }
 
@@ -889,13 +944,15 @@ describe('InspectedElementContext', () => {
       false,
     );
 
-    expect(inspectedElement).not.toBeNull();
-    expect(inspectedElement).toMatchSnapshot(`1: Inspected element ${id}`);
-    expect(inspectedElement.props.object).toEqual({
-      boolean: true,
-      number: 123,
-      string: 'abc',
-    });
+    expect(inspectedElement.props).toMatchInlineSnapshot(`
+      Object {
+        "object": Object {
+          "boolean": true,
+          "number": 123,
+          "string": "abc",
+        },
+      }
+    `);
 
     done();
   });
@@ -918,8 +975,7 @@ describe('InspectedElementContext', () => {
     let inspectedElement = null;
 
     function Suspender({target}) {
-      const {getInspectedElement} = React.useContext(InspectedElementContext);
-      inspectedElement = getInspectedElement(id);
+      inspectedElement = useInspectedElement(id);
       return null;
     }
 
@@ -937,12 +993,10 @@ describe('InspectedElementContext', () => {
       false,
     );
 
-    expect(inspectedElement).not.toBeNull();
-    expect(inspectedElement).toMatchSnapshot(`1: Inspected element ${id}`);
-    expect(inspectedElement.props.object).toEqual({
-      name: 'blah',
-      hasOwnProperty: true,
-    });
+    // TRICKY: Don't use toMatchInlineSnapshot() for this test!
+    // Our snapshot serializer relies on hasOwnProperty() for feature detection.
+    expect(inspectedElement.props.object.name).toBe('blah');
+    expect(inspectedElement.props.object.hasOwnProperty).toBe(true);
 
     done();
   });
@@ -977,9 +1031,15 @@ describe('InspectedElementContext', () => {
     let didFinish = false;
 
     function Suspender({target}) {
-      const {getInspectedElement} = React.useContext(InspectedElementContext);
-      const inspectedElement = getInspectedElement(id);
-      expect(inspectedElement).toMatchSnapshot(`1: Inspected element ${id}`);
+      const inspectedElement = useInspectedElement(id);
+      expect(inspectedElement.props).toMatchInlineSnapshot(`
+        Object {
+          "data": Object {
+            "_number": 42,
+            "number": 42,
+          },
+        }
+      `);
       didFinish = true;
       return null;
     }
@@ -1075,8 +1135,7 @@ describe('InspectedElementContext', () => {
     let inspectedElement = null;
 
     function Suspender({target}) {
-      const {getInspectedElement} = React.useContext(InspectedElementContext);
-      inspectedElement = getInspectedElement(id);
+      inspectedElement = useInspectedElement(id);
       return null;
     }
 
@@ -1094,15 +1153,17 @@ describe('InspectedElementContext', () => {
       false,
     );
 
-    expect(inspectedElement).not.toBeNull();
-    expect(inspectedElement).toMatchSnapshot(`1: Inspected element ${id}`);
-    expect(inspectedElement.props.object).toEqual({
-      123: 3,
-      'Symbol(enumerableSymbol)': 3,
-      'Symbol(enumerableSymbolBase)': 1,
-      enumerableString: 2,
-      enumerableStringBase: 1,
-    });
+    expect(inspectedElement.props).toMatchInlineSnapshot(`
+      Object {
+        "object": Object {
+          "123": 3,
+          "Symbol(enumerableSymbol)": 3,
+          "Symbol(enumerableSymbolBase)": 1,
+          "enumerableString": 2,
+          "enumerableStringBase": 1,
+        },
+      }
+    `);
 
     done();
   });
@@ -1144,96 +1205,155 @@ describe('InspectedElementContext', () => {
 
     const id = ((store.getElementIDAtIndex(0): any): number);
 
-    let getInspectedElementPath: GetInspectedElementPath = ((null: any): GetInspectedElementPath);
     let inspectedElement = null;
+    let inspectElementPath = null;
 
-    function Suspender({target}) {
-      const context = React.useContext(InspectedElementContext);
-      getInspectedElementPath = context.getInspectedElementPath;
-      inspectedElement = context.getInspectedElement(target);
+    function Suspender({path, target}) {
+      inspectedElement = useInspectedElement(id);
+      inspectElementPath = useInspectElementPath(id);
       return null;
     }
 
-    await utils.actAsync(
-      () =>
-        TestRenderer.create(
-          <Contexts
-            defaultSelectedElementID={id}
-            defaultSelectedElementIndex={0}>
-            <React.Suspense fallback={null}>
-              <Suspender target={id} />
-            </React.Suspense>
-          </Contexts>,
-        ),
-      false,
-    );
-    expect(getInspectedElementPath).not.toBeNull();
-    expect(inspectedElement).not.toBeNull();
-    expect(inspectedElement).toMatchSnapshot('1: Initially inspect element');
+    const renderer = TestRenderer.create(null);
 
-    inspectedElement = null;
-    TestUtilsAct(() => {
-      TestRendererAct(() => {
-        getInspectedElementPath(id, ['props', 'nestedObject', 'a']);
-        jest.runOnlyPendingTimers();
-      });
-    });
-    expect(inspectedElement).not.toBeNull();
-    expect(inspectedElement).toMatchSnapshot('2: Inspect props.nestedObject.a');
+    async function getInspectedElement() {
+      await utils.actAsync(
+        () =>
+          renderer.update(
+            <Contexts
+              defaultSelectedElementID={id}
+              defaultSelectedElementIndex={0}>
+              <React.Suspense fallback={null}>
+                <Suspender target={id} />
+              </React.Suspense>
+            </Contexts>,
+          ),
+        false,
+      );
+    }
 
-    inspectedElement = null;
-    TestUtilsAct(() => {
-      TestRendererAct(() => {
-        getInspectedElementPath(id, ['props', 'nestedObject', 'a', 'b', 'c']);
-        jest.runOnlyPendingTimers();
-      });
-    });
-    expect(inspectedElement).not.toBeNull();
-    expect(inspectedElement).toMatchSnapshot(
-      '3: Inspect props.nestedObject.a.b.c',
-    );
+    // Render once to get a handle on inspectElementPath()
+    await getInspectedElement();
 
-    inspectedElement = null;
-    TestUtilsAct(() => {
-      TestRendererAct(() => {
-        getInspectedElementPath(id, [
-          'props',
-          'nestedObject',
-          'a',
-          'b',
-          'c',
-          0,
-          'd',
-        ]);
-        jest.runOnlyPendingTimers();
+    async function loadPath(path) {
+      TestUtilsAct(() => {
+        TestRendererAct(() => {
+          inspectElementPath(path);
+          jest.runOnlyPendingTimers();
+        });
       });
-    });
-    expect(inspectedElement).not.toBeNull();
-    expect(inspectedElement).toMatchSnapshot(
-      '4: Inspect props.nestedObject.a.b.c.0.d',
-    );
+      await getInspectedElement();
+    }
 
-    inspectedElement = null;
-    TestUtilsAct(() => {
-      TestRendererAct(() => {
-        getInspectedElementPath(id, ['hooks', 0, 'value']);
-        jest.runOnlyPendingTimers();
-      });
-    });
-    expect(inspectedElement).not.toBeNull();
-    expect(inspectedElement).toMatchSnapshot('5: Inspect hooks.0.value');
+    expect(inspectedElement.props).toMatchInlineSnapshot(`
+      Object {
+        "nestedObject": Object {
+          "a": Dehydrated {
+            "preview_short": {…},
+            "preview_long": {b: {…}},
+          },
+        },
+      }
+    `);
+
+    await loadPath(['props', 'nestedObject', 'a']);
+
+    expect(inspectedElement.props).toMatchInlineSnapshot(`
+      Object {
+        "nestedObject": Object {
+          "a": Object {
+            "b": Object {
+              "c": Dehydrated {
+                "preview_short": Array(1),
+                "preview_long": [{…}],
+              },
+            },
+          },
+        },
+      }
+    `);
+
+    await loadPath(['props', 'nestedObject', 'a', 'b', 'c']);
+
+    expect(inspectedElement.props).toMatchInlineSnapshot(`
+      Object {
+        "nestedObject": Object {
+          "a": Object {
+            "b": Object {
+              "c": Array [
+                Object {
+                  "d": Dehydrated {
+                    "preview_short": {…},
+                    "preview_long": {e: {…}},
+                  },
+                },
+              ],
+            },
+          },
+        },
+      }
+    `);
+
+    await loadPath(['props', 'nestedObject', 'a', 'b', 'c', 0, 'd']);
+
+    expect(inspectedElement.props).toMatchInlineSnapshot(`
+      Object {
+        "nestedObject": Object {
+          "a": Object {
+            "b": Object {
+              "c": Array [
+                Object {
+                  "d": Object {
+                    "e": Object {},
+                  },
+                },
+              ],
+            },
+          },
+        },
+      }
+    `);
 
-    inspectedElement = null;
-    TestUtilsAct(() => {
-      TestRendererAct(() => {
-        getInspectedElementPath(id, ['hooks', 0, 'value', 'foo', 'bar']);
-        jest.runOnlyPendingTimers();
-      });
-    });
-    expect(inspectedElement).not.toBeNull();
-    expect(inspectedElement).toMatchSnapshot(
-      '6: Inspect hooks.0.value.foo.bar',
-    );
+    await loadPath(['hooks', 0, 'value']);
+
+    expect(inspectedElement.hooks).toMatchInlineSnapshot(`
+      Array [
+        Object {
+          "id": 0,
+          "isStateEditable": true,
+          "name": "State",
+          "subHooks": Array [],
+          "value": Object {
+            "foo": Object {
+              "bar": Dehydrated {
+                "preview_short": {…},
+                "preview_long": {baz: "hi"},
+              },
+            },
+          },
+        },
+      ]
+    `);
+
+    await loadPath(['hooks', 0, 'value', 'foo', 'bar']);
+
+    expect(inspectedElement.hooks).toMatchInlineSnapshot(`
+      Array [
+        Object {
+          "id": 0,
+          "isStateEditable": true,
+          "name": "State",
+          "subHooks": Array [],
+          "value": Object {
+            "foo": Object {
+              "bar": Object {
+                "baz": "hi",
+              },
+            },
+          },
+        },
+      ]
+    `);
 
     done();
   });
@@ -1253,42 +1373,79 @@ describe('InspectedElementContext', () => {
 
     const id = ((store.getElementIDAtIndex(0): any): number);
 
-    let getInspectedElementPath: GetInspectedElementPath = ((null: any): GetInspectedElementPath);
     let inspectedElement = null;
+    let inspectElementPath = null;
 
-    function Suspender({target}) {
-      const context = React.useContext(InspectedElementContext);
-      getInspectedElementPath = context.getInspectedElementPath;
-      inspectedElement = context.getInspectedElement(target);
+    function Suspender({path, target}) {
+      inspectedElement = useInspectedElement(id);
+      inspectElementPath = useInspectElementPath(id);
       return null;
     }
 
-    await utils.actAsync(
-      () =>
-        TestRenderer.create(
-          <Contexts
-            defaultSelectedElementID={id}
-            defaultSelectedElementIndex={0}>
-            <React.Suspense fallback={null}>
-              <Suspender target={id} />
-            </React.Suspense>
-          </Contexts>,
-        ),
-      false,
-    );
-    expect(getInspectedElementPath).not.toBeNull();
-    expect(inspectedElement).not.toBeNull();
-    expect(inspectedElement).toMatchSnapshot('1: Initially inspect element');
+    const renderer = TestRenderer.create(null);
 
-    inspectedElement = null;
-    TestUtilsAct(() => {
-      TestRendererAct(() => {
-        getInspectedElementPath(id, ['props', 'set_of_sets', 0]);
-        jest.runOnlyPendingTimers();
+    async function getInspectedElement() {
+      await utils.actAsync(
+        () =>
+          renderer.update(
+            <Contexts
+              defaultSelectedElementID={id}
+              defaultSelectedElementIndex={0}>
+              <React.Suspense fallback={null}>
+                <Suspender target={id} />
+              </React.Suspense>
+            </Contexts>,
+          ),
+        false,
+      );
+    }
+
+    // Render once to get a handle on inspectElementPath()
+    await getInspectedElement();
+
+    async function loadPath(path) {
+      TestUtilsAct(() => {
+        TestRendererAct(() => {
+          inspectElementPath(path);
+          jest.runOnlyPendingTimers();
+        });
       });
-    });
-    expect(inspectedElement).not.toBeNull();
-    expect(inspectedElement).toMatchSnapshot('2: Inspect props.set_of_sets.0');
+      await getInspectedElement();
+    }
+
+    expect(inspectedElement.props).toMatchInlineSnapshot(`
+      Object {
+        "set_of_sets": Object {
+          "0": Dehydrated {
+            "preview_short": Set(3),
+            "preview_long": Set(3) {1, 2, 3},
+          },
+          "1": Dehydrated {
+            "preview_short": Set(3),
+            "preview_long": Set(3) {"a", "b", "c"},
+          },
+        },
+      }
+    `);
+
+    await loadPath(['props', 'set_of_sets', 0]);
+
+    expect(inspectedElement.props).toMatchInlineSnapshot(`
+      Object {
+        "set_of_sets": Object {
+          "0": Object {
+            "0": 1,
+            "1": 2,
+            "2": 3,
+          },
+          "1": Object {
+            "0": "a",
+            "1": "b",
+            "2": "c",
+          },
+        },
+      }
+    `);
 
     done();
   });
@@ -1324,48 +1481,107 @@ describe('InspectedElementContext', () => {
 
     const id = ((store.getElementIDAtIndex(0): any): number);
 
-    let getInspectedElementPath: GetInspectedElementPath = ((null: any): GetInspectedElementPath);
     let inspectedElement = null;
+    let inspectElementPath = null;
 
-    function Suspender({target}) {
-      const context = React.useContext(InspectedElementContext);
-      getInspectedElementPath = context.getInspectedElementPath;
-      inspectedElement = context.getInspectedElement(id);
+    function Suspender({path, target}) {
+      inspectedElement = useInspectedElement(id);
+      inspectElementPath = useInspectElementPath(id);
       return null;
     }
 
-    await utils.actAsync(
-      () =>
-        TestRenderer.create(
-          <Contexts
-            defaultSelectedElementID={id}
-            defaultSelectedElementIndex={0}>
-            <React.Suspense fallback={null}>
-              <Suspender target={id} />
-            </React.Suspense>
-          </Contexts>,
-        ),
-      false,
-    );
-    expect(getInspectedElementPath).not.toBeNull();
-    expect(inspectedElement).not.toBeNull();
-    expect(inspectedElement).toMatchSnapshot('1: Initially inspect element');
+    const renderer = TestRenderer.create(null);
 
-    inspectedElement = null;
-    TestRendererAct(() => {
-      getInspectedElementPath(id, ['props', 'nestedObject', 'a']);
-      jest.runOnlyPendingTimers();
-    });
-    expect(inspectedElement).not.toBeNull();
-    expect(inspectedElement).toMatchSnapshot('2: Inspect props.nestedObject.a');
+    async function getInspectedElement() {
+      await utils.actAsync(
+        () =>
+          renderer.update(
+            <Contexts
+              defaultSelectedElementID={id}
+              defaultSelectedElementIndex={0}>
+              <React.Suspense fallback={null}>
+                <Suspender target={id} />
+              </React.Suspense>
+            </Contexts>,
+          ),
+        false,
+      );
+    }
 
-    inspectedElement = null;
-    TestRendererAct(() => {
-      getInspectedElementPath(id, ['props', 'nestedObject', 'c']);
-      jest.runOnlyPendingTimers();
-    });
-    expect(inspectedElement).not.toBeNull();
-    expect(inspectedElement).toMatchSnapshot('3: Inspect props.nestedObject.c');
+    // Render once to get a handle on inspectElementPath()
+    await getInspectedElement();
+
+    async function loadPath(path) {
+      TestUtilsAct(() => {
+        TestRendererAct(() => {
+          inspectElementPath(path);
+          jest.runOnlyPendingTimers();
+        });
+      });
+      await getInspectedElement();
+    }
+
+    expect(inspectedElement.props).toMatchInlineSnapshot(`
+      Object {
+        "nestedObject": Object {
+          "a": Dehydrated {
+            "preview_short": {…},
+            "preview_long": {b: {…}, value: 1},
+          },
+          "c": Dehydrated {
+            "preview_short": {…},
+            "preview_long": {d: {…}, value: 1},
+          },
+        },
+      }
+    `);
+
+    await loadPath(['props', 'nestedObject', 'a']);
+
+    expect(inspectedElement.props).toMatchInlineSnapshot(`
+      Object {
+        "nestedObject": Object {
+          "a": Object {
+            "b": Object {
+              "value": 1,
+            },
+            "value": 1,
+          },
+          "c": Object {
+            "d": Dehydrated {
+              "preview_short": {…},
+              "preview_long": {e: {…}, value: 1},
+            },
+            "value": 1,
+          },
+        },
+      }
+    `);
+
+    await loadPath(['props', 'nestedObject', 'c']);
+
+    expect(inspectedElement.props).toMatchInlineSnapshot(`
+      Object {
+        "nestedObject": Object {
+          "a": Object {
+            "b": Object {
+              "value": 1,
+            },
+            "value": 1,
+          },
+          "c": Object {
+            "d": Object {
+              "e": Dehydrated {
+                "preview_short": {…},
+                "preview_long": {value: 1},
+              },
+              "value": 1,
+            },
+            "value": 1,
+          },
+        },
+      }
+    `);
 
     TestRendererAct(() => {
       TestUtilsAct(() => {
@@ -1394,12 +1610,33 @@ describe('InspectedElementContext', () => {
       });
     });
 
-    TestRendererAct(() => {
-      inspectedElement = null;
-      jest.advanceTimersByTime(1000);
-    });
-    expect(inspectedElement).not.toBeNull();
-    expect(inspectedElement).toMatchSnapshot('4: update inspected element');
+    // Wait for pending poll-for-update and then update inspected element data.
+    jest.runOnlyPendingTimers();
+    await Promise.resolve();
+    await getInspectedElement();
+
+    expect(inspectedElement.props).toMatchInlineSnapshot(`
+      Object {
+        "nestedObject": Object {
+          "a": Object {
+            "b": Object {
+              "value": 2,
+            },
+            "value": 2,
+          },
+          "c": Object {
+            "d": Object {
+              "e": Dehydrated {
+                "preview_short": {…},
+                "preview_long": {value: 2},
+              },
+              "value": 2,
+            },
+            "value": 2,
+          },
+        },
+      }
+    `);
 
     done();
   });
@@ -1427,32 +1664,57 @@ describe('InspectedElementContext', () => {
 
     const id = ((store.getElementIDAtIndex(0): any): number);
 
-    let getInspectedElementPath: GetInspectedElementPath = ((null: any): GetInspectedElementPath);
     let inspectedElement = null;
+    let inspectElementPath = null;
 
-    function Suspender({target}) {
-      const context = React.useContext(InspectedElementContext);
-      getInspectedElementPath = context.getInspectedElementPath;
-      inspectedElement = context.getInspectedElement(id);
+    function Suspender({path, target}) {
+      inspectedElement = useInspectedElement(id);
+      inspectElementPath = useInspectElementPath(id);
       return null;
     }
 
-    await utils.actAsync(
-      () =>
-        TestRenderer.create(
-          <Contexts
-            defaultSelectedElementID={id}
-            defaultSelectedElementIndex={0}>
-            <React.Suspense fallback={null}>
-              <Suspender target={id} />
-            </React.Suspense>
-          </Contexts>,
-        ),
-      false,
-    );
-    expect(getInspectedElementPath).not.toBeNull();
-    expect(inspectedElement).not.toBeNull();
-    expect(inspectedElement).toMatchSnapshot('1: Initially inspect element');
+    const renderer = TestRenderer.create(null);
+
+    async function getInspectedElement() {
+      await utils.actAsync(
+        () =>
+          renderer.update(
+            <Contexts
+              defaultSelectedElementID={id}
+              defaultSelectedElementIndex={0}>
+              <React.Suspense fallback={null}>
+                <Suspender target={id} />
+              </React.Suspense>
+            </Contexts>,
+          ),
+        false,
+      );
+    }
+
+    // Render once to get a handle on inspectElementPath()
+    await getInspectedElement();
+
+    async function loadPath(path) {
+      TestUtilsAct(() => {
+        TestRendererAct(() => {
+          inspectElementPath(path);
+          jest.runOnlyPendingTimers();
+        });
+      });
+      await getInspectedElement();
+    }
+
+    expect(inspectedElement.props).toMatchInlineSnapshot(`
+      Object {
+        "nestedObject": Object {
+          "a": Dehydrated {
+            "preview_short": {…},
+            "preview_long": {b: {…}, value: 1},
+          },
+          "value": 1,
+        },
+      }
+    `);
 
     TestUtilsAct(() => {
       ReactDOM.render(
@@ -1471,16 +1733,21 @@ describe('InspectedElementContext', () => {
       );
     });
 
-    inspectedElement = null;
+    await loadPath(['props', 'nestedObject', 'a']);
 
-    TestRendererAct(() => {
-      TestUtilsAct(() => {
-        getInspectedElementPath(id, ['props', 'nestedObject', 'a']);
-        jest.runOnlyPendingTimers();
-      });
-    });
-    expect(inspectedElement).not.toBeNull();
-    expect(inspectedElement).toMatchSnapshot('2: Inspect props.nestedObject.a');
+    expect(inspectedElement.props).toMatchInlineSnapshot(`
+      Object {
+        "nestedObject": Object {
+          "a": Object {
+            "b": Object {
+              "value": 2,
+            },
+            "value": 2,
+          },
+          "value": 2,
+        },
+      }
+    `);
 
     done();
   });
@@ -1502,9 +1769,29 @@ describe('InspectedElementContext', () => {
     let didFinish = false;
 
     function Suspender({target}) {
-      const {getInspectedElement} = React.useContext(InspectedElementContext);
-      const inspectedElement = getInspectedElement(id);
-      expect(inspectedElement).toMatchSnapshot(`1: Inspected element ${id}`);
+      const inspectedElement = useInspectedElement(id);
+      expect(inspectedElement).toMatchInlineSnapshot(`
+        Object {
+          "context": null,
+          "events": undefined,
+          "hooks": Array [
+            Object {
+              "id": null,
+              "isStateEditable": false,
+              "name": "Context",
+              "subHooks": Array [],
+              "value": true,
+            },
+          ],
+          "id": 2,
+          "owners": null,
+          "props": Object {
+            "a": 1,
+            "b": "abc",
+          },
+          "state": null,
+        }
+      `);
       didFinish = true;
       return null;
     }
@@ -1554,8 +1841,20 @@ describe('InspectedElementContext', () => {
     let storeAsGlobal: StoreAsGlobal = ((null: any): StoreAsGlobal);
 
     function Suspender({target}) {
-      const context = React.useContext(InspectedElementContext);
-      storeAsGlobal = context.storeAsGlobal;
+      storeAsGlobal = (elementID: number, path: Array<string | number>) => {
+        const rendererID = store.getRendererIDForElement(elementID);
+        if (rendererID !== null) {
+          const {
+            storeAsGlobal: storeAsGlobalAPI,
+          } = require('react-devtools-shared/src/backendAPI');
+          storeAsGlobalAPI({
+            bridge,
+            id: elementID,
+            path,
+            rendererID,
+          });
+        }
+      };
       return null;
     }
 
@@ -1572,23 +1871,22 @@ describe('InspectedElementContext', () => {
         ),
       false,
     );
-    expect(storeAsGlobal).not.toBeNull();
 
     jest.spyOn(console, 'log').mockImplementation(() => {});
 
     // Should store the whole value (not just the hydrated parts)
     storeAsGlobal(id, ['props', 'nestedObject']);
     jest.runOnlyPendingTimers();
-    expect(console.log).toHaveBeenCalledWith('$reactTemp1');
-    expect(global.$reactTemp1).toBe(nestedObject);
+    expect(console.log).toHaveBeenCalledWith('$reactTemp0');
+    expect(global.$reactTemp0).toBe(nestedObject);
 
     console.log.mockReset();
 
     // Should store the nested property specified (not just the outer value)
     storeAsGlobal(id, ['props', 'nestedObject', 'a', 'b']);
     jest.runOnlyPendingTimers();
-    expect(console.log).toHaveBeenCalledWith('$reactTemp2');
-    expect(global.$reactTemp2).toBe(nestedObject.a.b);
+    expect(console.log).toHaveBeenCalledWith('$reactTemp1');
+    expect(global.$reactTemp1).toBe(nestedObject.a.b);
 
     done();
   });
@@ -1620,8 +1918,20 @@ describe('InspectedElementContext', () => {
     let copyPath: CopyInspectedElementPath = ((null: any): CopyInspectedElementPath);
 
     function Suspender({target}) {
-      const context = React.useContext(InspectedElementContext);
-      copyPath = context.copyInspectedElementPath;
+      copyPath = (elementID: number, path: Array<string | number>) => {
+        const rendererID = store.getRendererIDForElement(elementID);
+        if (rendererID !== null) {
+          const {
+            copyInspectedElementPath,
+          } = require('react-devtools-shared/src/backendAPI');
+          copyInspectedElementPath({
+            bridge,
+            id: elementID,
+            path,
+            rendererID,
+          });
+        }
+      };
       return null;
     }
 
@@ -1712,8 +2022,20 @@ describe('InspectedElementContext', () => {
     let copyPath: CopyInspectedElementPath = ((null: any): CopyInspectedElementPath);
 
     function Suspender({target}) {
-      const context = React.useContext(InspectedElementContext);
-      copyPath = context.copyInspectedElementPath;
+      copyPath = (elementID: number, path: Array<string | number>) => {
+        const rendererID = store.getRendererIDForElement(elementID);
+        if (rendererID !== null) {
+          const {
+            copyInspectedElementPath,
+          } = require('react-devtools-shared/src/backendAPI');
+          copyInspectedElementPath({
+            bridge,
+            id: elementID,
+            path,
+            rendererID,
+          });
+        }
+      };
       return null;
     }
 
@@ -1761,12 +2083,9 @@ describe('InspectedElementContext', () => {
   });
 
   it('should display complex values of useDebugValue', async done => {
-    let getInspectedElementPath: GetInspectedElementPath = ((null: any): GetInspectedElementPath);
     let inspectedElement = null;
     function Suspender({target}) {
-      const context = React.useContext(InspectedElementContext);
-      getInspectedElementPath = context.getInspectedElementPath;
-      inspectedElement = context.getInspectedElement(target);
+      inspectedElement = useInspectedElement(target);
       return null;
     }
 
@@ -1803,9 +2122,27 @@ describe('InspectedElementContext', () => {
         ),
       false,
     );
-    expect(getInspectedElementPath).not.toBeNull();
-    expect(inspectedElement).not.toBeNull();
-    expect(inspectedElement).toMatchSnapshot('DisplayedComplexValue');
+    expect(inspectedElement.hooks).toMatchInlineSnapshot(`
+      Array [
+        Object {
+          "id": null,
+          "isStateEditable": false,
+          "name": "DebuggableHook",
+          "subHooks": Array [
+            Object {
+              "id": 0,
+              "isStateEditable": true,
+              "name": "State",
+              "subHooks": Array [],
+              "value": 1,
+            },
+          ],
+          "value": Object {
+            "foo": 2,
+          },
+        },
+      ]
+    `);
 
     done();
   });
@@ -1825,8 +2162,7 @@ describe('InspectedElementContext', () => {
       let warnings = null;
 
       function Suspender({target}) {
-        const {getInspectedElement} = React.useContext(InspectedElementContext);
-        const inspectedElement = getInspectedElement(id);
+        const inspectedElement = useInspectedElement(id);
         errors = inspectedElement.errors;
         warnings = inspectedElement.warnings;
         return null;
@@ -2038,7 +2374,11 @@ describe('InspectedElementContext', () => {
         );
       });
 
-      store.clearErrorsAndWarnings();
+      const {
+        clearErrorsAndWarnings,
+      } = require('react-devtools-shared/src/backendAPI');
+      clearErrorsAndWarnings({bridge, store});
+
       // Flush events to the renderer.
       jest.runOnlyPendingTimers();
 
@@ -2051,7 +2391,7 @@ describe('InspectedElementContext', () => {
       `);
     });
 
-    it('can be cleared for a particular Fiber (only errors)', async () => {
+    it('can be cleared for a particular Fiber (only warnings)', async () => {
       const Example = ({id}) => {
         console.error(`test-only: render error #${id}`);
         console.warn(`test-only: render warning #${id}`);
@@ -2071,7 +2411,14 @@ describe('InspectedElementContext', () => {
         );
       });
 
-      store.clearWarningsForElement(2);
+      let id = ((store.getElementIDAtIndex(1): any): number);
+      const rendererID = store.getRendererIDForElement(id);
+
+      const {
+        clearWarningsForElement,
+      } = require('react-devtools-shared/src/backendAPI');
+      clearWarningsForElement({bridge, id, rendererID});
+
       // Flush events to the renderer.
       jest.runOnlyPendingTimers();
 
@@ -2107,7 +2454,9 @@ describe('InspectedElementContext', () => {
         ]
       `);
 
-      store.clearWarningsForElement(1);
+      id = ((store.getElementIDAtIndex(0): any): number);
+      clearWarningsForElement({bridge, id, rendererID});
+
       // Flush events to the renderer.
       jest.runOnlyPendingTimers();
 
@@ -2139,7 +2488,7 @@ describe('InspectedElementContext', () => {
       `);
     });
 
-    it('can be cleared for a particular Fiber (only warnings)', async () => {
+    it('can be cleared for a particular Fiber (only errors)', async () => {
       const Example = ({id}) => {
         console.error(`test-only: render error #${id}`);
         console.warn(`test-only: render warning #${id}`);
@@ -2159,7 +2508,14 @@ describe('InspectedElementContext', () => {
         );
       });
 
-      store.clearErrorsForElement(2);
+      let id = ((store.getElementIDAtIndex(1): any): number);
+      const rendererID = store.getRendererIDForElement(id);
+
+      const {
+        clearErrorsForElement,
+      } = require('react-devtools-shared/src/backendAPI');
+      clearErrorsForElement({bridge, id, rendererID});
+
       // Flush events to the renderer.
       jest.runOnlyPendingTimers();
 
@@ -2195,7 +2551,9 @@ describe('InspectedElementContext', () => {
         ]
       `);
 
-      store.clearErrorsForElement(1);
+      id = ((store.getElementIDAtIndex(0): any): number);
+      clearErrorsForElement({bridge, id, rendererID});
+
       // Flush events to the renderer.
       jest.runOnlyPendingTimers();
 
diff --git a/packages/react-devtools-shared/src/__tests__/inspectedElementSerializer.js b/packages/react-devtools-shared/src/__tests__/inspectedElementSerializer.js
index b6290daf61a95..a615777421fe3 100644
--- a/packages/react-devtools-shared/src/__tests__/inspectedElementSerializer.js
+++ b/packages/react-devtools-shared/src/__tests__/inspectedElementSerializer.js
@@ -12,17 +12,14 @@ export function test(maybeInspectedElement) {
 
 // print() is part of Jest's serializer API
 export function print(inspectedElement, serialize, indent) {
-  return JSON.stringify(
-    {
-      id: inspectedElement.id,
-      owners: inspectedElement.owners,
-      context: inspectedElement.context,
-      events: inspectedElement.events,
-      hooks: inspectedElement.hooks,
-      props: inspectedElement.props,
-      state: inspectedElement.state,
-    },
-    null,
-    2,
-  );
+  // Don't stringify this object; that would break nested serializers.
+  return serialize({
+    context: inspectedElement.context,
+    events: inspectedElement.events,
+    hooks: inspectedElement.hooks,
+    id: inspectedElement.id,
+    owners: inspectedElement.owners,
+    props: inspectedElement.props,
+    state: inspectedElement.state,
+  });
 }
diff --git a/packages/react-devtools-shared/src/__tests__/legacy/__snapshots__/inspectElement-test.js.snap b/packages/react-devtools-shared/src/__tests__/legacy/__snapshots__/inspectElement-test.js.snap
deleted file mode 100644
index 1ed17c109bb12..0000000000000
--- a/packages/react-devtools-shared/src/__tests__/legacy/__snapshots__/inspectElement-test.js.snap
+++ /dev/null
@@ -1,303 +0,0 @@
-// Jest Snapshot v1, https://goo.gl/fbAQLP
-
-exports[`InspectedElementContext should inspect the currently selected element: 1: Initial inspection 1`] = `
-Object {
-  "id": 2,
-  "type": "full-data",
-  "value": {
-  "id": 2,
-  "owners": null,
-  "context": {},
-  "hooks": null,
-  "props": {
-    "a": 1,
-    "b": "abc"
-  },
-  "state": null
-},
-}
-`;
-
-exports[`InspectedElementContext should not consume iterables while inspecting: 1: Initial inspection 1`] = `
-Object {
-  "id": 2,
-  "type": "full-data",
-  "value": {
-  "id": 2,
-  "owners": null,
-  "context": {},
-  "hooks": null,
-  "props": {
-    "iteratable": {}
-  },
-  "state": null
-},
-}
-`;
-
-exports[`InspectedElementContext should not dehydrate nested values until explicitly requested: 1: Initially inspect element 1`] = `
-Object {
-  "id": 2,
-  "type": "full-data",
-  "value": {
-  "id": 2,
-  "owners": null,
-  "context": {},
-  "hooks": null,
-  "props": {
-    "nestedObject": {
-      "a": {}
-    }
-  },
-  "state": null
-},
-}
-`;
-
-exports[`InspectedElementContext should not dehydrate nested values until explicitly requested: 2: Inspect props.nestedObject.a 1`] = `
-Object {
-  "id": 2,
-  "type": "full-data",
-  "value": {
-  "id": 2,
-  "owners": null,
-  "context": {},
-  "hooks": null,
-  "props": {
-    "nestedObject": {
-      "a": {
-        "b": {
-          "c": {}
-        }
-      }
-    }
-  },
-  "state": null
-},
-}
-`;
-
-exports[`InspectedElementContext should not dehydrate nested values until explicitly requested: 3: Inspect props.nestedObject.a.b.c 1`] = `
-Object {
-  "id": 2,
-  "type": "full-data",
-  "value": {
-  "id": 2,
-  "owners": null,
-  "context": {},
-  "hooks": null,
-  "props": {
-    "nestedObject": {
-      "a": {
-        "b": {
-          "c": [
-            {
-              "d": {}
-            }
-          ]
-        }
-      }
-    }
-  },
-  "state": null
-},
-}
-`;
-
-exports[`InspectedElementContext should not dehydrate nested values until explicitly requested: 4: Inspect props.nestedObject.a.b.c.0.d 1`] = `
-Object {
-  "id": 2,
-  "type": "full-data",
-  "value": {
-  "id": 2,
-  "owners": null,
-  "context": {},
-  "hooks": null,
-  "props": {
-    "nestedObject": {
-      "a": {
-        "b": {
-          "c": [
-            {
-              "d": {
-                "e": {}
-              }
-            }
-          ]
-        }
-      }
-    }
-  },
-  "state": null
-},
-}
-`;
-
-exports[`InspectedElementContext should support complex data types: 1: Initial inspection 1`] = `
-Object {
-  "id": 2,
-  "type": "full-data",
-  "value": {
-  "id": 2,
-  "owners": null,
-  "context": {},
-  "hooks": null,
-  "props": {
-    "anonymous_fn": {},
-    "array_buffer": {},
-    "array_of_arrays": [
-      {}
-    ],
-    "big_int": {},
-    "bound_fn": {},
-    "data_view": {},
-    "date": {},
-    "fn": {},
-    "html_element": {},
-    "immutable": {
-      "0": {},
-      "1": {},
-      "2": {}
-    },
-    "map": {
-      "0": {},
-      "1": {}
-    },
-    "map_of_maps": {
-      "0": {},
-      "1": {}
-    },
-    "object_of_objects": {
-      "inner": {}
-    },
-    "react_element": {},
-    "regexp": {},
-    "set": {
-      "0": "abc",
-      "1": 123
-    },
-    "set_of_sets": {
-      "0": {},
-      "1": {}
-    },
-    "symbol": {},
-    "typed_array": {
-      "0": 100,
-      "1": -100,
-      "2": 0
-    }
-  },
-  "state": null
-},
-}
-`;
-
-exports[`InspectedElementContext should support custom objects with enumerable properties and getters: 1: Initial inspection 1`] = `
-Object {
-  "id": 2,
-  "type": "full-data",
-  "value": {
-  "id": 2,
-  "owners": null,
-  "context": {},
-  "hooks": null,
-  "props": {
-    "data": {
-      "_number": 42,
-      "number": 42
-    }
-  },
-  "state": null
-},
-}
-`;
-
-exports[`InspectedElementContext should support objects with no prototype: 1: Initial inspection 1`] = `
-Object {
-  "id": 2,
-  "type": "full-data",
-  "value": {
-  "id": 2,
-  "owners": null,
-  "context": {},
-  "hooks": null,
-  "props": {
-    "object": {
-      "string": "abc",
-      "number": 123,
-      "boolean": true
-    }
-  },
-  "state": null
-},
-}
-`;
-
-exports[`InspectedElementContext should support objects with overridden hasOwnProperty: 1: Initial inspection 1`] = `
-Object {
-  "id": 2,
-  "type": "full-data",
-  "value": {
-  "id": 2,
-  "owners": null,
-  "context": {},
-  "hooks": null,
-  "props": {
-    "object": {
-      "name": "blah",
-      "hasOwnProperty": true
-    }
-  },
-  "state": null
-},
-}
-`;
-
-exports[`InspectedElementContext should support objects with with inherited keys: 1: Initial inspection 1`] = `
-Object {
-  "id": 2,
-  "type": "full-data",
-  "value": {
-  "id": 2,
-  "owners": null,
-  "context": {},
-  "hooks": null,
-  "props": {
-    "data": {
-      "123": 3,
-      "enumerableString": 2,
-      "Symbol(enumerableSymbol)": 3,
-      "enumerableStringBase": 1,
-      "Symbol(enumerableSymbolBase)": 1
-    }
-  },
-  "state": null
-},
-}
-`;
-
-exports[`InspectedElementContext should support simple data types: 1: Initial inspection 1`] = `
-Object {
-  "id": 2,
-  "type": "full-data",
-  "value": {
-  "id": 2,
-  "owners": null,
-  "context": {},
-  "hooks": null,
-  "props": {
-    "boolean_false": false,
-    "boolean_true": true,
-    "infinity": null,
-    "integer_zero": 0,
-    "integer_one": 1,
-    "float": 1.23,
-    "string": "abc",
-    "string_empty": "",
-    "nan": null,
-    "value_null": null
-  },
-  "state": null
-},
-}
-`;
diff --git a/packages/react-devtools-shared/src/__tests__/legacy/inspectElement-test.js b/packages/react-devtools-shared/src/__tests__/legacy/inspectElement-test.js
index 6ccff70c77772..05d2bdcbeb6eb 100644
--- a/packages/react-devtools-shared/src/__tests__/legacy/inspectElement-test.js
+++ b/packages/react-devtools-shared/src/__tests__/legacy/inspectElement-test.js
@@ -7,71 +7,50 @@
  * @flow
  */
 
-import type {InspectedElementPayload} from 'react-devtools-shared/src/backend/types';
-import type {DehydratedData} from 'react-devtools-shared/src/devtools/views/Components/types';
 import type {FrontendBridge} from 'react-devtools-shared/src/bridge';
 import type Store from 'react-devtools-shared/src/devtools/store';
 
 describe('InspectedElementContext', () => {
   let React;
   let ReactDOM;
-  let hydrate;
-  let meta;
   let bridge: FrontendBridge;
   let store: Store;
 
+  let backendAPI;
+
   const act = (callback: Function) => {
     callback();
 
     jest.runAllTimers(); // Flush Bridge operations
   };
 
-  function dehydrateHelper(
-    dehydratedData: DehydratedData | null,
-  ): Object | null {
-    if (dehydratedData !== null) {
-      return hydrate(
-        dehydratedData.data,
-        dehydratedData.cleaned,
-        dehydratedData.unserializable,
-      );
-    } else {
-      return null;
-    }
-  }
-
   async function read(
     id: number,
-    path?: Array<string | number>,
+    inspectedPaths?: Object = {},
   ): Promise<Object> {
-    return new Promise((resolve, reject) => {
-      const rendererID = ((store.getRendererIDForElement(id): any): number);
-
-      const onInspectedElement = (payload: InspectedElementPayload) => {
-        bridge.removeListener('inspectedElement', onInspectedElement);
-
-        if (payload.type === 'full-data' && payload.value !== null) {
-          payload.value.context = dehydrateHelper(payload.value.context);
-          payload.value.props = dehydrateHelper(payload.value.props);
-          payload.value.state = dehydrateHelper(payload.value.state);
-        }
-
-        resolve(payload);
-      };
+    const rendererID = ((store.getRendererIDForElement(id): any): number);
+    const promise = backendAPI
+      .inspectElement({
+        bridge,
+        forceUpdate: true,
+        id,
+        inspectedPaths,
+        rendererID,
+      })
+      .then(data =>
+        backendAPI.convertInspectedElementBackendToFrontend(data.value),
+      );
 
-      bridge.addListener('inspectedElement', onInspectedElement);
-      bridge.send('inspectElement', {id, path, rendererID});
+    jest.runOnlyPendingTimers();
 
-      jest.runOnlyPendingTimers();
-    });
+    return promise;
   }
 
   beforeEach(() => {
     bridge = global.bridge;
     store = global.store;
 
-    hydrate = require('react-devtools-shared/src/hydration').hydrate;
-    meta = require('react-devtools-shared/src/hydration').meta;
+    backendAPI = require('react-devtools-shared/src/backendAPI');
 
     // Redirect all React/ReactDOM requires to the v15 UMD.
     // We use the UMD because Jest doesn't enable us to mock deep imports (e.g. "react/lib/Something").
@@ -94,7 +73,20 @@ describe('InspectedElementContext', () => {
     const id = ((store.getElementIDAtIndex(0): any): number);
     const inspectedElement = await read(id);
 
-    expect(inspectedElement).toMatchSnapshot('1: Initial inspection');
+    expect(inspectedElement).toMatchInlineSnapshot(`
+      Object {
+        "context": Object {},
+        "events": undefined,
+        "hooks": null,
+        "id": 2,
+        "owners": null,
+        "props": Object {
+          "a": 1,
+          "b": "abc",
+        },
+        "state": null,
+      }
+    `);
 
     done();
   });
@@ -124,20 +116,29 @@ describe('InspectedElementContext', () => {
     const id = ((store.getElementIDAtIndex(0): any): number);
     const inspectedElement = await read(id);
 
-    expect(inspectedElement).toMatchSnapshot('1: Initial inspection');
-
-    const {props} = inspectedElement.value;
-    expect(props.boolean_false).toBe(false);
-    expect(props.boolean_true).toBe(true);
-    expect(Number.isFinite(props.infinity)).toBe(false);
-    expect(props.integer_zero).toEqual(0);
-    expect(props.integer_one).toEqual(1);
-    expect(props.float).toEqual(1.23);
-    expect(props.string).toEqual('abc');
-    expect(props.string_empty).toEqual('');
-    expect(props.nan).toBeNaN();
-    expect(props.value_null).toBeNull();
-    expect(props.value_undefined).toBeUndefined();
+    expect(inspectedElement).toMatchInlineSnapshot(`
+      Object {
+        "context": Object {},
+        "events": undefined,
+        "hooks": null,
+        "id": 2,
+        "owners": null,
+        "props": Object {
+          "boolean_false": false,
+          "boolean_true": true,
+          "float": 1.23,
+          "infinity": Infinity,
+          "integer_one": 1,
+          "integer_zero": 0,
+          "nan": NaN,
+          "string": "abc",
+          "string_empty": "",
+          "value_null": null,
+          "value_undefined": undefined,
+        },
+        "state": null,
+      }
+    `);
 
     done();
   });
@@ -211,8 +212,6 @@ describe('InspectedElementContext', () => {
     const id = ((store.getElementIDAtIndex(0): any): number);
     const inspectedElement = await read(id);
 
-    expect(inspectedElement).toMatchSnapshot('1: Initial inspection');
-
     const {
       anonymous_fn,
       array_buffer,
@@ -233,7 +232,9 @@ describe('InspectedElementContext', () => {
       set_of_sets,
       symbol,
       typed_array,
-    } = inspectedElement.value.props;
+    } = inspectedElement.props;
+
+    const {meta} = require('react-devtools-shared/src/hydration');
 
     expect(anonymous_fn[meta.inspectable]).toBe(false);
     expect(anonymous_fn[meta.name]).toBe('function');
@@ -360,12 +361,15 @@ describe('InspectedElementContext', () => {
     const id = ((store.getElementIDAtIndex(0): any): number);
     const inspectedElement = await read(id);
 
-    expect(inspectedElement).toMatchSnapshot('1: Initial inspection');
-    expect(inspectedElement.value.props.object).toEqual({
-      boolean: true,
-      number: 123,
-      string: 'abc',
-    });
+    expect(inspectedElement.props).toMatchInlineSnapshot(`
+      Object {
+        "object": Object {
+          "boolean": true,
+          "number": 123,
+          "string": "abc",
+        },
+      }
+    `);
 
     done();
   });
@@ -388,11 +392,10 @@ describe('InspectedElementContext', () => {
     const id = ((store.getElementIDAtIndex(0): any): number);
     const inspectedElement = await read(id);
 
-    expect(inspectedElement).toMatchSnapshot('1: Initial inspection');
-    expect(inspectedElement.value.props.object).toEqual({
-      name: 'blah',
-      hasOwnProperty: true,
-    });
+    // TRICKY: Don't use toMatchInlineSnapshot() for this test!
+    // Our snapshot serializer relies on hasOwnProperty() for feature detection.
+    expect(inspectedElement.props.object.name).toBe('blah');
+    expect(inspectedElement.props.object.hasOwnProperty).toBe(true);
 
     done();
   });
@@ -417,7 +420,22 @@ describe('InspectedElementContext', () => {
     const id = ((store.getElementIDAtIndex(0): any): number);
     const inspectedElement = await read(id);
 
-    expect(inspectedElement).toMatchSnapshot('1: Initial inspection');
+    expect(inspectedElement).toMatchInlineSnapshot(`
+      Object {
+        "context": Object {},
+        "events": undefined,
+        "hooks": null,
+        "id": 2,
+        "owners": null,
+        "props": Object {
+          "iteratable": Dehydrated {
+            "preview_short": Generator,
+            "preview_long": Generator,
+          },
+        },
+        "state": null,
+      }
+    `);
 
     // Inspecting should not consume the iterable.
     expect(iteratable.next().value).toEqual(1);
@@ -457,7 +475,22 @@ describe('InspectedElementContext', () => {
     const id = ((store.getElementIDAtIndex(0): any): number);
     const inspectedElement = await read(id);
 
-    expect(inspectedElement).toMatchSnapshot('1: Initial inspection');
+    expect(inspectedElement).toMatchInlineSnapshot(`
+      Object {
+        "context": Object {},
+        "events": undefined,
+        "hooks": null,
+        "id": 2,
+        "owners": null,
+        "props": Object {
+          "data": Object {
+            "_number": 42,
+            "number": 42,
+          },
+        },
+        "state": null,
+      }
+    `);
 
     done();
   });
@@ -532,7 +565,25 @@ describe('InspectedElementContext', () => {
     const id = ((store.getElementIDAtIndex(0): any): number);
     const inspectedElement = await read(id);
 
-    expect(inspectedElement).toMatchSnapshot('1: Initial inspection');
+    expect(inspectedElement).toMatchInlineSnapshot(`
+      Object {
+        "context": Object {},
+        "events": undefined,
+        "hooks": null,
+        "id": 2,
+        "owners": null,
+        "props": Object {
+          "data": Object {
+            "123": 3,
+            "Symbol(enumerableSymbol)": 3,
+            "Symbol(enumerableSymbolBase)": 1,
+            "enumerableString": 2,
+            "enumerableStringBase": 1,
+          },
+        },
+        "state": null,
+      }
+    `);
 
     done();
   });
@@ -564,28 +615,75 @@ describe('InspectedElementContext', () => {
     const id = ((store.getElementIDAtIndex(0): any): number);
 
     let inspectedElement = await read(id);
-    expect(inspectedElement).toMatchSnapshot('1: Initially inspect element');
-
-    inspectedElement = await read(id, ['props', 'nestedObject', 'a']);
-    expect(inspectedElement).toMatchSnapshot('2: Inspect props.nestedObject.a');
+    expect(inspectedElement.props).toMatchInlineSnapshot(`
+      Object {
+        "nestedObject": Object {
+          "a": Dehydrated {
+            "preview_short": {…},
+            "preview_long": {b: {…}},
+          },
+        },
+      }
+    `);
+
+    inspectedElement = await read(id, {props: {nestedObject: {a: {}}}});
+    expect(inspectedElement.props).toMatchInlineSnapshot(`
+      Object {
+        "nestedObject": Object {
+          "a": Object {
+            "b": Object {
+              "c": Dehydrated {
+                "preview_short": Array(1),
+                "preview_long": [{…}],
+              },
+            },
+          },
+        },
+      }
+    `);
 
-    inspectedElement = await read(id, ['props', 'nestedObject', 'a', 'b', 'c']);
-    expect(inspectedElement).toMatchSnapshot(
-      '3: Inspect props.nestedObject.a.b.c',
-    );
+    inspectedElement = await read(id, {
+      props: {nestedObject: {a: {b: {c: {}}}}},
+    });
+    expect(inspectedElement.props).toMatchInlineSnapshot(`
+      Object {
+        "nestedObject": Object {
+          "a": Object {
+            "b": Object {
+              "c": Array [
+                Object {
+                  "d": Dehydrated {
+                    "preview_short": {…},
+                    "preview_long": {e: {…}},
+                  },
+                },
+              ],
+            },
+          },
+        },
+      }
+    `);
 
-    inspectedElement = await read(id, [
-      'props',
-      'nestedObject',
-      'a',
-      'b',
-      'c',
-      0,
-      'd',
-    ]);
-    expect(inspectedElement).toMatchSnapshot(
-      '4: Inspect props.nestedObject.a.b.c.0.d',
-    );
+    inspectedElement = await read(id, {
+      props: {nestedObject: {a: {b: {c: {0: {d: {}}}}}}},
+    });
+    expect(inspectedElement.props).toMatchInlineSnapshot(`
+      Object {
+        "nestedObject": Object {
+          "a": Object {
+            "b": Object {
+              "c": Array [
+                Object {
+                  "d": Object {
+                    "e": Object {},
+                  },
+                },
+              ],
+            },
+          },
+        },
+      }
+    `);
 
     done();
   });
@@ -619,28 +717,30 @@ describe('InspectedElementContext', () => {
     spyOn(console, 'log').and.callFake(logSpy);
 
     // Should store the whole value (not just the hydrated parts)
-    bridge.send('storeAsGlobal', {
-      count: 1,
+    backendAPI.storeAsGlobal({
+      bridge,
       id,
       path: ['props', 'nestedObject'],
       rendererID,
     });
+
     jest.runOnlyPendingTimers();
-    expect(logSpy).toHaveBeenCalledWith('$reactTemp1');
-    expect(global.$reactTemp1).toBe(nestedObject);
+    expect(logSpy).toHaveBeenCalledWith('$reactTemp0');
+    expect(global.$reactTemp0).toBe(nestedObject);
 
     logSpy.mockReset();
 
     // Should store the nested property specified (not just the outer value)
-    bridge.send('storeAsGlobal', {
-      count: 2,
+    backendAPI.storeAsGlobal({
+      bridge,
       id,
       path: ['props', 'nestedObject', 'a', 'b'],
       rendererID,
     });
+
     jest.runOnlyPendingTimers();
-    expect(logSpy).toHaveBeenCalledWith('$reactTemp2');
-    expect(global.$reactTemp2).toBe(nestedObject.a.b);
+    expect(logSpy).toHaveBeenCalledWith('$reactTemp1');
+    expect(global.$reactTemp1).toBe(nestedObject.a.b);
   });
 
   it('should enable inspected values to be copied to the clipboard', () => {
@@ -669,11 +769,13 @@ describe('InspectedElementContext', () => {
     const rendererID = ((store.getRendererIDForElement(id): any): number);
 
     // Should copy the whole value (not just the hydrated parts)
-    bridge.send('copyElementPath', {
+    backendAPI.copyInspectedElementPath({
+      bridge,
       id,
       path: ['props', 'nestedObject'],
       rendererID,
     });
+
     jest.runOnlyPendingTimers();
     expect(global.mockClipboardCopy).toHaveBeenCalledTimes(1);
     expect(global.mockClipboardCopy).toHaveBeenCalledWith(
@@ -683,11 +785,13 @@ describe('InspectedElementContext', () => {
     global.mockClipboardCopy.mockReset();
 
     // Should copy the nested property specified (not just the outer value)
-    bridge.send('copyElementPath', {
+    backendAPI.copyInspectedElementPath({
+      bridge,
       id,
       path: ['props', 'nestedObject', 'a', 'b'],
       rendererID,
     });
+
     jest.runOnlyPendingTimers();
     expect(global.mockClipboardCopy).toHaveBeenCalledTimes(1);
     expect(global.mockClipboardCopy).toHaveBeenCalledWith(
@@ -745,7 +849,8 @@ describe('InspectedElementContext', () => {
     const rendererID = ((store.getRendererIDForElement(id): any): number);
 
     // Should copy the whole value (not just the hydrated parts)
-    bridge.send('copyElementPath', {
+    backendAPI.copyInspectedElementPath({
+      bridge,
       id,
       path: ['props'],
       rendererID,
@@ -756,7 +861,8 @@ describe('InspectedElementContext', () => {
     global.mockClipboardCopy.mockReset();
 
     // Should copy the nested property specified (not just the outer value)
-    bridge.send('copyElementPath', {
+    backendAPI.copyInspectedElementPath({
+      bridge,
       id,
       path: ['props', 'bigInt'],
       rendererID,
@@ -770,7 +876,8 @@ describe('InspectedElementContext', () => {
     global.mockClipboardCopy.mockReset();
 
     // Should copy the nested property specified (not just the outer value)
-    bridge.send('copyElementPath', {
+    backendAPI.copyInspectedElementPath({
+      bridge,
       id,
       path: ['props', 'typedArray'],
       rendererID,
diff --git a/packages/react-devtools-shared/src/__tests__/store-test.js b/packages/react-devtools-shared/src/__tests__/store-test.js
index d219f3cba7def..d5b9c3cbc9213 100644
--- a/packages/react-devtools-shared/src/__tests__/store-test.js
+++ b/packages/react-devtools-shared/src/__tests__/store-test.js
@@ -12,12 +12,14 @@ describe('Store', () => {
   let ReactDOM;
   let agent;
   let act;
+  let bridge;
   let getRendererID;
   let store;
   let withErrorsOrWarningsIgnored;
 
   beforeEach(() => {
     agent = global.agent;
+    bridge = global.bridge;
     store = global.store;
 
     React = require('react');
@@ -1159,7 +1161,11 @@ describe('Store', () => {
             <Example> ✕⚠
       `);
 
-      store.clearErrorsAndWarnings();
+      const {
+        clearErrorsAndWarnings,
+      } = require('react-devtools-shared/src/backendAPI');
+      clearErrorsAndWarnings({bridge, store});
+
       // flush events to the renderer
       jest.runAllTimers();
 
@@ -1196,7 +1202,14 @@ describe('Store', () => {
             <Example> ✕⚠
       `);
 
-      store.clearWarningsForElement(2);
+      const id = ((store.getElementIDAtIndex(1): any): number);
+      const rendererID = store.getRendererIDForElement(id);
+
+      const {
+        clearWarningsForElement,
+      } = require('react-devtools-shared/src/backendAPI');
+      clearWarningsForElement({bridge, id, rendererID});
+
       // Flush events to the renderer.
       jest.runAllTimers();
 
@@ -1234,7 +1247,14 @@ describe('Store', () => {
             <Example> ✕⚠
       `);
 
-      store.clearErrorsForElement(2);
+      const id = ((store.getElementIDAtIndex(1): any): number);
+      const rendererID = store.getRendererIDForElement(id);
+
+      const {
+        clearErrorsForElement,
+      } = require('react-devtools-shared/src/backendAPI');
+      clearErrorsForElement({bridge, id, rendererID});
+
       // Flush events to the renderer.
       jest.runAllTimers();
 
diff --git a/packages/react-devtools-shared/src/__tests__/treeContext-test.js b/packages/react-devtools-shared/src/__tests__/treeContext-test.js
index 8c057fb720807..36f615c1155b6 100644
--- a/packages/react-devtools-shared/src/__tests__/treeContext-test.js
+++ b/packages/react-devtools-shared/src/__tests__/treeContext-test.js
@@ -1441,20 +1441,28 @@ describe('TreeListContext', () => {
   });
 
   describe('inline errors/warnings state', () => {
+    const {
+      clearErrorsAndWarnings: clearErrorsAndWarningsAPI,
+      clearErrorsForElement: clearErrorsForElementAPI,
+      clearWarningsForElement: clearWarningsForElementAPI,
+    } = require('react-devtools-shared/src/backendAPI');
+
     function clearAllErrors() {
-      utils.act(() => store.clearErrorsAndWarnings());
+      utils.act(() => clearErrorsAndWarningsAPI({bridge, store}));
       // flush events to the renderer
       jest.runAllTimers();
     }
 
     function clearErrorsForElement(id) {
-      utils.act(() => store.clearErrorsForElement(id));
+      const rendererID = store.getRendererIDForElement(id);
+      utils.act(() => clearErrorsForElementAPI({bridge, id, rendererID}));
       // flush events to the renderer
       jest.runAllTimers();
     }
 
     function clearWarningsForElement(id) {
-      utils.act(() => store.clearWarningsForElement(id));
+      const rendererID = store.getRendererIDForElement(id);
+      utils.act(() => clearWarningsForElementAPI({bridge, id, rendererID}));
       // flush events to the renderer
       jest.runAllTimers();
     }
diff --git a/packages/react-devtools-shared/src/backend/agent.js b/packages/react-devtools-shared/src/backend/agent.js
index 326b0fb2e340f..e570febf5d67e 100644
--- a/packages/react-devtools-shared/src/backend/agent.js
+++ b/packages/react-devtools-shared/src/backend/agent.js
@@ -70,8 +70,10 @@ type CopyElementParams = {|
 
 type InspectElementParams = {|
   id: number,
-  path?: Array<string | number>,
+  inspectedPaths: Object,
+  forceUpdate: boolean,
   rendererID: number,
+  requestID: number,
 |};
 
 type OverrideHookParams = {|
@@ -328,12 +330,21 @@ export default class Agent extends EventEmitter<{|
     }
   };
 
-  inspectElement = ({id, path, rendererID}: InspectElementParams) => {
+  inspectElement = ({
+    id,
+    inspectedPaths,
+    forceUpdate,
+    rendererID,
+    requestID,
+  }: InspectElementParams) => {
     const renderer = this._rendererInterfaces[rendererID];
     if (renderer == null) {
       console.warn(`Invalid renderer id "${rendererID}" for element "${id}"`);
     } else {
-      this._bridge.send('inspectedElement', renderer.inspectElement(id, path));
+      this._bridge.send(
+        'inspectedElement',
+        renderer.inspectElement(requestID, id, inspectedPaths, forceUpdate),
+      );
 
       // When user selects an element, stop trying to restore the selection,
       // and instead remember the current selection for the next reload.
diff --git a/packages/react-devtools-shared/src/backend/legacy/renderer.js b/packages/react-devtools-shared/src/backend/legacy/renderer.js
index 90f0f3ad12b46..63c9b1660e75e 100644
--- a/packages/react-devtools-shared/src/backend/legacy/renderer.js
+++ b/packages/react-devtools-shared/src/backend/legacy/renderer.js
@@ -584,25 +584,12 @@ export function attach(
   }
 
   let currentlyInspectedElementID: number | null = null;
-  let currentlyInspectedPaths: Object = {};
-
-  // Track the intersection of currently inspected paths,
-  // so that we can send their data along if the element is re-rendered.
-  function mergeInspectedPaths(path: Array<string | number>) {
-    let current = currentlyInspectedPaths;
-    path.forEach(key => {
-      if (!current[key]) {
-        current[key] = {};
-      }
-      current = current[key];
-    });
-  }
 
-  function createIsPathAllowed(key: string) {
+  function createIsPathAllowed(key: string, inspectedPaths: Object) {
     // This function helps prevent previously-inspected paths from being dehydrated in updates.
     // This is important to avoid a bad user experience where expanded toggles collapse on update.
     return function isPathAllowed(path: Array<string | number>): boolean {
-      let current = currentlyInspectedPaths[key];
+      let current = inspectedPaths[key];
       if (!current) {
         return false;
       }
@@ -691,26 +678,23 @@ export function attach(
   }
 
   function inspectElement(
+    requestID: number,
     id: number,
-    path?: Array<string | number>,
+    inspectedPaths: Object,
   ): InspectedElementPayload {
     if (currentlyInspectedElementID !== id) {
       currentlyInspectedElementID = id;
-      currentlyInspectedPaths = {};
     }
 
     const inspectedElement = inspectElementRaw(id);
     if (inspectedElement === null) {
       return {
         id,
+        responseID: requestID,
         type: 'not-found',
       };
     }
 
-    if (path != null) {
-      mergeInspectedPaths(path);
-    }
-
     // Any time an inspected element has an update,
     // we should update the selected $r value as wel.
     // Do this before dehyration (cleanForBridge).
@@ -718,19 +702,20 @@ export function attach(
 
     inspectedElement.context = cleanForBridge(
       inspectedElement.context,
-      createIsPathAllowed('context'),
+      createIsPathAllowed('context', inspectedPaths),
     );
     inspectedElement.props = cleanForBridge(
       inspectedElement.props,
-      createIsPathAllowed('props'),
+      createIsPathAllowed('props', inspectedPaths),
     );
     inspectedElement.state = cleanForBridge(
       inspectedElement.state,
-      createIsPathAllowed('state'),
+      createIsPathAllowed('state', inspectedPaths),
     );
 
     return {
       id,
+      responseID: requestID,
       type: 'full-data',
       value: inspectedElement,
     };
diff --git a/packages/react-devtools-shared/src/backend/renderer.js b/packages/react-devtools-shared/src/backend/renderer.js
index b942b7561b7b8..ee90dfee688ac 100644
--- a/packages/react-devtools-shared/src/backend/renderer.js
+++ b/packages/react-devtools-shared/src/backend/renderer.js
@@ -2718,7 +2718,6 @@ export function attach(
 
   let mostRecentlyInspectedElement: InspectedElement | null = null;
   let hasElementUpdatedSinceLastInspected: boolean = false;
-  let currentlyInspectedPaths: Object = {};
 
   function isMostRecentlyInspectedElementCurrent(id: number): boolean {
     return (
@@ -2728,21 +2727,10 @@ export function attach(
     );
   }
 
-  // Track the intersection of currently inspected paths,
-  // so that we can send their data along if the element is re-rendered.
-  function mergeInspectedPaths(path: Array<string | number>) {
-    let current = currentlyInspectedPaths;
-    path.forEach(key => {
-      if (!current[key]) {
-        current[key] = {};
-      }
-      current = current[key];
-    });
-  }
-
   function createIsPathAllowed(
     key: string | null,
     secondaryCategory: 'hooks' | null,
+    inspectedPaths: Object,
   ) {
     // This function helps prevent previously-inspected paths from being dehydrated in updates.
     // This is important to avoid a bad user experience where expanded toggles collapse on update.
@@ -2767,8 +2755,7 @@ export function attach(
           break;
       }
 
-      let current =
-        key === null ? currentlyInspectedPaths : currentlyInspectedPaths[key];
+      let current = key === null ? inspectedPaths : inspectedPaths[key];
       if (!current) {
         return false;
       }
@@ -2863,97 +2850,66 @@ export function attach(
   }
 
   function inspectElement(
+    requestID: number,
     id: number,
-    path?: Array<string | number>,
+    inspectedPaths: Object,
+    forceUpdate: boolean,
   ): InspectedElementPayload {
-    const isCurrent = isMostRecentlyInspectedElementCurrent(id);
+    const isCurrent = !forceUpdate && isMostRecentlyInspectedElementCurrent(id);
 
     if (isCurrent) {
-      if (path != null) {
-        mergeInspectedPaths(path);
-
-        let secondaryCategory = null;
-        if (path[0] === 'hooks') {
-          secondaryCategory = 'hooks';
-        }
-
-        // If this element has not been updated since it was last inspected,
-        // we can just return the subset of data in the newly-inspected path.
-        return {
-          id,
-          type: 'hydrated-path',
-          path,
-          value: cleanForBridge(
-            getInObject(
-              ((mostRecentlyInspectedElement: any): InspectedElement),
-              path,
-            ),
-            createIsPathAllowed(null, secondaryCategory),
-            path,
-          ),
-        };
-      } else {
-        // If this element has not been updated since it was last inspected, we don't need to re-run it.
-        // Instead we can just return the ID to indicate that it has not changed.
-        return {
-          id,
-          type: 'no-change',
-        };
-      }
-    } else {
-      hasElementUpdatedSinceLastInspected = false;
-
-      if (
-        mostRecentlyInspectedElement === null ||
-        mostRecentlyInspectedElement.id !== id
-      ) {
-        currentlyInspectedPaths = {};
-      }
-
-      mostRecentlyInspectedElement = inspectElementRaw(id);
-      if (mostRecentlyInspectedElement === null) {
-        return {
-          id,
-          type: 'not-found',
-        };
-      }
-
-      if (path != null) {
-        mergeInspectedPaths(path);
-      }
-
-      // Any time an inspected element has an update,
-      // we should update the selected $r value as wel.
-      // Do this before dehydration (cleanForBridge).
-      updateSelectedElement(mostRecentlyInspectedElement);
+      // If this element has not been updated since it was last inspected, we don't need to return it.
+      // Instead we can just return the ID to indicate that it has not changed.
+      return {
+        id,
+        responseID: requestID,
+        type: 'no-change',
+      };
+    }
 
-      // Clone before cleaning so that we preserve the full data.
-      // This will enable us to send patches without re-inspecting if hydrated paths are requested.
-      // (Reducing how often we shallow-render is a better DX for function components that use hooks.)
-      const cleanedInspectedElement = {...mostRecentlyInspectedElement};
-      cleanedInspectedElement.context = cleanForBridge(
-        cleanedInspectedElement.context,
-        createIsPathAllowed('context', null),
-      );
-      cleanedInspectedElement.hooks = cleanForBridge(
-        cleanedInspectedElement.hooks,
-        createIsPathAllowed('hooks', 'hooks'),
-      );
-      cleanedInspectedElement.props = cleanForBridge(
-        cleanedInspectedElement.props,
-        createIsPathAllowed('props', null),
-      );
-      cleanedInspectedElement.state = cleanForBridge(
-        cleanedInspectedElement.state,
-        createIsPathAllowed('state', null),
-      );
+    hasElementUpdatedSinceLastInspected = false;
 
+    mostRecentlyInspectedElement = inspectElementRaw(id);
+    if (mostRecentlyInspectedElement === null) {
       return {
         id,
-        type: 'full-data',
-        value: cleanedInspectedElement,
+        responseID: requestID,
+        type: 'not-found',
       };
     }
+
+    // Any time an inspected element has an update,
+    // we should update the selected $r value as wel.
+    // Do this before dehydration (cleanForBridge).
+    updateSelectedElement(mostRecentlyInspectedElement);
+
+    // Clone before cleaning so that we preserve the full data.
+    // This will enable us to send patches without re-inspecting if hydrated paths are requested.
+    // (Reducing how often we shallow-render is a better DX for function components that use hooks.)
+    const cleanedInspectedElement = {...mostRecentlyInspectedElement};
+    cleanedInspectedElement.context = cleanForBridge(
+      cleanedInspectedElement.context,
+      createIsPathAllowed('context', null, inspectedPaths),
+    );
+    cleanedInspectedElement.hooks = cleanForBridge(
+      cleanedInspectedElement.hooks,
+      createIsPathAllowed('hooks', 'hooks', inspectedPaths),
+    );
+    cleanedInspectedElement.props = cleanForBridge(
+      cleanedInspectedElement.props,
+      createIsPathAllowed('props', null, inspectedPaths),
+    );
+    cleanedInspectedElement.state = cleanForBridge(
+      cleanedInspectedElement.state,
+      createIsPathAllowed('state', null, inspectedPaths),
+    );
+
+    return {
+      id,
+      responseID: requestID,
+      type: 'full-data',
+      value: cleanedInspectedElement,
+    };
   }
 
   function logElementToConsole(id) {
diff --git a/packages/react-devtools-shared/src/backend/types.js b/packages/react-devtools-shared/src/backend/types.js
index 4cecf59df2983..09b32aa1ceef3 100644
--- a/packages/react-devtools-shared/src/backend/types.js
+++ b/packages/react-devtools-shared/src/backend/types.js
@@ -257,34 +257,28 @@ export type InspectedElement = {|
 export const InspectElementFullDataType = 'full-data';
 export const InspectElementNoChangeType = 'no-change';
 export const InspectElementNotFoundType = 'not-found';
-export const InspectElementHydratedPathType = 'hydrated-path';
 
 type InspectElementFullData = {|
   id: number,
+  responseID: number,
   type: 'full-data',
   value: InspectedElement,
 |};
 
-type InspectElementHydratedPath = {|
-  id: number,
-  type: 'hydrated-path',
-  path: Array<string | number>,
-  value: any,
-|};
-
 type InspectElementNoChange = {|
   id: number,
+  responseID: number,
   type: 'no-change',
 |};
 
 type InspectElementNotFound = {|
   id: number,
+  responseID: number,
   type: 'not-found',
 |};
 
 export type InspectedElementPayload =
   | InspectElementFullData
-  | InspectElementHydratedPath
   | InspectElementNoChange
   | InspectElementNotFound;
 
@@ -319,8 +313,10 @@ export type RendererInterface = {
   handleCommitFiberRoot: (fiber: Object, commitPriority?: number) => void,
   handleCommitFiberUnmount: (fiber: Object) => void,
   inspectElement: (
+    requestID: number,
     id: number,
-    path?: Array<string | number>,
+    inspectedPaths: Object,
+    forceUpdate: boolean,
   ) => InspectedElementPayload,
   logElementToConsole: (id: number) => void,
   overrideSuspense: (id: number, forceFallback: boolean) => void,
diff --git a/packages/react-devtools-shared/src/backendAPI.js b/packages/react-devtools-shared/src/backendAPI.js
new file mode 100644
index 0000000000000..372fd247d3b5a
--- /dev/null
+++ b/packages/react-devtools-shared/src/backendAPI.js
@@ -0,0 +1,283 @@
+/**
+ * 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 {hydrate, fillInPath} from 'react-devtools-shared/src/hydration';
+import {separateDisplayNameAndHOCs} from 'react-devtools-shared/src/utils';
+import Store from 'react-devtools-shared/src/devtools/store';
+
+import type {
+  InspectedElement as InspectedElementBackend,
+  InspectedElementPayload,
+} from 'react-devtools-shared/src/backend/types';
+import type {
+  BackendEvents,
+  FrontendBridge,
+} from 'react-devtools-shared/src/bridge';
+import type {
+  DehydratedData,
+  InspectedElement as InspectedElementFrontend,
+} from 'react-devtools-shared/src/devtools/views/Components/types';
+
+export function clearErrorsAndWarnings({
+  bridge,
+  store,
+}: {|
+  bridge: FrontendBridge,
+  store: Store,
+|}): void {
+  store.rootIDToRendererID.forEach(rendererID => {
+    bridge.send('clearErrorsAndWarnings', {rendererID});
+  });
+}
+
+export function clearErrorsForElement({
+  bridge,
+  id,
+  rendererID,
+}: {|
+  bridge: FrontendBridge,
+  id: number,
+  rendererID: number,
+|}): void {
+  bridge.send('clearErrorsForFiberID', {
+    rendererID,
+    id,
+  });
+}
+
+export function clearWarningsForElement({
+  bridge,
+  id,
+  rendererID,
+}: {|
+  bridge: FrontendBridge,
+  id: number,
+  rendererID: number,
+|}): void {
+  bridge.send('clearWarningsForFiberID', {
+    rendererID,
+    id,
+  });
+}
+
+export function copyInspectedElementPath({
+  bridge,
+  id,
+  path,
+  rendererID,
+}: {|
+  bridge: FrontendBridge,
+  id: number,
+  path: Array<string | number>,
+  rendererID: number,
+|}): void {
+  bridge.send('copyElementPath', {
+    id,
+    path,
+    rendererID,
+  });
+}
+
+export function inspectElement({
+  bridge,
+  forceUpdate,
+  id,
+  inspectedPaths,
+  rendererID,
+}: {|
+  bridge: FrontendBridge,
+  forceUpdate: boolean,
+  id: number,
+  inspectedPaths: Object,
+  rendererID: number,
+|}): Promise<InspectedElementPayload> {
+  const requestID = requestCounter++;
+  const promise = getPromiseForRequestID<InspectedElementPayload>(
+    requestID,
+    'inspectedElement',
+    bridge,
+  );
+
+  bridge.send('inspectElement', {
+    forceUpdate,
+    id,
+    inspectedPaths,
+    rendererID,
+    requestID,
+  });
+
+  return promise;
+}
+
+let storeAsGlobalCount = 0;
+
+export function storeAsGlobal({
+  bridge,
+  id,
+  path,
+  rendererID,
+}: {|
+  bridge: FrontendBridge,
+  id: number,
+  path: Array<string | number>,
+  rendererID: number,
+|}): void {
+  bridge.send('storeAsGlobal', {
+    count: storeAsGlobalCount++,
+    id,
+    path,
+    rendererID,
+  });
+}
+
+const TIMEOUT_DELAY = 5000;
+
+let requestCounter = 0;
+
+function getPromiseForRequestID<T>(
+  requestID: number,
+  eventType: $Keys<BackendEvents>,
+  bridge: FrontendBridge,
+): Promise<T> {
+  return new Promise((resolve, reject) => {
+    const cleanup = () => {
+      bridge.removeListener(eventType, onInspectedElement);
+
+      clearTimeout(timeoutID);
+    };
+
+    const onInspectedElement = (data: any) => {
+      if (data.responseID === requestID) {
+        cleanup();
+        resolve((data: T));
+      }
+    };
+
+    const onTimeout = () => {
+      cleanup();
+      reject();
+    };
+
+    bridge.addListener(eventType, onInspectedElement);
+
+    const timeoutID = setTimeout(onTimeout, TIMEOUT_DELAY);
+  });
+}
+
+export function cloneInspectedElementWithPath(
+  inspectedElement: InspectedElementFrontend,
+  path: Array<string | number>,
+  value: Object,
+): InspectedElementFrontend {
+  const hydratedValue = hydrateHelper(value, path);
+  const clonedInspectedElement = {...inspectedElement};
+
+  fillInPath(clonedInspectedElement, value, path, hydratedValue);
+
+  return clonedInspectedElement;
+}
+
+export function convertInspectedElementBackendToFrontend(
+  inspectedElementBackend: InspectedElementBackend,
+): InspectedElementFrontend {
+  const {
+    canEditFunctionProps,
+    canEditFunctionPropsDeletePaths,
+    canEditFunctionPropsRenamePaths,
+    canEditHooks,
+    canEditHooksAndDeletePaths,
+    canEditHooksAndRenamePaths,
+    canToggleSuspense,
+    canViewSource,
+    hasLegacyContext,
+    id,
+    source,
+    type,
+    owners,
+    context,
+    hooks,
+    props,
+    rendererPackageName,
+    rendererVersion,
+    rootType,
+    state,
+    key,
+    errors,
+    warnings,
+  } = inspectedElementBackend;
+
+  const inspectedElement: InspectedElementFrontend = {
+    canEditFunctionProps,
+    canEditFunctionPropsDeletePaths,
+    canEditFunctionPropsRenamePaths,
+    canEditHooks,
+    canEditHooksAndDeletePaths,
+    canEditHooksAndRenamePaths,
+    canToggleSuspense,
+    canViewSource,
+    hasLegacyContext,
+    id,
+    key,
+    rendererPackageName,
+    rendererVersion,
+    rootType,
+    source,
+    type,
+    owners:
+      owners === null
+        ? null
+        : owners.map(owner => {
+            const [displayName, hocDisplayNames] = separateDisplayNameAndHOCs(
+              owner.displayName,
+              owner.type,
+            );
+            return {
+              ...owner,
+              displayName,
+              hocDisplayNames,
+            };
+          }),
+    context: hydrateHelper(context),
+    hooks: hydrateHelper(hooks),
+    props: hydrateHelper(props),
+    state: hydrateHelper(state),
+    errors,
+    warnings,
+  };
+
+  return inspectedElement;
+}
+
+function hydrateHelper(
+  dehydratedData: DehydratedData | null,
+  path?: Array<string | number>,
+): Object | null {
+  if (dehydratedData !== null) {
+    const {cleaned, data, unserializable} = dehydratedData;
+
+    if (path) {
+      const {length} = path;
+      if (length > 0) {
+        // Hydration helper requires full paths, but inspection dehydrates with relative paths.
+        // In that event it's important that we adjust the "cleaned" paths to match.
+        return hydrate(
+          data,
+          cleaned.map(cleanedPath => cleanedPath.slice(length)),
+          unserializable.map(unserializablePath =>
+            unserializablePath.slice(length),
+          ),
+        );
+      }
+    }
+
+    return hydrate(data, cleaned, unserializable);
+  } else {
+    return null;
+  }
+}
diff --git a/packages/react-devtools-shared/src/bridge.js b/packages/react-devtools-shared/src/bridge.js
index 1da4d8eb3e549..dad25e93258c1 100644
--- a/packages/react-devtools-shared/src/bridge.js
+++ b/packages/react-devtools-shared/src/bridge.js
@@ -89,7 +89,9 @@ type ViewAttributeSourceParams = {|
 
 type InspectElementParams = {|
   ...ElementAndRendererID,
-  path?: Array<string | number>,
+  forceUpdate: boolean,
+  inspectedPaths: Object,
+  requestID: number,
 |};
 
 type StoreAsGlobalParams = {|
@@ -117,7 +119,7 @@ type UpdateConsolePatchSettingsParams = {|
   showInlineWarningsAndErrors: boolean,
 |};
 
-type BackendEvents = {|
+export type BackendEvents = {|
   extensionBackendInitialized: [],
   inspectedElement: [InspectedElementPayload],
   isBackendStorageAPISupported: [boolean],
diff --git a/packages/react-devtools-shared/src/devtools/cache.js b/packages/react-devtools-shared/src/devtools/cache.js
index af6a02faa0cf2..573f666402717 100644
--- a/packages/react-devtools-shared/src/devtools/cache.js
+++ b/packages/react-devtools-shared/src/devtools/cache.js
@@ -12,6 +12,8 @@ import type {Thenable} from 'shared/ReactTypes';
 import * as React from 'react';
 import {createContext} from 'react';
 
+// TODO (cache) Remove this cache; it is outdated and will not work with newer APIs like startTransition.
+
 // Cache implementation was forked from the React repo:
 // https://github.com/facebook/react/blob/master/packages/react-cache/src/ReactCache.js
 //
diff --git a/packages/react-devtools-shared/src/devtools/store.js b/packages/react-devtools-shared/src/devtools/store.js
index 6b1b83e524838..e5a6ac15fbde1 100644
--- a/packages/react-devtools-shared/src/devtools/store.js
+++ b/packages/react-devtools-shared/src/devtools/store.js
@@ -101,7 +101,7 @@ export default class Store extends EventEmitter<{|
 
   // Map of ID to (mutable) Element.
   // Elements are mutated to avoid excessive cloning during tree updates.
-  // The InspectedElementContext also relies on this mutability for its WeakMap usage.
+  // The InspectedElement Suspense cache also relies on this mutability for its WeakMap usage.
   _idToElement: Map<number, Element> = new Map();
 
   // Should the React Native style editor panel be shown?
@@ -378,42 +378,6 @@ export default class Store extends EventEmitter<{|
     return this._cachedWarningCount;
   }
 
-  clearErrorsAndWarnings(): void {
-    this._rootIDToRendererID.forEach(rendererID => {
-      this._bridge.send('clearErrorsAndWarnings', {
-        rendererID,
-      });
-    });
-  }
-
-  clearErrorsForElement(id: number): void {
-    const rendererID = this.getRendererIDForElement(id);
-    if (rendererID === null) {
-      console.warn(
-        `Unable to find rendererID for element ${id} when clearing errors.`,
-      );
-    } else {
-      this._bridge.send('clearErrorsForFiberID', {
-        rendererID,
-        id,
-      });
-    }
-  }
-
-  clearWarningsForElement(id: number): void {
-    const rendererID = this.getRendererIDForElement(id);
-    if (rendererID === null) {
-      console.warn(
-        `Unable to find rendererID for element ${id} when clearing warnings.`,
-      );
-    } else {
-      this._bridge.send('clearWarningsForFiberID', {
-        rendererID,
-        id,
-      });
-    }
-  }
-
   containsElement(id: number): boolean {
     return this._idToElement.get(id) != null;
   }
diff --git a/packages/react-devtools-shared/src/devtools/views/Components/Components.js b/packages/react-devtools-shared/src/devtools/views/Components/Components.js
index 91e31d4bf0294..712afdd987287 100644
--- a/packages/react-devtools-shared/src/devtools/views/Components/Components.js
+++ b/packages/react-devtools-shared/src/devtools/views/Components/Components.js
@@ -17,7 +17,6 @@ import {
   useRef,
 } from 'react';
 import Tree from './Tree';
-import {InspectedElementContextController} from './InspectedElementContext';
 import {OwnersListContextController} from './OwnersListContext';
 import portaledContent from '../portaledContent';
 import {SettingsModalContextController} from 'react-devtools-shared/src/devtools/views/Settings/SettingsModalContext';
@@ -25,7 +24,9 @@ import {
   localStorageGetItem,
   localStorageSetItem,
 } from 'react-devtools-shared/src/storage';
+import InspectedElementErrorBoundary from './InspectedElementErrorBoundary';
 import InspectedElement from './InspectedElement';
+import {InspectedElementContextController} from './InspectedElementContext';
 import {ModalDialog} from '../ModalDialog';
 import SettingsModal from 'react-devtools-shared/src/devtools/views/Settings/SettingsModal';
 import {NativeStyleContextController} from './NativeStyleEditor/context';
@@ -151,32 +152,34 @@ function Components(_: {||}) {
   return (
     <SettingsModalContextController>
       <OwnersListContextController>
-        <InspectedElementContextController>
-          <div
-            ref={wrapperElementRef}
-            className={styles.Components}
-            onMouseMove={onResize}
-            onMouseLeave={onResizeEnd}
-            onMouseUp={onResizeEnd}>
-            <Fragment>
-              <div ref={resizeElementRef} className={styles.TreeWrapper}>
-                <Tree />
-              </div>
-              <div className={styles.ResizeBarWrapper}>
-                <div onMouseDown={onResizeStart} className={styles.ResizeBar} />
-              </div>
-              <div className={styles.InspectedElementWrapper}>
-                <NativeStyleContextController>
+        <div
+          ref={wrapperElementRef}
+          className={styles.Components}
+          onMouseMove={onResize}
+          onMouseLeave={onResizeEnd}
+          onMouseUp={onResizeEnd}>
+          <Fragment>
+            <div ref={resizeElementRef} className={styles.TreeWrapper}>
+              <Tree />
+            </div>
+            <div className={styles.ResizeBarWrapper}>
+              <div onMouseDown={onResizeStart} className={styles.ResizeBar} />
+            </div>
+            <div className={styles.InspectedElementWrapper}>
+              <NativeStyleContextController>
+                <InspectedElementErrorBoundary>
                   <Suspense fallback={<Loading />}>
-                    <InspectedElement />
+                    <InspectedElementContextController>
+                      <InspectedElement />
+                    </InspectedElementContextController>
                   </Suspense>
-                </NativeStyleContextController>
-              </div>
-              <ModalDialog />
-              <SettingsModal />
-            </Fragment>
-          </div>
-        </InspectedElementContextController>
+                </InspectedElementErrorBoundary>
+              </NativeStyleContextController>
+            </div>
+            <ModalDialog />
+            <SettingsModal />
+          </Fragment>
+        </div>
       </OwnersListContextController>
     </SettingsModalContextController>
   );
diff --git a/packages/react-devtools-shared/src/devtools/views/Components/ExpandCollapseToggle.js b/packages/react-devtools-shared/src/devtools/views/Components/ExpandCollapseToggle.js
index 8950696b7e4fc..1851c75e0391e 100644
--- a/packages/react-devtools-shared/src/devtools/views/Components/ExpandCollapseToggle.js
+++ b/packages/react-devtools-shared/src/devtools/views/Components/ExpandCollapseToggle.js
@@ -14,17 +14,20 @@ import ButtonIcon from '../ButtonIcon';
 import styles from './ExpandCollapseToggle.css';
 
 type ExpandCollapseToggleProps = {|
+  disabled: boolean,
   isOpen: boolean,
   setIsOpen: Function,
 |};
 
 export default function ExpandCollapseToggle({
+  disabled,
   isOpen,
   setIsOpen,
 }: ExpandCollapseToggleProps) {
   return (
     <Button
       className={styles.ExpandCollapseToggle}
+      disabled={disabled}
       onClick={() => setIsOpen(prevIsOpen => !prevIsOpen)}
       title={`${isOpen ? 'Collapse' : 'Expand'} prop value`}>
       <ButtonIcon type={isOpen ? 'expanded' : 'collapsed'} />
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 f0ecc32be3770..3645993d51b12 100644
--- a/packages/react-devtools-shared/src/devtools/views/Components/InspectedElement.js
+++ b/packages/react-devtools-shared/src/devtools/views/Components/InspectedElement.js
@@ -14,16 +14,15 @@ import {BridgeContext, StoreContext} from '../context';
 import Button from '../Button';
 import ButtonIcon from '../ButtonIcon';
 import {ModalDialogContext} from '../ModalDialog';
-import {InspectedElementContext} from './InspectedElementContext';
 import ViewElementSourceContext from './ViewElementSourceContext';
 import Toggle from '../Toggle';
 import {ElementTypeSuspense} from 'react-devtools-shared/src/types';
 import CannotSuspendWarningMessage from './CannotSuspendWarningMessage';
 import InspectedElementView from './InspectedElementView';
+import {InspectedElementContext} from './InspectedElementContext';
 
 import styles from './InspectedElement.css';
 
-import type {InspectedElementContextType} from './InspectedElementContext';
 import type {InspectedElement} from './types';
 
 export type Props = {||};
@@ -38,21 +37,13 @@ export default function InspectedElementWrapper(_: Props) {
   const store = useContext(StoreContext);
   const {dispatch: modalDialogDispatch} = useContext(ModalDialogContext);
 
-  const {
-    copyInspectedElementPath,
-    getInspectedElementPath,
-    getInspectedElement,
-    storeAsGlobal,
-  } = useContext<InspectedElementContextType>(InspectedElementContext);
+  const {inspectedElement} = useContext(InspectedElementContext);
 
   const element =
     inspectedElementID !== null
       ? store.getElementByID(inspectedElementID)
       : null;
 
-  const inspectedElement =
-    inspectedElementID != null ? getInspectedElement(inspectedElementID) : null;
-
   const highlightElement = useCallback(() => {
     if (element !== null && inspectedElementID !== null) {
       const rendererID = store.getRendererIDForElement(inspectedElementID);
@@ -228,11 +219,8 @@ export default function InspectedElementWrapper(_: Props) {
           key={
             inspectedElementID /* Force reset when selected Element changes */
           }
-          copyInspectedElementPath={copyInspectedElementPath}
           element={element}
-          getInspectedElementPath={getInspectedElementPath}
           inspectedElement={inspectedElement}
-          storeAsGlobal={storeAsGlobal}
         />
       )}
     </div>
diff --git a/packages/react-devtools-shared/src/devtools/views/Components/InspectedElementContext.js b/packages/react-devtools-shared/src/devtools/views/Components/InspectedElementContext.js
index 01b05a9fa33a1..48768b57aaac1 100644
--- a/packages/react-devtools-shared/src/devtools/views/Components/InspectedElementContext.js
+++ b/packages/react-devtools-shared/src/devtools/views/Components/InspectedElementContext.js
@@ -10,6 +10,8 @@
 import * as React from 'react';
 import {
   createContext,
+  unstable_startTransition as startTransition,
+  unstable_useCacheRefresh as useCacheRefresh,
   useCallback,
   useContext,
   useEffect,
@@ -17,360 +19,134 @@ import {
   useRef,
   useState,
 } from 'react';
-import {unstable_batchedUpdates as batchedUpdates} from 'react-dom';
-import {createResource} from '../../cache';
-import {BridgeContext, StoreContext} from '../context';
-import {hydrate, fillInPath} from 'react-devtools-shared/src/hydration';
 import {TreeStateContext} from './TreeContext';
-import {separateDisplayNameAndHOCs} from 'react-devtools-shared/src/utils';
+import {BridgeContext, StoreContext} from '../context';
+import {
+  checkForUpdate,
+  inspectElement,
+} from 'react-devtools-shared/src/inspectedElementCache';
 
+import type {ReactNodeList} from 'shared/ReactTypes';
 import type {
-  InspectedElement as InspectedElementBackend,
-  InspectedElementPayload,
-} from 'react-devtools-shared/src/backend/types';
-import type {
-  DehydratedData,
   Element,
-  InspectedElement as InspectedElementFrontend,
+  InspectedElement,
 } from 'react-devtools-shared/src/devtools/views/Components/types';
-import type {Resource, Thenable} from '../../cache';
-
-export type StoreAsGlobal = (id: number, path: Array<string | number>) => void;
 
-export type CopyInspectedElementPath = (
-  id: number,
-  path: Array<string | number>,
-) => void;
+type Path = Array<string | number>;
+type InspectPathFunction = (path: Path) => void;
 
-export type GetInspectedElementPath = (
-  id: number,
-  path: Array<string | number>,
-) => void;
-
-export type GetInspectedElement = (
-  id: number,
-) => InspectedElementFrontend | null;
-
-type RefreshInspectedElement = () => void;
-
-export type InspectedElementContextType = {|
-  copyInspectedElementPath: CopyInspectedElementPath,
-  getInspectedElementPath: GetInspectedElementPath,
-  getInspectedElement: GetInspectedElement,
-  refreshInspectedElement: RefreshInspectedElement,
-  storeAsGlobal: StoreAsGlobal,
+type Context = {|
+  inspectedElement: InspectedElement | null,
+  inspectPaths: InspectPathFunction,
 |};
 
-const InspectedElementContext = createContext<InspectedElementContextType>(
-  ((null: any): InspectedElementContextType),
+export const InspectedElementContext = createContext<Context>(
+  ((null: any): Context),
 );
-InspectedElementContext.displayName = 'InspectedElementContext';
-
-type ResolveFn = (inspectedElement: InspectedElementFrontend) => void;
-type InProgressRequest = {|
-  promise: Thenable<InspectedElementFrontend>,
-  resolveFn: ResolveFn,
-|};
-
-const inProgressRequests: WeakMap<Element, InProgressRequest> = new WeakMap();
-const resource: Resource<
-  Element,
-  Element,
-  InspectedElementFrontend,
-> = createResource(
-  (element: Element) => {
-    const request = inProgressRequests.get(element);
-    if (request != null) {
-      return request.promise;
-    }
-
-    let resolveFn = ((null: any): ResolveFn);
-    const promise = new Promise(resolve => {
-      resolveFn = resolve;
-    });
-
-    inProgressRequests.set(element, {promise, resolveFn});
 
-    return promise;
-  },
-  (element: Element) => element,
-  {useWeakMap: true},
-);
+const POLL_INTERVAL = 1000;
 
-type Props = {|
-  children: React$Node,
+export type Props = {|
+  children: ReactNodeList,
 |};
 
-function InspectedElementContextController({children}: Props) {
+export function InspectedElementContextController({children}: Props) {
+  const {selectedElementID} = useContext(TreeStateContext);
   const bridge = useContext(BridgeContext);
   const store = useContext(StoreContext);
 
-  const storeAsGlobalCount = useRef(1);
-
-  // Ask the backend to store the value at the specified path as a global variable.
-  const storeAsGlobal = useCallback<GetInspectedElementPath>(
-    (id: number, path: Array<string | number>) => {
-      const rendererID = store.getRendererIDForElement(id);
-      if (rendererID !== null) {
-        bridge.send('storeAsGlobal', {
-          count: storeAsGlobalCount.current++,
-          id,
-          path,
-          rendererID,
-        });
-      }
-    },
-    [bridge, store],
-  );
-
-  // Ask the backend to copy the specified path to the clipboard.
-  const copyInspectedElementPath = useCallback<GetInspectedElementPath>(
-    (id: number, path: Array<string | number>) => {
-      const rendererID = store.getRendererIDForElement(id);
-      if (rendererID !== null) {
-        bridge.send('copyElementPath', {id, path, rendererID});
-      }
-    },
-    [bridge, store],
-  );
-
-  // Ask the backend to fill in a "dehydrated" path; this will result in a "inspectedElement".
-  const getInspectedElementPath = useCallback<GetInspectedElementPath>(
-    (id: number, path: Array<string | number>) => {
-      const rendererID = store.getRendererIDForElement(id);
-      if (rendererID !== null) {
-        bridge.send('inspectElement', {id, path, rendererID});
-      }
-    },
-    [bridge, store],
-  );
-
-  const getInspectedElement = useCallback<GetInspectedElement>(
-    (id: number) => {
-      const element = store.getElementByID(id);
-      if (element !== null) {
-        return resource.read(element);
-      } else {
-        return null;
-      }
-    },
-    [store],
-  );
-
-  // It's very important that this context consumes selectedElementID and not inspectedElementID.
-  // Otherwise the effect that sends the "inspect" message across the bridge-
-  // would itself be blocked by the same render that suspends (waiting for the data).
-  const {selectedElementID} = useContext(TreeStateContext);
-
-  const refreshInspectedElement = useCallback<RefreshInspectedElement>(() => {
-    if (selectedElementID !== null) {
-      const rendererID = store.getRendererIDForElement(selectedElementID);
-      if (rendererID !== null) {
-        bridge.send('inspectElement', {id: selectedElementID, rendererID});
-      }
-    }
-  }, [bridge, selectedElementID]);
-
-  const [
-    currentlyInspectedElement,
-    setCurrentlyInspectedElement,
-  ] = useState<InspectedElementFrontend | null>(null);
-
-  // This effect handler invalidates the suspense cache and schedules rendering updates with React.
-  useEffect(() => {
-    const onInspectedElement = (data: InspectedElementPayload) => {
-      const {id} = data;
+  const refresh = useCacheRefresh();
 
-      let element;
+  // Track when insepected paths have changed; we need to force the backend to send an udpate then.
+  const forceUpdateRef = useRef<boolean>(true);
 
-      switch (data.type) {
-        case 'no-change':
-        case 'not-found':
-          // No-op
-          break;
-        case 'hydrated-path':
-          // Merge new data into previous object and invalidate cache
-          element = store.getElementByID(id);
-          if (element !== null) {
-            if (currentlyInspectedElement != null) {
-              const value = hydrateHelper(data.value, data.path);
-              const inspectedElement = {...currentlyInspectedElement};
+  // Track the paths insepected for the currently selected element.
+  const [state, setState] = useState<{|
+    element: Element | null,
+    inspectedPaths: Object,
+  |}>({
+    element: null,
+    inspectedPaths: {},
+  });
 
-              fillInPath(inspectedElement, data.value, data.path, value);
+  const element =
+    selectedElementID !== null ? store.getElementByID(selectedElementID) : null;
 
-              resource.write(element, inspectedElement);
+  const elementHasChanged = element !== null && element !== state.element;
 
-              // Schedule update with React if the currently-selected element has been invalidated.
-              if (id === selectedElementID) {
-                setCurrentlyInspectedElement(inspectedElement);
-              }
-            }
-          }
-          break;
-        case 'full-data':
-          const {
-            canEditFunctionProps,
-            canEditFunctionPropsDeletePaths,
-            canEditFunctionPropsRenamePaths,
-            canEditHooks,
-            canEditHooksAndDeletePaths,
-            canEditHooksAndRenamePaths,
-            canToggleSuspense,
-            canViewSource,
-            hasLegacyContext,
-            source,
-            type,
-            owners,
-            context,
-            hooks,
-            props,
-            rendererPackageName,
-            rendererVersion,
-            rootType,
-            state,
-            key,
-            errors,
-            warnings,
-          } = ((data.value: any): InspectedElementBackend);
-
-          const inspectedElement: InspectedElementFrontend = {
-            canEditFunctionProps,
-            canEditFunctionPropsDeletePaths,
-            canEditFunctionPropsRenamePaths,
-            canEditHooks,
-            canEditHooksAndDeletePaths,
-            canEditHooksAndRenamePaths,
-            canToggleSuspense,
-            canViewSource,
-            hasLegacyContext,
-            id,
-            key,
-            rendererPackageName,
-            rendererVersion,
-            rootType,
-            source,
-            type,
-            owners:
-              owners === null
-                ? null
-                : owners.map(owner => {
-                    const [
-                      displayName,
-                      hocDisplayNames,
-                    ] = separateDisplayNameAndHOCs(
-                      owner.displayName,
-                      owner.type,
-                    );
-                    return {
-                      ...owner,
-                      displayName,
-                      hocDisplayNames,
-                    };
-                  }),
-            context: hydrateHelper(context),
-            hooks: hydrateHelper(hooks),
-            props: hydrateHelper(props),
-            state: hydrateHelper(state),
-            errors,
-            warnings,
-          };
-
-          element = store.getElementByID(id);
-          if (element !== null) {
-            const request = inProgressRequests.get(element);
-            if (request != null) {
-              inProgressRequests.delete(element);
-              batchedUpdates(() => {
-                request.resolveFn(inspectedElement);
-                setCurrentlyInspectedElement(inspectedElement);
-              });
-            } else {
-              resource.write(element, inspectedElement);
+  // Reset the cached inspected paths when a new element is selected.
+  if (elementHasChanged) {
+    setState({
+      element,
+      inspectedPaths: {},
+    });
+  }
 
-              // Schedule update with React if the currently-selected element has been invalidated.
-              if (id === selectedElementID) {
-                setCurrentlyInspectedElement(inspectedElement);
-              }
+  // Don't load a stale element from the backend; it wastes bridge bandwidth.
+  const inspectedElement =
+    !elementHasChanged && element !== null
+      ? inspectElement(
+          element,
+          state.inspectedPaths,
+          forceUpdateRef.current,
+          store,
+          bridge,
+        )
+      : null;
+
+  const inspectPaths: InspectPathFunction = useCallback<InspectPathFunction>(
+    (path: Path) => {
+      startTransition(() => {
+        forceUpdateRef.current = true;
+        setState(prevState => {
+          const cloned = {...prevState};
+          let current = cloned.inspectedPaths;
+          path.forEach(key => {
+            if (!current[key]) {
+              current[key] = {};
             }
-          }
-          break;
-        default:
-          break;
-      }
-    };
-
-    bridge.addListener('inspectedElement', onInspectedElement);
-    return () => bridge.removeListener('inspectedElement', onInspectedElement);
-  }, [bridge, currentlyInspectedElement, selectedElementID, store]);
+            current = current[key];
+          });
+          return cloned;
+        });
+        refresh();
+      });
+    },
+    [setState],
+  );
 
-  // This effect handler polls for updates on the currently selected element.
+  // Force backend update when inspected paths change.
   useEffect(() => {
-    if (selectedElementID === null) {
-      return () => {};
-    }
-
-    const rendererID = store.getRendererIDForElement(selectedElementID);
-
-    let timeoutID: TimeoutID | null = null;
+    forceUpdateRef.current = false;
+  }, [element, state]);
 
-    const sendRequest = () => {
-      timeoutID = null;
-
-      if (rendererID !== null) {
-        bridge.send('inspectElement', {id: selectedElementID, rendererID});
-      }
-    };
-
-    // Send the initial inspection request.
-    // We'll poll for an update in the response handler below.
-    sendRequest();
-
-    const onInspectedElement = (data: InspectedElementPayload) => {
-      // If this is the element we requested, wait a little bit and then ask for another update.
-      if (data.id === selectedElementID) {
-        switch (data.type) {
-          case 'no-change':
-          case 'full-data':
-          case 'hydrated-path':
-            if (timeoutID !== null) {
-              clearTimeout(timeoutID);
-            }
-            timeoutID = setTimeout(sendRequest, 1000);
-            break;
-          default:
-            break;
-        }
-      }
-    };
-
-    bridge.addListener('inspectedElement', onInspectedElement);
-
-    return () => {
-      bridge.removeListener('inspectedElement', onInspectedElement);
-
-      if (timeoutID !== null) {
+  // Periodically poll the selected element for updates.
+  useEffect(() => {
+    if (element !== null) {
+      const inspectedPaths = state.inspectedPaths;
+      const checkForUpdateWrapper = () => {
+        checkForUpdate({bridge, element, inspectedPaths, refresh, store});
+        timeoutID = setTimeout(checkForUpdateWrapper, POLL_INTERVAL);
+      };
+      let timeoutID = setTimeout(checkForUpdateWrapper, POLL_INTERVAL);
+      return () => {
         clearTimeout(timeoutID);
-      }
-    };
-  }, [bridge, selectedElementID, store]);
-
-  const value = useMemo(
+      };
+    }
+  }, [
+    element,
+    // Reset this timer any time the element we're inspecting gets a new response.
+    // No sense to ping right away after e.g. inspecting/hydrating a path.
+    inspectedElement,
+    state,
+  ]);
+
+  const value = useMemo<Context>(
     () => ({
-      copyInspectedElementPath,
-      getInspectedElement,
-      getInspectedElementPath,
-      refreshInspectedElement,
-      storeAsGlobal,
+      inspectedElement,
+      inspectPaths,
     }),
-    // InspectedElement is used to invalidate the cache and schedule an update with React.
-    [
-      copyInspectedElementPath,
-      currentlyInspectedElement,
-      getInspectedElement,
-      getInspectedElementPath,
-      refreshInspectedElement,
-      storeAsGlobal,
-    ],
+    [inspectedElement, inspectPaths],
   );
 
   return (
@@ -379,33 +155,3 @@ function InspectedElementContextController({children}: Props) {
     </InspectedElementContext.Provider>
   );
 }
-
-function hydrateHelper(
-  dehydratedData: DehydratedData | null,
-  path?: Array<string | number>,
-): Object | null {
-  if (dehydratedData !== null) {
-    const {cleaned, data, unserializable} = dehydratedData;
-
-    if (path) {
-      const {length} = path;
-      if (length > 0) {
-        // Hydration helper requires full paths, but inspection dehydrates with relative paths.
-        // In that event it's important that we adjust the "cleaned" paths to match.
-        return hydrate(
-          data,
-          cleaned.map(cleanedPath => cleanedPath.slice(length)),
-          unserializable.map(unserializablePath =>
-            unserializablePath.slice(length),
-          ),
-        );
-      }
-    }
-
-    return hydrate(data, cleaned, unserializable);
-  } else {
-    return null;
-  }
-}
-
-export {InspectedElementContext, InspectedElementContextController};
diff --git a/packages/react-devtools-shared/src/devtools/views/Components/InspectedElementContextTree.js b/packages/react-devtools-shared/src/devtools/views/Components/InspectedElementContextTree.js
index 22347cef07f63..8dfe6310a3609 100644
--- a/packages/react-devtools-shared/src/devtools/views/Components/InspectedElementContextTree.js
+++ b/packages/react-devtools-shared/src/devtools/views/Components/InspectedElementContextTree.js
@@ -20,20 +20,20 @@ import {
   ElementTypeFunction,
 } from 'react-devtools-shared/src/types';
 
-import type {GetInspectedElementPath} from './InspectedElementContext';
 import type {InspectedElement} from './types';
 import type {FrontendBridge} from 'react-devtools-shared/src/bridge';
+import type {Element} from 'react-devtools-shared/src/devtools/views/Components/types';
 
 type Props = {|
   bridge: FrontendBridge,
-  getInspectedElementPath: GetInspectedElementPath,
+  element: Element,
   inspectedElement: InspectedElement,
   store: Store,
 |};
 
 export default function InspectedElementContextTree({
   bridge,
-  getInspectedElementPath,
+  element,
   inspectedElement,
   store,
 }: Props) {
@@ -81,15 +81,15 @@ export default function InspectedElementContextTree({
               canEditValues={!isReadOnly}
               canRenamePaths={!isReadOnly}
               canRenamePathsAtDepth={canRenamePathsAtDepth}
-              type="context"
               depth={1}
-              getInspectedElementPath={getInspectedElementPath}
+              element={element}
               hidden={false}
               inspectedElement={inspectedElement}
               name={name}
               path={[name]}
               pathRoot="context"
               store={store}
+              type="context"
               value={value}
             />
           ))}
diff --git a/packages/react-devtools-shared/src/devtools/views/Components/InspectedElementErrorBoundary.css b/packages/react-devtools-shared/src/devtools/views/Components/InspectedElementErrorBoundary.css
new file mode 100644
index 0000000000000..399f83cec45b0
--- /dev/null
+++ b/packages/react-devtools-shared/src/devtools/views/Components/InspectedElementErrorBoundary.css
@@ -0,0 +1,21 @@
+.Error {
+  justify-content: center;
+  align-items: center;
+  display: flex;
+      flex-direction: column;
+  height: 100%;
+  font-size: var(--font-size-sans-large);
+    font-weight: bold;
+    text-align: center;
+  background-color: var(--color-error-background);
+  color: var(--color-error-text);
+  border: 1px solid var(--color-error-border);
+  padding: 1rem;
+}
+
+.Message {
+  margin-bottom: 1rem;
+}
+
+.RetryButton {
+}
\ No newline at end of file
diff --git a/packages/react-devtools-shared/src/devtools/views/Components/InspectedElementErrorBoundary.js b/packages/react-devtools-shared/src/devtools/views/Components/InspectedElementErrorBoundary.js
new file mode 100644
index 0000000000000..09c11664dcf77
--- /dev/null
+++ b/packages/react-devtools-shared/src/devtools/views/Components/InspectedElementErrorBoundary.js
@@ -0,0 +1,93 @@
+/**
+ * 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 {Component, useContext} from 'react';
+import {TreeDispatcherContext} from './TreeContext';
+import Button from 'react-devtools-shared/src/devtools/views/Button';
+import styles from './InspectedElementErrorBoundary.css';
+
+import type {DispatcherContext} from './InspectedElementErrorBoundary.css';
+
+type WrapperProps = {|
+  children: React$Node,
+|};
+
+export default function InspectedElementErrorBoundaryWrapper({
+  children,
+}: WrapperProps) {
+  const dispatch = useContext(TreeDispatcherContext);
+
+  return (
+    <InspectedElementErrorBoundary children={children} dispatch={dispatch} />
+  );
+}
+
+type Props = {|
+  children: React$Node,
+  dispatch: DispatcherContext,
+|};
+
+type State = {|
+  errorMessage: string | null,
+  hasError: boolean,
+|};
+
+const InitialState: State = {
+  errorMessage: null,
+  hasError: false,
+};
+
+class InspectedElementErrorBoundary extends Component<Props, State> {
+  state: State = InitialState;
+
+  static getDerivedStateFromError(error: any) {
+    const errorMessage =
+      typeof error === 'object' &&
+      error !== null &&
+      error.hasOwnProperty('message')
+        ? error.message
+        : error;
+
+    return {
+      errorMessage,
+      hasError: true,
+    };
+  }
+
+  render() {
+    const {children} = this.props;
+    const {errorMessage, hasError} = this.state;
+
+    if (hasError) {
+      return (
+        <div className={styles.Error}>
+          <div className={styles.Message}>{errorMessage || 'Error'}</div>
+          <Button className={styles.RetryButton} onClick={this._retry}>
+            Dismiss
+          </Button>
+        </div>
+      );
+    }
+
+    return children;
+  }
+
+  _retry = () => {
+    const {dispatch} = this.props;
+    dispatch({
+      type: 'SELECT_ELEMENT_BY_ID',
+      payload: null,
+    });
+    this.setState({
+      errorMessage: null,
+      hasError: false,
+    });
+  };
+}
diff --git a/packages/react-devtools-shared/src/devtools/views/Components/InspectedElementErrorsAndWarningsTree.js b/packages/react-devtools-shared/src/devtools/views/Components/InspectedElementErrorsAndWarningsTree.js
index 1f161c98836b6..3b7aaca3216d5 100644
--- a/packages/react-devtools-shared/src/devtools/views/Components/InspectedElementErrorsAndWarningsTree.js
+++ b/packages/react-devtools-shared/src/devtools/views/Components/InspectedElementErrorsAndWarningsTree.js
@@ -8,14 +8,21 @@
  */
 
 import * as React from 'react';
-import {useContext} from 'react';
+import {
+  useContext,
+  unstable_useCacheRefresh as useCacheRefresh,
+  unstable_useTransition as useTransition,
+} from 'react';
 import Button from '../Button';
 import ButtonIcon from '../ButtonIcon';
 import Store from '../../store';
 import sharedStyles from './InspectedElementSharedStyles.css';
 import styles from './InspectedElementErrorsAndWarningsTree.css';
 import {SettingsContext} from '../Settings/SettingsContext';
-import {InspectedElementContext} from './InspectedElementContext';
+import {
+  clearErrorsForElement as clearErrorsForElementAPI,
+  clearWarningsForElement as clearWarningsForElementAPI,
+} from 'react-devtools-shared/src/backendAPI';
 
 import type {InspectedElement} from './types';
 import type {FrontendBridge} from 'react-devtools-shared/src/bridge';
@@ -31,7 +38,45 @@ export default function InspectedElementErrorsAndWarningsTree({
   inspectedElement,
   store,
 }: Props) {
-  const {refreshInspectedElement} = useContext(InspectedElementContext);
+  const refresh = useCacheRefresh();
+
+  const [
+    startClearErrorsTransition,
+    isErrorsTransitionPending,
+  ] = useTransition();
+  const clearErrorsForInspectedElement = () => {
+    const {id} = inspectedElement;
+    const rendererID = store.getRendererIDForElement(id);
+    if (rendererID !== null) {
+      startClearErrorsTransition(() => {
+        clearErrorsForElementAPI({
+          bridge,
+          id,
+          rendererID,
+        });
+        refresh();
+      });
+    }
+  };
+
+  const [
+    startClearWarningsTransition,
+    isWarningsTransitionPending,
+  ] = useTransition();
+  const clearWarningsForInspectedElement = () => {
+    const {id} = inspectedElement;
+    const rendererID = store.getRendererIDForElement(id);
+    if (rendererID !== null) {
+      startClearWarningsTransition(() => {
+        clearWarningsForElementAPI({
+          bridge,
+          id,
+          rendererID,
+        });
+        refresh();
+      });
+    }
+  };
 
   const {showInlineWarningsAndErrors} = useContext(SettingsContext);
   if (!showInlineWarningsAndErrors) {
@@ -40,26 +85,6 @@ export default function InspectedElementErrorsAndWarningsTree({
 
   const {errors, warnings} = inspectedElement;
 
-  const clearErrors = () => {
-    const {id} = inspectedElement;
-    store.clearErrorsForElement(id);
-
-    // Immediately poll for updated data.
-    // This avoids a delay between clicking the clear button and refreshing errors.
-    // Ideally this would be done with useTranstion but that requires updating to a newer Cache strategy.
-    refreshInspectedElement();
-  };
-
-  const clearWarnings = () => {
-    const {id} = inspectedElement;
-    store.clearWarningsForElement(id);
-
-    // Immediately poll for updated data.
-    // This avoids a delay between clicking the clear button and refreshing warnings.
-    // Ideally this would be done with useTranstion but that requires updating to a newer Cache strategy.
-    refreshInspectedElement();
-  };
-
   return (
     <React.Fragment>
       {errors.length > 0 && (
@@ -67,8 +92,9 @@ export default function InspectedElementErrorsAndWarningsTree({
           badgeClassName={styles.ErrorBadge}
           bridge={bridge}
           className={styles.ErrorTree}
-          clearMessages={clearErrors}
+          clearMessages={clearErrorsForInspectedElement}
           entries={errors}
+          isTransitionPending={isErrorsTransitionPending}
           label="errors"
           messageClassName={styles.Error}
         />
@@ -78,8 +104,9 @@ export default function InspectedElementErrorsAndWarningsTree({
           badgeClassName={styles.WarningBadge}
           bridge={bridge}
           className={styles.WarningTree}
-          clearMessages={clearWarnings}
+          clearMessages={clearWarningsForInspectedElement}
           entries={warnings}
+          isTransitionPending={isWarningsTransitionPending}
           label="warnings"
           messageClassName={styles.Warning}
         />
@@ -94,6 +121,7 @@ type TreeProps = {|
   className: string,
   clearMessages: () => {},
   entries: Array<[string, number]>,
+  isTransitionPending: boolean,
   label: string,
   messageClassName: string,
 |};
@@ -104,6 +132,7 @@ function Tree({
   className,
   clearMessages,
   entries,
+  isTransitionPending,
   label,
   messageClassName,
 }: TreeProps) {
@@ -115,6 +144,7 @@ function Tree({
       <div className={`${sharedStyles.HeaderRow} ${styles.HeaderRow}`}>
         <div className={sharedStyles.Header}>{label}</div>
         <Button
+          disabled={isTransitionPending}
           onClick={clearMessages}
           title={`Clear all ${label} for this component`}>
           <ButtonIcon type="clear" />
diff --git a/packages/react-devtools-shared/src/devtools/views/Components/InspectedElementHooksTree.js b/packages/react-devtools-shared/src/devtools/views/Components/InspectedElementHooksTree.js
index 3bb939b6d74e3..4303f8e406ad5 100644
--- a/packages/react-devtools-shared/src/devtools/views/Components/InspectedElementHooksTree.js
+++ b/packages/react-devtools-shared/src/devtools/views/Components/InspectedElementHooksTree.js
@@ -22,20 +22,20 @@ import useContextMenu from '../../ContextMenu/useContextMenu';
 import {meta} from '../../../hydration';
 
 import type {InspectedElement} from './types';
-import type {GetInspectedElementPath} from './InspectedElementContext';
 import type {HooksNode, HooksTree} from 'react-debug-tools/src/ReactDebugHooks';
 import type {FrontendBridge} from 'react-devtools-shared/src/bridge';
+import type {Element} from 'react-devtools-shared/src/devtools/views/Components/types';
 
 type HooksTreeViewProps = {|
   bridge: FrontendBridge,
-  getInspectedElementPath: GetInspectedElementPath,
+  element: Element,
   inspectedElement: InspectedElement,
   store: Store,
 |};
 
 export function InspectedElementHooksTree({
   bridge,
-  getInspectedElementPath,
+  element,
   inspectedElement,
   store,
 }: HooksTreeViewProps) {
@@ -57,7 +57,7 @@ export function InspectedElementHooksTree({
         <InnerHooksTreeView
           hooks={hooks}
           id={id}
-          getInspectedElementPath={getInspectedElementPath}
+          element={element}
           inspectedElement={inspectedElement}
           path={[]}
         />
@@ -67,7 +67,7 @@ export function InspectedElementHooksTree({
 }
 
 type InnerHooksTreeViewProps = {|
-  getInspectedElementPath: GetInspectedElementPath,
+  element: Element,
   hooks: HooksTree,
   id: number,
   inspectedElement: InspectedElement,
@@ -75,7 +75,7 @@ type InnerHooksTreeViewProps = {|
 |};
 
 export function InnerHooksTreeView({
-  getInspectedElementPath,
+  element,
   hooks,
   id,
   inspectedElement,
@@ -85,7 +85,7 @@ export function InnerHooksTreeView({
   return hooks.map((hook, index) => (
     <HookView
       key={index}
-      getInspectedElementPath={getInspectedElementPath}
+      element={element}
       hook={hooks[index]}
       id={id}
       inspectedElement={inspectedElement}
@@ -95,20 +95,14 @@ export function InnerHooksTreeView({
 }
 
 type HookViewProps = {|
-  getInspectedElementPath: GetInspectedElementPath,
+  element: Element,
   hook: HooksNode,
   id: number,
   inspectedElement: InspectedElement,
   path: Array<string | number>,
 |};
 
-function HookView({
-  getInspectedElementPath,
-  hook,
-  id,
-  inspectedElement,
-  path,
-}: HookViewProps) {
+function HookView({element, hook, id, inspectedElement, path}: HookViewProps) {
   const {
     canEditHooks,
     canEditHooksAndDeletePaths,
@@ -195,7 +189,7 @@ function HookView({
   if (isCustomHook) {
     const subHooksView = Array.isArray(subHooks) ? (
       <InnerHooksTreeView
-        getInspectedElementPath={getInspectedElementPath}
+        element={element}
         hooks={subHooks}
         id={id}
         inspectedElement={inspectedElement}
@@ -210,7 +204,7 @@ function HookView({
         canRenamePaths={canRenamePaths}
         canRenamePathsAtDepth={canRenamePathsAtDepth}
         depth={1}
-        getInspectedElementPath={getInspectedElementPath}
+        element={element}
         hookID={hookID}
         inspectedElement={inspectedElement}
         name="subHooks"
@@ -244,7 +238,7 @@ function HookView({
               canRenamePaths={canRenamePaths}
               canRenamePathsAtDepth={canRenamePathsAtDepth}
               depth={1}
-              getInspectedElementPath={getInspectedElementPath}
+              element={element}
               hookID={hookID}
               inspectedElement={inspectedElement}
               name="DebugValue"
@@ -290,7 +284,7 @@ function HookView({
             canRenamePaths={canRenamePaths}
             canRenamePathsAtDepth={canRenamePathsAtDepth}
             depth={1}
-            getInspectedElementPath={getInspectedElementPath}
+            element={element}
             hookID={hookID}
             inspectedElement={inspectedElement}
             name={name}
@@ -311,7 +305,7 @@ function HookView({
             canEditValues={canEditValues}
             canRenamePaths={false}
             depth={1}
-            getInspectedElementPath={getInspectedElementPath}
+            element={element}
             hookID={hookID}
             inspectedElement={inspectedElement}
             name={name}
diff --git a/packages/react-devtools-shared/src/devtools/views/Components/InspectedElementPropsTree.js b/packages/react-devtools-shared/src/devtools/views/Components/InspectedElementPropsTree.js
index a82389aa58d91..7b6569cd32ce5 100644
--- a/packages/react-devtools-shared/src/devtools/views/Components/InspectedElementPropsTree.js
+++ b/packages/react-devtools-shared/src/devtools/views/Components/InspectedElementPropsTree.js
@@ -19,19 +19,19 @@ import styles from './InspectedElementSharedStyles.css';
 import {ElementTypeClass} from 'react-devtools-shared/src/types';
 
 import type {InspectedElement} from './types';
-import type {GetInspectedElementPath} from './InspectedElementContext';
 import type {FrontendBridge} from 'react-devtools-shared/src/bridge';
+import type {Element} from 'react-devtools-shared/src/devtools/views/Components/types';
 
 type Props = {|
   bridge: FrontendBridge,
-  getInspectedElementPath: GetInspectedElementPath,
+  element: Element,
   inspectedElement: InspectedElement,
   store: Store,
 |};
 
 export default function InspectedElementPropsTree({
   bridge,
-  getInspectedElementPath,
+  element,
   inspectedElement,
   store,
 }: Props) {
@@ -78,7 +78,7 @@ export default function InspectedElementPropsTree({
             canEditValues={canEditValues}
             canRenamePaths={canRenamePaths}
             depth={1}
-            getInspectedElementPath={getInspectedElementPath}
+            element={element}
             hidden={false}
             inspectedElement={inspectedElement}
             name={name}
diff --git a/packages/react-devtools-shared/src/devtools/views/Components/InspectedElementStateTree.js b/packages/react-devtools-shared/src/devtools/views/Components/InspectedElementStateTree.js
index 6d8e5ff740cf8..a8658fa3df807 100644
--- a/packages/react-devtools-shared/src/devtools/views/Components/InspectedElementStateTree.js
+++ b/packages/react-devtools-shared/src/devtools/views/Components/InspectedElementStateTree.js
@@ -17,19 +17,19 @@ import Store from '../../store';
 import styles from './InspectedElementSharedStyles.css';
 
 import type {InspectedElement} from './types';
-import type {GetInspectedElementPath} from './InspectedElementContext';
 import type {FrontendBridge} from 'react-devtools-shared/src/bridge';
+import type {Element} from 'react-devtools-shared/src/devtools/views/Components/types';
 
 type Props = {|
   bridge: FrontendBridge,
-  getInspectedElementPath: GetInspectedElementPath,
+  element: Element,
   inspectedElement: InspectedElement,
   store: Store,
 |};
 
 export default function InspectedElementStateTree({
   bridge,
-  getInspectedElementPath,
+  element,
   inspectedElement,
   store,
 }: Props) {
@@ -68,7 +68,7 @@ export default function InspectedElementStateTree({
               canEditValues={true}
               canRenamePaths={true}
               depth={1}
-              getInspectedElementPath={getInspectedElementPath}
+              element={element}
               hidden={false}
               inspectedElement={inspectedElement}
               name={name}
diff --git a/packages/react-devtools-shared/src/devtools/views/Components/InspectedElementView.js b/packages/react-devtools-shared/src/devtools/views/Components/InspectedElementView.js
index a1c77b9b37b18..4d8662d80dae5 100644
--- a/packages/react-devtools-shared/src/devtools/views/Components/InspectedElementView.js
+++ b/packages/react-devtools-shared/src/devtools/views/Components/InspectedElementView.js
@@ -27,15 +27,14 @@ import InspectedElementSuspenseToggle from './InspectedElementSuspenseToggle';
 import NativeStyleEditor from './NativeStyleEditor';
 import Badge from './Badge';
 import {useHighlightNativeElement} from '../hooks';
+import {
+  copyInspectedElementPath as copyInspectedElementPathAPI,
+  storeAsGlobal as storeAsGlobalAPI,
+} from 'react-devtools-shared/src/backendAPI';
 
 import styles from './InspectedElementView.css';
 
 import type {ContextMenuContextType} from '../context';
-import type {
-  CopyInspectedElementPath,
-  GetInspectedElementPath,
-  StoreAsGlobal,
-} from './InspectedElementContext';
 import type {Element, InspectedElement, Owner} from './types';
 import type {ElementType} from 'react-devtools-shared/src/types';
 
@@ -43,19 +42,13 @@ export type CopyPath = (path: Array<string | number>) => void;
 export type InspectPath = (path: Array<string | number>) => void;
 
 type Props = {|
-  copyInspectedElementPath: CopyInspectedElementPath,
   element: Element,
-  getInspectedElementPath: GetInspectedElementPath,
   inspectedElement: InspectedElement,
-  storeAsGlobal: StoreAsGlobal,
 |};
 
 export default function InspectedElementView({
-  copyInspectedElementPath,
   element,
-  getInspectedElementPath,
   inspectedElement,
-  storeAsGlobal,
 }: Props) {
   const {id} = element;
   const {
@@ -89,7 +82,7 @@ export default function InspectedElementView({
 
         <InspectedElementPropsTree
           bridge={bridge}
-          getInspectedElementPath={getInspectedElementPath}
+          element={element}
           inspectedElement={inspectedElement}
           store={store}
         />
@@ -102,28 +95,28 @@ export default function InspectedElementView({
 
         <InspectedElementStateTree
           bridge={bridge}
-          getInspectedElementPath={getInspectedElementPath}
+          element={element}
           inspectedElement={inspectedElement}
           store={store}
         />
 
         <InspectedElementHooksTree
           bridge={bridge}
-          getInspectedElementPath={getInspectedElementPath}
+          element={element}
           inspectedElement={inspectedElement}
           store={store}
         />
 
         <InspectedElementContextTree
           bridge={bridge}
-          getInspectedElementPath={getInspectedElementPath}
+          element={element}
           inspectedElement={inspectedElement}
           store={store}
         />
 
         <InspectedElementErrorsAndWarningsTree
           bridge={bridge}
-          getInspectedElementPath={getInspectedElementPath}
+          element={element}
           inspectedElement={inspectedElement}
           store={store}
         />
@@ -160,34 +153,60 @@ export default function InspectedElementView({
 
       {isContextMenuEnabledForInspectedElement && (
         <ContextMenu id="InspectedElement">
-          {data => (
-            <Fragment>
-              <ContextMenuItem
-                onClick={() => copyInspectedElementPath(id, data.path)}
-                title="Copy value to clipboard">
-                <Icon className={styles.ContextMenuIcon} type="copy" /> Copy
-                value to clipboard
-              </ContextMenuItem>
-              <ContextMenuItem
-                onClick={() => storeAsGlobal(id, data.path)}
-                title="Store as global variable">
-                <Icon
-                  className={styles.ContextMenuIcon}
-                  type="store-as-global-variable"
-                />{' '}
-                Store as global variable
-              </ContextMenuItem>
-              {viewAttributeSourceFunction !== null &&
-                data.type === 'function' && (
-                  <ContextMenuItem
-                    onClick={() => viewAttributeSourceFunction(id, data.path)}
-                    title="Go to definition">
-                    <Icon className={styles.ContextMenuIcon} type="code" /> Go
-                    to definition
-                  </ContextMenuItem>
-                )}
-            </Fragment>
-          )}
+          {({path, type: pathType}) => {
+            const copyInspectedElementPath = () => {
+              const rendererID = store.getRendererIDForElement(id);
+              if (rendererID !== null) {
+                copyInspectedElementPathAPI({
+                  bridge,
+                  id,
+                  path,
+                  rendererID,
+                });
+              }
+            };
+
+            const storeAsGlobal = () => {
+              const rendererID = store.getRendererIDForElement(id);
+              if (rendererID !== null) {
+                storeAsGlobalAPI({
+                  bridge,
+                  id,
+                  path,
+                  rendererID,
+                });
+              }
+            };
+
+            return (
+              <Fragment>
+                <ContextMenuItem
+                  onClick={copyInspectedElementPath}
+                  title="Copy value to clipboard">
+                  <Icon className={styles.ContextMenuIcon} type="copy" /> Copy
+                  value to clipboard
+                </ContextMenuItem>
+                <ContextMenuItem
+                  onClick={storeAsGlobal}
+                  title="Store as global variable">
+                  <Icon
+                    className={styles.ContextMenuIcon}
+                    type="store-as-global-variable"
+                  />{' '}
+                  Store as global variable
+                </ContextMenuItem>
+                {viewAttributeSourceFunction !== null &&
+                  pathType === 'function' && (
+                    <ContextMenuItem
+                      onClick={() => viewAttributeSourceFunction(id, path)}
+                      title="Go to definition">
+                      <Icon className={styles.ContextMenuIcon} type="code" /> Go
+                      to definition
+                    </ContextMenuItem>
+                  )}
+              </Fragment>
+            );
+          }}
         </ContextMenu>
       )}
     </Fragment>
diff --git a/packages/react-devtools-shared/src/devtools/views/Components/KeyValue.css b/packages/react-devtools-shared/src/devtools/views/Components/KeyValue.css
index 4add9729d833a..dec3e2df96178 100644
--- a/packages/react-devtools-shared/src/devtools/views/Components/KeyValue.css
+++ b/packages/react-devtools-shared/src/devtools/views/Components/KeyValue.css
@@ -2,6 +2,10 @@
   display: flex;
 }
 
+.DisabledItem {
+  opacity: 0.5;
+}
+
 .Name {
   color: var(--color-attribute-name-not-editable);
   flex: 0 0 auto;
diff --git a/packages/react-devtools-shared/src/devtools/views/Components/KeyValue.js b/packages/react-devtools-shared/src/devtools/views/Components/KeyValue.js
index 539ba467cde8d..aa5b1bc56eec4 100644
--- a/packages/react-devtools-shared/src/devtools/views/Components/KeyValue.js
+++ b/packages/react-devtools-shared/src/devtools/views/Components/KeyValue.js
@@ -8,11 +8,17 @@
  */
 
 import * as React from 'react';
-import {useEffect, useRef, useState} from 'react';
+import {
+  unstable_useTransition as useTransition,
+  useContext,
+  useRef,
+  useState,
+} from 'react';
 import EditableName from './EditableName';
 import EditableValue from './EditableValue';
 import NewArrayValue from './NewArrayValue';
 import NewKeyValue from './NewKeyValue';
+import LoadingAnimation from './LoadingAnimation';
 import ExpandCollapseToggle from './ExpandCollapseToggle';
 import {alphaSortEntries, getMetaValueLabel} from '../utils';
 import {meta} from '../../../hydration';
@@ -22,11 +28,12 @@ import {parseHookPathForEdit} from './utils';
 import styles from './KeyValue.css';
 import Button from 'react-devtools-shared/src/devtools/views/Button';
 import ButtonIcon from 'react-devtools-shared/src/devtools/views/ButtonIcon';
+import {InspectedElementContext} from './InspectedElementContext';
 
 import type {InspectedElement} from './types';
-import type {Element} from 'react';
+import type {Element} from 'react-devtools-shared/src/devtools/views/Components/types';
+import type {Element as ReactElement} from 'react';
 import type {FrontendBridge} from 'react-devtools-shared/src/bridge';
-import type {GetInspectedElementPath} from './InspectedElementContext';
 
 type Type = 'props' | 'state' | 'context' | 'hooks';
 
@@ -38,9 +45,9 @@ type KeyValueProps = {|
   canRenamePaths: boolean,
   canRenamePathsAtDepth?: (depth: number) => boolean,
   depth: number,
+  element: Element,
   hidden: boolean,
   hookID?: ?number,
-  getInspectedElementPath: GetInspectedElementPath,
   inspectedElement: InspectedElement,
   isDirectChildOfAnArray?: boolean,
   name: string,
@@ -58,7 +65,7 @@ export default function KeyValue({
   canRenamePaths,
   canRenamePathsAtDepth,
   depth,
-  getInspectedElementPath,
+  element,
   inspectedElement,
   isDirectChildOfAnArray,
   hidden,
@@ -72,9 +79,10 @@ export default function KeyValue({
   const {id} = inspectedElement;
 
   const [isOpen, setIsOpen] = useState<boolean>(false);
-  const prevIsOpenRef = useRef(isOpen);
   const contextMenuTriggerRef = useRef(null);
 
+  const {inspectPaths} = useContext(InspectedElementContext);
+
   let isInspectable = false;
   let isReadOnly = false;
   if (value !== null && typeof value === 'object') {
@@ -82,14 +90,20 @@ export default function KeyValue({
     isReadOnly = value[meta.readonly];
   }
 
-  useEffect(() => {
-    if (isInspectable && isOpen && !prevIsOpenRef.current) {
-      getInspectedElementPath(id, [pathRoot, ...path]);
-    }
-    prevIsOpenRef.current = isOpen;
-  }, [getInspectedElementPath, isInspectable, isOpen, path, pathRoot]);
+  const [startInspectPathsTransition, isInspectPathsPending] = useTransition();
+  const toggleIsOpen = () => {
+    if (isOpen) {
+      setIsOpen(false);
+    } else {
+      setIsOpen(true);
 
-  const toggleIsOpen = () => setIsOpen(prevIsOpen => !prevIsOpen);
+      if (isInspectable) {
+        startInspectPathsTransition(() => {
+          inspectPaths([pathRoot, ...path]);
+        });
+      }
+    }
+  };
 
   useContextMenu({
     data: {
@@ -255,7 +269,7 @@ export default function KeyValue({
         ref={contextMenuTriggerRef}
         style={style}>
         {isInspectable ? (
-          <ExpandCollapseToggle isOpen={isOpen} setIsOpen={setIsOpen} />
+          <ExpandCollapseToggle isOpen={isOpen} setIsOpen={toggleIsOpen} />
         ) : (
           <div className={styles.ExpandCollapseToggleSpacer} />
         )}
@@ -268,6 +282,18 @@ export default function KeyValue({
         </span>
       </div>
     );
+
+    if (isInspectPathsPending) {
+      children = (
+        <>
+          {children}
+          <div className={styles.Item} style={style}>
+            <div className={styles.ExpandCollapseToggleSpacer} />
+            <LoadingAnimation />
+          </div>
+        </>
+      );
+    }
   } else {
     if (Array.isArray(value)) {
       const hasChildren = value.length > 0 || canEditValues;
@@ -283,7 +309,7 @@ export default function KeyValue({
           canRenamePaths={canRenamePaths && !isReadOnly}
           canRenamePathsAtDepth={canRenamePathsAtDepth}
           depth={depth + 1}
-          getInspectedElementPath={getInspectedElementPath}
+          element={element}
           hookID={hookID}
           inspectedElement={inspectedElement}
           isDirectChildOfAnArray={true}
@@ -305,7 +331,7 @@ export default function KeyValue({
             hidden={hidden || !isOpen}
             hookID={hookID}
             index={value.length}
-            getInspectedElementPath={getInspectedElementPath}
+            element={element}
             inspectedElement={inspectedElement}
             path={path}
             store={store}
@@ -347,7 +373,7 @@ export default function KeyValue({
       const hasChildren = entries.length > 0 || canEditValues;
       const displayName = getMetaValueLabel(value);
 
-      children = entries.map<Element<any>>(([key, keyValue]) => (
+      children = entries.map<ReactElement<any>>(([key, keyValue]) => (
         <KeyValue
           key={key}
           alphaSort={alphaSort}
@@ -357,7 +383,7 @@ export default function KeyValue({
           canRenamePaths={canRenamePaths && !isReadOnly}
           canRenamePathsAtDepth={canRenamePathsAtDepth}
           depth={depth + 1}
-          getInspectedElementPath={getInspectedElementPath}
+          element={element}
           hookID={hookID}
           inspectedElement={inspectedElement}
           hidden={hidden || !isOpen}
@@ -375,7 +401,7 @@ export default function KeyValue({
             key="NewKeyValue"
             bridge={bridge}
             depth={depth + 1}
-            getInspectedElementPath={getInspectedElementPath}
+            element={element}
             hidden={hidden || !isOpen}
             hookID={hookID}
             inspectedElement={inspectedElement}
diff --git a/packages/react-devtools-shared/src/devtools/views/Components/LoadingAnimation.css b/packages/react-devtools-shared/src/devtools/views/Components/LoadingAnimation.css
new file mode 100644
index 0000000000000..2141ede58bd79
--- /dev/null
+++ b/packages/react-devtools-shared/src/devtools/views/Components/LoadingAnimation.css
@@ -0,0 +1,5 @@
+.Icon {
+  width: 1rem;
+  height: 1rem;
+  fill: currentColor;
+}
diff --git a/packages/react-devtools-shared/src/devtools/views/Components/LoadingAnimation.js b/packages/react-devtools-shared/src/devtools/views/Components/LoadingAnimation.js
new file mode 100644
index 0000000000000..eb3268cefcbb1
--- /dev/null
+++ b/packages/react-devtools-shared/src/devtools/views/Components/LoadingAnimation.js
@@ -0,0 +1,55 @@
+/**
+ * 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 styles from './LoadingAnimation.css';
+
+type Props = {|
+  className?: string,
+|};
+
+export default function LoadingAnimation({className = ''}: Props) {
+  return (
+    <svg
+      xmlns="http://www.w3.org/2000/svg"
+      className={`${styles.Icon} ${className}`}
+      width="24"
+      height="24"
+      viewBox="0 0 100 100">
+      <path d="M0 0h100v100H0z" fill="none" />
+      <circle fill="currentColor" stroke="none" cx="20" cy="50" r="10">
+        <animate
+          attributeName="opacity"
+          dur="1s"
+          values="0;1;0"
+          repeatCount="indefinite"
+          begin="0.1"
+        />
+      </circle>
+      <circle fill="currentColor" stroke="none" cx="50" cy="50" r="10">
+        <animate
+          attributeName="opacity"
+          dur="1s"
+          values="0;1;0"
+          repeatCount="indefinite"
+          begin="0.2"
+        />
+      </circle>
+      <circle fill="currentColor" stroke="none" cx="80" cy="50" r="10">
+        <animate
+          attributeName="opacity"
+          dur="1s"
+          values="0;1;0"
+          repeatCount="indefinite"
+          begin="0.3"
+        />
+      </circle>
+    </svg>
+  );
+}
diff --git a/packages/react-devtools-shared/src/devtools/views/Components/Tree.js b/packages/react-devtools-shared/src/devtools/views/Components/Tree.js
index 467af81b77f3d..629bc0f721139 100644
--- a/packages/react-devtools-shared/src/devtools/views/Components/Tree.js
+++ b/packages/react-devtools-shared/src/devtools/views/Components/Tree.js
@@ -32,7 +32,7 @@ import SettingsModalContextToggle from 'react-devtools-shared/src/devtools/views
 import SelectedTreeHighlight from './SelectedTreeHighlight';
 import TreeFocusedContext from './TreeFocusedContext';
 import {useHighlightNativeElement, useSubscription} from '../hooks';
-
+import {clearErrorsAndWarnings as clearErrorsAndWarningsAPI} from 'react-devtools-shared/src/backendAPI';
 import styles from './Tree.css';
 import ButtonIcon from '../ButtonIcon';
 import Button from '../Button';
@@ -327,6 +327,10 @@ export default function Tree(props: Props) {
   );
   const {errors, warnings} = useSubscription(errorsOrWarningsSubscription);
 
+  const clearErrorsAndWarnings = () => {
+    clearErrorsAndWarningsAPI({bridge, store});
+  };
+
   return (
     <TreeFocusedContext.Provider value={treeFocused}>
       <div className={styles.Tree} ref={treeRef}>
@@ -368,7 +372,7 @@ export default function Tree(props: Props) {
                   <ButtonIcon type="down" />
                 </Button>
                 <Button
-                  onClick={() => store.clearErrorsAndWarnings()}
+                  onClick={clearErrorsAndWarnings}
                   title="Clear all errors and warnings">
                   <ButtonIcon type="clear" />
                 </Button>
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<Props, State> {
   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<Props, State> {
     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<T> = {|
+  status: 1,
+  value: T,
+|};
+
+type RejectedRecord = {|
+  status: 2,
+  value: string,
+|};
+
+type Record<T> = PendingRecord | ResolvedRecord<T> | RejectedRecord;
+
+function readRecord<T>(record: Record<T>): ResolvedRecord<T> {
+  if (record.status === Resolved) {
+    // This is just a type refinement.
+    return record;
+  } else {
+    throw record.value;
+  }
+}
+
+type InspectedElementMap = WeakMap<Element, Record<InspectedElementFrontend>>;
+type CacheSeedKey = () => InspectedElementMap;
+
+function createMap(): InspectedElementMap {
+  return new WeakMap();
+}
+
+function getRecordMap(): WeakMap<Element, Record<InspectedElementFrontend>> {
+  return unstable_getCacheForType(createMap);
+}
+
+function createCacheSeed(
+  element: Element,
+  inspectedElement: InspectedElementFrontend,
+): [CacheSeedKey, InspectedElementMap] {
+  const newRecord: Record<InspectedElementFrontend> = {
+    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<InspectedElementFrontend> = (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<InspectedElementFrontend>);
+              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/', '<rootDir>/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});
     }