diff --git a/packages/react-devtools-core/src/standalone.js b/packages/react-devtools-core/src/standalone.js
index 817b34386c205..e31b43d724d1b 100644
--- a/packages/react-devtools-core/src/standalone.js
+++ b/packages/react-devtools-core/src/standalone.js
@@ -20,6 +20,7 @@ import {
   getAppendComponentStack,
   getBreakOnConsoleErrors,
   getSavedComponentFilters,
+  getShowInlineWarningsAndErrors,
 } from 'react-devtools-shared/src/utils';
 import {Server} from 'ws';
 import {join} from 'path';
@@ -303,6 +304,9 @@ function startServer(
       )};
       window.__REACT_DEVTOOLS_COMPONENT_FILTERS__ = ${JSON.stringify(
         getSavedComponentFilters(),
+      )};
+      window.__REACT_DEVTOOLS_SHOW_INLINE_WARNINGS_AND_ERRORS__ = ${JSON.stringify(
+        getShowInlineWarningsAndErrors(),
       )};`;
 
     response.end(
diff --git a/packages/react-devtools-extensions/src/main.js b/packages/react-devtools-extensions/src/main.js
index 00e3735c72c11..d8789e1c69afc 100644
--- a/packages/react-devtools-extensions/src/main.js
+++ b/packages/react-devtools-extensions/src/main.js
@@ -10,6 +10,7 @@ import {
   getAppendComponentStack,
   getBreakOnConsoleErrors,
   getSavedComponentFilters,
+  getShowInlineWarningsAndErrors,
 } from 'react-devtools-shared/src/utils';
 import {
   localStorageGetItem,
@@ -29,18 +30,18 @@ let panelCreated = false;
 // because they are stored in localStorage within the context of the extension.
 // Instead it relies on the extension to pass filters through.
 function syncSavedPreferences() {
-  const appendComponentStack = getAppendComponentStack();
-  const breakOnConsoleErrors = getBreakOnConsoleErrors();
-  const componentFilters = getSavedComponentFilters();
   chrome.devtools.inspectedWindow.eval(
     `window.__REACT_DEVTOOLS_APPEND_COMPONENT_STACK__ = ${JSON.stringify(
-      appendComponentStack,
+      getAppendComponentStack(),
     )};
     window.__REACT_DEVTOOLS_BREAK_ON_CONSOLE_ERRORS__ = ${JSON.stringify(
-      breakOnConsoleErrors,
+      getBreakOnConsoleErrors(),
     )};
     window.__REACT_DEVTOOLS_COMPONENT_FILTERS__ = ${JSON.stringify(
-      componentFilters,
+      getSavedComponentFilters(),
+    )};
+    window.__REACT_DEVTOOLS_SHOW_INLINE_WARNINGS_AND_ERRORS__ = ${JSON.stringify(
+      getShowInlineWarningsAndErrors(),
     )};`,
   );
 }
diff --git a/packages/react-devtools-inline/src/backend.js b/packages/react-devtools-inline/src/backend.js
index fb46276924652..7a5bd57b928eb 100644
--- a/packages/react-devtools-inline/src/backend.js
+++ b/packages/react-devtools-inline/src/backend.js
@@ -24,11 +24,13 @@ function startActivation(contentWindow: window) {
           appendComponentStack,
           breakOnConsoleErrors,
           componentFilters,
+          showInlineWarningsAndErrors,
         } = data;
 
         contentWindow.__REACT_DEVTOOLS_APPEND_COMPONENT_STACK__ = appendComponentStack;
         contentWindow.__REACT_DEVTOOLS_BREAK_ON_CONSOLE_ERRORS__ = breakOnConsoleErrors;
         contentWindow.__REACT_DEVTOOLS_COMPONENT_FILTERS__ = componentFilters;
+        contentWindow.__REACT_DEVTOOLS_SHOW_INLINE_WARNINGS_AND_ERRORS__ = showInlineWarningsAndErrors;
 
         // TRICKY
         // The backend entry point may be required in the context of an iframe or the parent window.
@@ -40,6 +42,7 @@ function startActivation(contentWindow: window) {
           window.__REACT_DEVTOOLS_APPEND_COMPONENT_STACK__ = appendComponentStack;
           window.__REACT_DEVTOOLS_BREAK_ON_CONSOLE_ERRORS__ = breakOnConsoleErrors;
           window.__REACT_DEVTOOLS_COMPONENT_FILTERS__ = componentFilters;
+          window.__REACT_DEVTOOLS_SHOW_INLINE_WARNINGS_AND_ERRORS__ = showInlineWarningsAndErrors;
         }
 
         finishActivation(contentWindow);
diff --git a/packages/react-devtools-inline/src/frontend.js b/packages/react-devtools-inline/src/frontend.js
index 9a5c145471139..3972f1789ab74 100644
--- a/packages/react-devtools-inline/src/frontend.js
+++ b/packages/react-devtools-inline/src/frontend.js
@@ -9,6 +9,7 @@ import {
   getAppendComponentStack,
   getBreakOnConsoleErrors,
   getSavedComponentFilters,
+  getShowInlineWarningsAndErrors,
 } from 'react-devtools-shared/src/utils';
 import {
   MESSAGE_TYPE_GET_SAVED_PREFERENCES,
@@ -41,6 +42,7 @@ export function initialize(
             appendComponentStack: getAppendComponentStack(),
             breakOnConsoleErrors: getBreakOnConsoleErrors(),
             componentFilters: getSavedComponentFilters(),
+            showInlineWarningsAndErrors: getShowInlineWarningsAndErrors(),
           },
           '*',
         );
diff --git a/packages/react-devtools-shared/src/__tests__/__snapshots__/ownersListContext-test.js.snap b/packages/react-devtools-shared/src/__tests__/__snapshots__/ownersListContext-test.js.snap
index 25e56daf6f44e..d5e244adf062f 100644
--- a/packages/react-devtools-shared/src/__tests__/__snapshots__/ownersListContext-test.js.snap
+++ b/packages/react-devtools-shared/src/__tests__/__snapshots__/ownersListContext-test.js.snap
@@ -12,19 +12,19 @@ Array [
   Object {
     "displayName": "Grandparent",
     "hocDisplayNames": null,
-    "id": 7,
+    "id": 2,
     "type": 5,
   },
   Object {
     "displayName": "Parent",
     "hocDisplayNames": null,
-    "id": 9,
+    "id": 3,
     "type": 5,
   },
   Object {
     "displayName": "Child",
     "hocDisplayNames": null,
-    "id": 8,
+    "id": 4,
     "type": 5,
   },
 ]
@@ -115,7 +115,7 @@ Array [
   Object {
     "displayName": "Grandparent",
     "hocDisplayNames": null,
-    "id": 5,
+    "id": 2,
     "type": 5,
   },
 ]
diff --git a/packages/react-devtools-shared/src/__tests__/__snapshots__/storeComponentFilters-test.js.snap b/packages/react-devtools-shared/src/__tests__/__snapshots__/storeComponentFilters-test.js.snap
deleted file mode 100644
index 49035b9927836..0000000000000
--- a/packages/react-devtools-shared/src/__tests__/__snapshots__/storeComponentFilters-test.js.snap
+++ /dev/null
@@ -1,140 +0,0 @@
-// Jest Snapshot v1, https://goo.gl/fbAQLP
-
-exports[`Store component filters should filter HOCs: 1: mount 1`] = `
-[root]
-  ▾ <Component> [Bar][Foo]
-    ▾ <Component> [Foo]
-      ▾ <Component>
-          <div>
-`;
-
-exports[`Store component filters should filter HOCs: 2: hide all HOCs 1`] = `
-[root]
-  ▾ <Component>
-      <div>
-`;
-
-exports[`Store component filters should filter HOCs: 3: disable HOC filter 1`] = `
-[root]
-  ▾ <Component> [Bar][Foo]
-    ▾ <Component> [Foo]
-      ▾ <Component>
-          <div>
-`;
-
-exports[`Store component filters should filter by display name: 1: mount 1`] = `
-[root]
-  ▾ <Foo>
-      <Text>
-  ▾ <Bar>
-      <Text>
-  ▾ <Baz>
-      <Text>
-`;
-
-exports[`Store component filters should filter by display name: 2: filter "Foo" 1`] = `
-[root]
-    <Text>
-  ▾ <Bar>
-      <Text>
-  ▾ <Baz>
-      <Text>
-`;
-
-exports[`Store component filters should filter by display name: 3: filter "Ba" 1`] = `
-[root]
-  ▾ <Foo>
-      <Text>
-    <Text>
-    <Text>
-`;
-
-exports[`Store component filters should filter by display name: 4: filter "B.z" 1`] = `
-[root]
-  ▾ <Foo>
-      <Text>
-  ▾ <Bar>
-      <Text>
-    <Text>
-`;
-
-exports[`Store component filters should filter by path: 1: mount 1`] = `
-[root]
-  ▾ <Component>
-      <div>
-`;
-
-exports[`Store component filters should filter by path: 2: hide all components declared within this test filed 1`] = `[root]`;
-
-exports[`Store component filters should filter by path: 3: hide components in a made up fake path 1`] = `
-[root]
-  ▾ <Component>
-      <div>
-`;
-
-exports[`Store component filters should ignore invalid ElementTypeRoot filter: 1: mount 1`] = `
-[root]
-  ▾ <Root>
-      <div>
-`;
-
-exports[`Store component filters should ignore invalid ElementTypeRoot filter: 2: add invalid filter 1`] = `
-[root]
-  ▾ <Root>
-      <div>
-`;
-
-exports[`Store component filters should not break when Suspense nodes are filtered from the tree: 1: suspended 1`] = `
-[root]
-  ▾ <Wrapper>
-    ▾ <Loading>
-        <div>
-`;
-
-exports[`Store component filters should not break when Suspense nodes are filtered from the tree: 2: resolved 1`] = `
-[root]
-  ▾ <Wrapper>
-      <Component>
-`;
-
-exports[`Store component filters should not break when Suspense nodes are filtered from the tree: 3: suspended 1`] = `
-[root]
-  ▾ <Wrapper>
-    ▾ <Loading>
-        <div>
-`;
-
-exports[`Store component filters should support filtering by element type: 1: mount 1`] = `
-[root]
-  ▾ <Root>
-    ▾ <div>
-      ▾ <Component>
-          <div>
-`;
-
-exports[`Store component filters should support filtering by element type: 2: hide host components 1`] = `
-[root]
-  ▾ <Root>
-      <Component>
-`;
-
-exports[`Store component filters should support filtering by element type: 3: hide class components 1`] = `
-[root]
-  ▾ <div>
-    ▾ <Component>
-        <div>
-`;
-
-exports[`Store component filters should support filtering by element type: 4: hide class and function components 1`] = `
-[root]
-  ▾ <div>
-      <div>
-`;
-
-exports[`Store component filters should support filtering by element type: 5: disable all filters 1`] = `
-[root]
-  ▾ <Root>
-    ▾ <div>
-      ▾ <Component>
-          <div>
-`;
diff --git a/packages/react-devtools-shared/src/__tests__/__snapshots__/treeContext-test.js.snap b/packages/react-devtools-shared/src/__tests__/__snapshots__/treeContext-test.js.snap
deleted file mode 100644
index 1e72710e4bd2b..0000000000000
--- a/packages/react-devtools-shared/src/__tests__/__snapshots__/treeContext-test.js.snap
+++ /dev/null
@@ -1,1293 +0,0 @@
-// Jest Snapshot v1, https://goo.gl/fbAQLP
-
-exports[`TreeListContext owners state should exit the owners list if an element outside the list is selected: 0: mount 1`] = `
-[root]
-  ▾ <Parent>
-    ▾ <Suspense>
-      ▾ <Child>
-        ▾ <Suspense>
-            <Grandchild>
-`;
-
-exports[`TreeListContext owners state should exit the owners list if an element outside the list is selected: 1: initial state 1`] = `
-Object {
-  "inspectedElementID": null,
-  "numElements": 5,
-  "ownerFlatTree": null,
-  "ownerID": null,
-  "ownerSubtreeLeafElementID": null,
-  "searchIndex": null,
-  "searchResults": Array [],
-  "searchText": "",
-  "selectedElementID": null,
-  "selectedElementIndex": null,
-}
-`;
-
-exports[`TreeListContext owners state should exit the owners list if an element outside the list is selected: 2: child owners tree 1`] = `
-Object {
-  "inspectedElementID": 4,
-  "numElements": 3,
-  "ownerFlatTree": Array [
-    Object {
-      "children": Array [
-        5,
-      ],
-      "depth": 0,
-      "displayName": "Child",
-      "hocDisplayNames": null,
-      "id": 4,
-      "isCollapsed": false,
-      "key": null,
-      "ownerID": 2,
-      "parentID": 3,
-      "type": 5,
-      "weight": 3,
-    },
-    Object {
-      "children": Array [
-        6,
-      ],
-      "depth": 1,
-      "displayName": "Suspense",
-      "hocDisplayNames": null,
-      "id": 5,
-      "isCollapsed": false,
-      "key": null,
-      "ownerID": 4,
-      "parentID": 4,
-      "type": 12,
-      "weight": 2,
-    },
-    Object {
-      "children": Array [],
-      "depth": 2,
-      "displayName": "Grandchild",
-      "hocDisplayNames": null,
-      "id": 6,
-      "isCollapsed": false,
-      "key": null,
-      "ownerID": 4,
-      "parentID": 5,
-      "type": 5,
-      "weight": 1,
-    },
-  ],
-  "ownerID": 4,
-  "ownerSubtreeLeafElementID": null,
-  "searchIndex": null,
-  "searchResults": Array [],
-  "searchText": "",
-  "selectedElementID": 4,
-  "selectedElementIndex": 0,
-}
-`;
-
-exports[`TreeListContext owners state should exit the owners list if an element outside the list is selected: 3: child owners tree 1`] = `
-Object {
-  "inspectedElementID": 5,
-  "numElements": 3,
-  "ownerFlatTree": Array [
-    Object {
-      "children": Array [
-        5,
-      ],
-      "depth": 0,
-      "displayName": "Child",
-      "hocDisplayNames": null,
-      "id": 4,
-      "isCollapsed": false,
-      "key": null,
-      "ownerID": 2,
-      "parentID": 3,
-      "type": 5,
-      "weight": 3,
-    },
-    Object {
-      "children": Array [
-        6,
-      ],
-      "depth": 1,
-      "displayName": "Suspense",
-      "hocDisplayNames": null,
-      "id": 5,
-      "isCollapsed": false,
-      "key": null,
-      "ownerID": 4,
-      "parentID": 4,
-      "type": 12,
-      "weight": 2,
-    },
-    Object {
-      "children": Array [],
-      "depth": 2,
-      "displayName": "Grandchild",
-      "hocDisplayNames": null,
-      "id": 6,
-      "isCollapsed": false,
-      "key": null,
-      "ownerID": 4,
-      "parentID": 5,
-      "type": 5,
-      "weight": 1,
-    },
-  ],
-  "ownerID": 4,
-  "ownerSubtreeLeafElementID": null,
-  "searchIndex": null,
-  "searchResults": Array [],
-  "searchText": "",
-  "selectedElementID": 5,
-  "selectedElementIndex": 1,
-}
-`;
-
-exports[`TreeListContext owners state should exit the owners list if an element outside the list is selected: 4: main tree 1`] = `
-Object {
-  "inspectedElementID": 5,
-  "numElements": 5,
-  "ownerFlatTree": null,
-  "ownerID": null,
-  "ownerSubtreeLeafElementID": null,
-  "searchIndex": null,
-  "searchResults": Array [],
-  "searchText": "",
-  "selectedElementID": 5,
-  "selectedElementIndex": 1,
-}
-`;
-
-exports[`TreeListContext owners state should exit the owners list if the current owner is unmounted: 0: mount 1`] = `
-[root]
-  ▾ <Parent>
-      <Child>
-`;
-
-exports[`TreeListContext owners state should exit the owners list if the current owner is unmounted: 1: initial state 1`] = `
-Object {
-  "inspectedElementID": null,
-  "numElements": 2,
-  "ownerFlatTree": null,
-  "ownerID": null,
-  "ownerSubtreeLeafElementID": null,
-  "searchIndex": null,
-  "searchResults": Array [],
-  "searchText": "",
-  "selectedElementID": null,
-  "selectedElementIndex": null,
-}
-`;
-
-exports[`TreeListContext owners state should exit the owners list if the current owner is unmounted: 2: child owners tree 1`] = `
-Object {
-  "inspectedElementID": 3,
-  "numElements": 1,
-  "ownerFlatTree": Array [
-    Object {
-      "children": Array [],
-      "depth": 0,
-      "displayName": "Child",
-      "hocDisplayNames": null,
-      "id": 3,
-      "isCollapsed": false,
-      "key": null,
-      "ownerID": 0,
-      "parentID": 2,
-      "type": 5,
-      "weight": 1,
-    },
-  ],
-  "ownerID": 3,
-  "ownerSubtreeLeafElementID": null,
-  "searchIndex": null,
-  "searchResults": Array [],
-  "searchText": "",
-  "selectedElementID": 3,
-  "selectedElementIndex": 0,
-}
-`;
-
-exports[`TreeListContext owners state should exit the owners list if the current owner is unmounted: 3: remove child 1`] = `
-Object {
-  "inspectedElementID": null,
-  "numElements": 1,
-  "ownerFlatTree": null,
-  "ownerID": null,
-  "ownerSubtreeLeafElementID": null,
-  "searchIndex": null,
-  "searchResults": Array [],
-  "searchText": "",
-  "selectedElementID": null,
-  "selectedElementIndex": 0,
-}
-`;
-
-exports[`TreeListContext owners state should exit the owners list if the current owner is unmounted: 4: parent owners tree 1`] = `
-Object {
-  "inspectedElementID": 2,
-  "numElements": 1,
-  "ownerFlatTree": Array [
-    Object {
-      "children": Array [],
-      "depth": 0,
-      "displayName": "Parent",
-      "hocDisplayNames": null,
-      "id": 2,
-      "isCollapsed": false,
-      "key": null,
-      "ownerID": 0,
-      "parentID": 1,
-      "type": 5,
-      "weight": 1,
-    },
-  ],
-  "ownerID": 2,
-  "ownerSubtreeLeafElementID": null,
-  "searchIndex": null,
-  "searchResults": Array [],
-  "searchText": "",
-  "selectedElementID": 2,
-  "selectedElementIndex": 0,
-}
-`;
-
-exports[`TreeListContext owners state should exit the owners list if the current owner is unmounted: 5: unmount root 1`] = `
-Object {
-  "inspectedElementID": null,
-  "numElements": 0,
-  "ownerFlatTree": null,
-  "ownerID": null,
-  "ownerSubtreeLeafElementID": null,
-  "searchIndex": null,
-  "searchResults": Array [],
-  "searchText": "",
-  "selectedElementID": null,
-  "selectedElementIndex": 0,
-}
-`;
-
-exports[`TreeListContext owners state should remove an element from the owners list if it is unmounted: 0: mount 1`] = `
-[root]
-  ▾ <Grandparent>
-    ▾ <Parent>
-        <Child key="0">
-        <Child key="1">
-`;
-
-exports[`TreeListContext owners state should remove an element from the owners list if it is unmounted: 1: initial state 1`] = `
-Object {
-  "inspectedElementID": null,
-  "numElements": 4,
-  "ownerFlatTree": null,
-  "ownerID": null,
-  "ownerSubtreeLeafElementID": null,
-  "searchIndex": null,
-  "searchResults": Array [],
-  "searchText": "",
-  "selectedElementID": null,
-  "selectedElementIndex": null,
-}
-`;
-
-exports[`TreeListContext owners state should remove an element from the owners list if it is unmounted: 2: parent owners tree 1`] = `
-Object {
-  "inspectedElementID": 3,
-  "numElements": 3,
-  "ownerFlatTree": Array [
-    Object {
-      "children": Array [
-        4,
-        5,
-      ],
-      "depth": 0,
-      "displayName": "Parent",
-      "hocDisplayNames": null,
-      "id": 3,
-      "isCollapsed": false,
-      "key": null,
-      "ownerID": 2,
-      "parentID": 2,
-      "type": 5,
-      "weight": 3,
-    },
-    Object {
-      "children": Array [],
-      "depth": 1,
-      "displayName": "Child",
-      "hocDisplayNames": null,
-      "id": 4,
-      "isCollapsed": false,
-      "key": "0",
-      "ownerID": 3,
-      "parentID": 3,
-      "type": 5,
-      "weight": 1,
-    },
-    Object {
-      "children": Array [],
-      "depth": 1,
-      "displayName": "Child",
-      "hocDisplayNames": null,
-      "id": 5,
-      "isCollapsed": false,
-      "key": "1",
-      "ownerID": 3,
-      "parentID": 3,
-      "type": 5,
-      "weight": 1,
-    },
-  ],
-  "ownerID": 3,
-  "ownerSubtreeLeafElementID": null,
-  "searchIndex": null,
-  "searchResults": Array [],
-  "searchText": "",
-  "selectedElementID": 3,
-  "selectedElementIndex": 0,
-}
-`;
-
-exports[`TreeListContext owners state should remove an element from the owners list if it is unmounted: 3: remove second child 1`] = `
-Object {
-  "inspectedElementID": 3,
-  "numElements": 2,
-  "ownerFlatTree": Array [
-    Object {
-      "children": Array [
-        4,
-      ],
-      "depth": 0,
-      "displayName": "Parent",
-      "hocDisplayNames": null,
-      "id": 3,
-      "isCollapsed": false,
-      "key": null,
-      "ownerID": 2,
-      "parentID": 2,
-      "type": 5,
-      "weight": 2,
-    },
-    Object {
-      "children": Array [],
-      "depth": 1,
-      "displayName": "Child",
-      "hocDisplayNames": null,
-      "id": 4,
-      "isCollapsed": false,
-      "key": "0",
-      "ownerID": 3,
-      "parentID": 3,
-      "type": 5,
-      "weight": 1,
-    },
-  ],
-  "ownerID": 3,
-  "ownerSubtreeLeafElementID": null,
-  "searchIndex": null,
-  "searchResults": Array [],
-  "searchText": "",
-  "selectedElementID": 3,
-  "selectedElementIndex": 0,
-}
-`;
-
-exports[`TreeListContext owners state should remove an element from the owners list if it is unmounted: 4: remove first child 1`] = `
-Object {
-  "inspectedElementID": 3,
-  "numElements": 1,
-  "ownerFlatTree": Array [
-    Object {
-      "children": Array [],
-      "depth": 0,
-      "displayName": "Parent",
-      "hocDisplayNames": null,
-      "id": 3,
-      "isCollapsed": false,
-      "key": null,
-      "ownerID": 2,
-      "parentID": 2,
-      "type": 5,
-      "weight": 1,
-    },
-  ],
-  "ownerID": 3,
-  "ownerSubtreeLeafElementID": null,
-  "searchIndex": null,
-  "searchResults": Array [],
-  "searchText": "",
-  "selectedElementID": 3,
-  "selectedElementIndex": 0,
-}
-`;
-
-exports[`TreeListContext owners state should support entering and existing the owners tree view: 0: mount 1`] = `
-[root]
-  ▾ <Grandparent>
-    ▾ <Parent>
-        <Child>
-        <Child>
-`;
-
-exports[`TreeListContext owners state should support entering and existing the owners tree view: 1: initial state 1`] = `
-Object {
-  "inspectedElementID": null,
-  "numElements": 4,
-  "ownerFlatTree": null,
-  "ownerID": null,
-  "ownerSubtreeLeafElementID": null,
-  "searchIndex": null,
-  "searchResults": Array [],
-  "searchText": "",
-  "selectedElementID": null,
-  "selectedElementIndex": null,
-}
-`;
-
-exports[`TreeListContext owners state should support entering and existing the owners tree view: 2: parent owners tree 1`] = `
-Object {
-  "inspectedElementID": 3,
-  "numElements": 3,
-  "ownerFlatTree": Array [
-    Object {
-      "children": Array [
-        4,
-        5,
-      ],
-      "depth": 0,
-      "displayName": "Parent",
-      "hocDisplayNames": null,
-      "id": 3,
-      "isCollapsed": false,
-      "key": null,
-      "ownerID": 2,
-      "parentID": 2,
-      "type": 5,
-      "weight": 3,
-    },
-    Object {
-      "children": Array [],
-      "depth": 1,
-      "displayName": "Child",
-      "hocDisplayNames": null,
-      "id": 4,
-      "isCollapsed": false,
-      "key": null,
-      "ownerID": 3,
-      "parentID": 3,
-      "type": 5,
-      "weight": 1,
-    },
-    Object {
-      "children": Array [],
-      "depth": 1,
-      "displayName": "Child",
-      "hocDisplayNames": null,
-      "id": 5,
-      "isCollapsed": false,
-      "key": null,
-      "ownerID": 3,
-      "parentID": 3,
-      "type": 5,
-      "weight": 1,
-    },
-  ],
-  "ownerID": 3,
-  "ownerSubtreeLeafElementID": null,
-  "searchIndex": null,
-  "searchResults": Array [],
-  "searchText": "",
-  "selectedElementID": 3,
-  "selectedElementIndex": 0,
-}
-`;
-
-exports[`TreeListContext owners state should support entering and existing the owners tree view: 3: final state 1`] = `
-Object {
-  "inspectedElementID": 3,
-  "numElements": 4,
-  "ownerFlatTree": null,
-  "ownerID": null,
-  "ownerSubtreeLeafElementID": null,
-  "searchIndex": null,
-  "searchResults": Array [],
-  "searchText": "",
-  "selectedElementID": 3,
-  "selectedElementIndex": 1,
-}
-`;
-
-exports[`TreeListContext search state should add newly mounted elements to the search results set if they match the current text: 0: mount 1`] = `
-[root]
-    <Foo>
-    <Bar>
-`;
-
-exports[`TreeListContext search state should add newly mounted elements to the search results set if they match the current text: 1: initial state 1`] = `
-Object {
-  "inspectedElementID": null,
-  "numElements": 2,
-  "ownerFlatTree": null,
-  "ownerID": null,
-  "ownerSubtreeLeafElementID": null,
-  "searchIndex": null,
-  "searchResults": Array [],
-  "searchText": "",
-  "selectedElementID": null,
-  "selectedElementIndex": null,
-}
-`;
-
-exports[`TreeListContext search state should add newly mounted elements to the search results set if they match the current text: 2: search for "ba" 1`] = `
-Object {
-  "inspectedElementID": 3,
-  "numElements": 2,
-  "ownerFlatTree": null,
-  "ownerID": null,
-  "ownerSubtreeLeafElementID": null,
-  "searchIndex": 0,
-  "searchResults": Array [
-    3,
-  ],
-  "searchText": "ba",
-  "selectedElementID": 3,
-  "selectedElementIndex": 1,
-}
-`;
-
-exports[`TreeListContext search state should add newly mounted elements to the search results set if they match the current text: 3: mount Baz 1`] = `
-Object {
-  "inspectedElementID": 3,
-  "numElements": 3,
-  "ownerFlatTree": null,
-  "ownerID": null,
-  "ownerSubtreeLeafElementID": null,
-  "searchIndex": 0,
-  "searchResults": Array [
-    3,
-    4,
-  ],
-  "searchText": "ba",
-  "selectedElementID": 3,
-  "selectedElementIndex": 1,
-}
-`;
-
-exports[`TreeListContext search state should find elements matching search text: 0: mount 1`] = `
-[root]
-    <Foo>
-    <Bar>
-    <Baz>
-    <Qux> [withHOC]
-`;
-
-exports[`TreeListContext search state should find elements matching search text: 1: initial state 1`] = `
-Object {
-  "inspectedElementID": null,
-  "numElements": 4,
-  "ownerFlatTree": null,
-  "ownerID": null,
-  "ownerSubtreeLeafElementID": null,
-  "searchIndex": null,
-  "searchResults": Array [],
-  "searchText": "",
-  "selectedElementID": null,
-  "selectedElementIndex": null,
-}
-`;
-
-exports[`TreeListContext search state should find elements matching search text: 2: search for "ba" 1`] = `
-Object {
-  "inspectedElementID": 3,
-  "numElements": 4,
-  "ownerFlatTree": null,
-  "ownerID": null,
-  "ownerSubtreeLeafElementID": null,
-  "searchIndex": 0,
-  "searchResults": Array [
-    3,
-    4,
-  ],
-  "searchText": "ba",
-  "selectedElementID": 3,
-  "selectedElementIndex": 1,
-}
-`;
-
-exports[`TreeListContext search state should find elements matching search text: 3: search for "f" 1`] = `
-Object {
-  "inspectedElementID": 2,
-  "numElements": 4,
-  "ownerFlatTree": null,
-  "ownerID": null,
-  "ownerSubtreeLeafElementID": null,
-  "searchIndex": 0,
-  "searchResults": Array [
-    2,
-  ],
-  "searchText": "f",
-  "selectedElementID": 2,
-  "selectedElementIndex": 0,
-}
-`;
-
-exports[`TreeListContext search state should find elements matching search text: 4: search for "y" 1`] = `
-Object {
-  "inspectedElementID": 2,
-  "numElements": 4,
-  "ownerFlatTree": null,
-  "ownerID": null,
-  "ownerSubtreeLeafElementID": null,
-  "searchIndex": null,
-  "searchResults": Array [],
-  "searchText": "y",
-  "selectedElementID": 2,
-  "selectedElementIndex": 0,
-}
-`;
-
-exports[`TreeListContext search state should find elements matching search text: 5: search for "w" 1`] = `
-Object {
-  "inspectedElementID": 5,
-  "numElements": 4,
-  "ownerFlatTree": null,
-  "ownerID": null,
-  "ownerSubtreeLeafElementID": null,
-  "searchIndex": 0,
-  "searchResults": Array [
-    5,
-  ],
-  "searchText": "w",
-  "selectedElementID": 5,
-  "selectedElementIndex": 3,
-}
-`;
-
-exports[`TreeListContext search state should remove unmounted elements from the search results set: 0: mount 1`] = `
-[root]
-    <Foo>
-    <Bar>
-    <Baz>
-`;
-
-exports[`TreeListContext search state should remove unmounted elements from the search results set: 1: initial state 1`] = `
-Object {
-  "inspectedElementID": null,
-  "numElements": 3,
-  "ownerFlatTree": null,
-  "ownerID": null,
-  "ownerSubtreeLeafElementID": null,
-  "searchIndex": null,
-  "searchResults": Array [],
-  "searchText": "",
-  "selectedElementID": null,
-  "selectedElementIndex": null,
-}
-`;
-
-exports[`TreeListContext search state should remove unmounted elements from the search results set: 2: search for "ba" 1`] = `
-Object {
-  "inspectedElementID": 3,
-  "numElements": 3,
-  "ownerFlatTree": null,
-  "ownerID": null,
-  "ownerSubtreeLeafElementID": null,
-  "searchIndex": 0,
-  "searchResults": Array [
-    3,
-    4,
-  ],
-  "searchText": "ba",
-  "selectedElementID": 3,
-  "selectedElementIndex": 1,
-}
-`;
-
-exports[`TreeListContext search state should remove unmounted elements from the search results set: 3: go to second result 1`] = `
-Object {
-  "inspectedElementID": 4,
-  "numElements": 3,
-  "ownerFlatTree": null,
-  "ownerID": null,
-  "ownerSubtreeLeafElementID": null,
-  "searchIndex": 1,
-  "searchResults": Array [
-    3,
-    4,
-  ],
-  "searchText": "ba",
-  "selectedElementID": 4,
-  "selectedElementIndex": 2,
-}
-`;
-
-exports[`TreeListContext search state should remove unmounted elements from the search results set: 4: unmount Baz 1`] = `
-Object {
-  "inspectedElementID": null,
-  "numElements": 2,
-  "ownerFlatTree": null,
-  "ownerID": null,
-  "ownerSubtreeLeafElementID": null,
-  "searchIndex": 0,
-  "searchResults": Array [
-    3,
-  ],
-  "searchText": "ba",
-  "selectedElementID": null,
-  "selectedElementIndex": null,
-}
-`;
-
-exports[`TreeListContext search state should select the next and previous items within the search results: 0: mount 1`] = `
-[root]
-    <Foo>
-    <Baz>
-    <Bar>
-    <Baz>
-`;
-
-exports[`TreeListContext search state should select the next and previous items within the search results: 1: initial state 1`] = `
-Object {
-  "inspectedElementID": null,
-  "numElements": 4,
-  "ownerFlatTree": null,
-  "ownerID": null,
-  "ownerSubtreeLeafElementID": null,
-  "searchIndex": null,
-  "searchResults": Array [],
-  "searchText": "",
-  "selectedElementID": null,
-  "selectedElementIndex": null,
-}
-`;
-
-exports[`TreeListContext search state should select the next and previous items within the search results: 2: search for "ba" 1`] = `
-Object {
-  "inspectedElementID": 3,
-  "numElements": 4,
-  "ownerFlatTree": null,
-  "ownerID": null,
-  "ownerSubtreeLeafElementID": null,
-  "searchIndex": 0,
-  "searchResults": Array [
-    3,
-    4,
-    5,
-  ],
-  "searchText": "ba",
-  "selectedElementID": 3,
-  "selectedElementIndex": 1,
-}
-`;
-
-exports[`TreeListContext search state should select the next and previous items within the search results: 3: go to second result 1`] = `
-Object {
-  "inspectedElementID": 4,
-  "numElements": 4,
-  "ownerFlatTree": null,
-  "ownerID": null,
-  "ownerSubtreeLeafElementID": null,
-  "searchIndex": 1,
-  "searchResults": Array [
-    3,
-    4,
-    5,
-  ],
-  "searchText": "ba",
-  "selectedElementID": 4,
-  "selectedElementIndex": 2,
-}
-`;
-
-exports[`TreeListContext search state should select the next and previous items within the search results: 4: go to third result 1`] = `
-Object {
-  "inspectedElementID": 5,
-  "numElements": 4,
-  "ownerFlatTree": null,
-  "ownerID": null,
-  "ownerSubtreeLeafElementID": null,
-  "searchIndex": 2,
-  "searchResults": Array [
-    3,
-    4,
-    5,
-  ],
-  "searchText": "ba",
-  "selectedElementID": 5,
-  "selectedElementIndex": 3,
-}
-`;
-
-exports[`TreeListContext search state should select the next and previous items within the search results: 5: go to second result 1`] = `
-Object {
-  "inspectedElementID": 4,
-  "numElements": 4,
-  "ownerFlatTree": null,
-  "ownerID": null,
-  "ownerSubtreeLeafElementID": null,
-  "searchIndex": 1,
-  "searchResults": Array [
-    3,
-    4,
-    5,
-  ],
-  "searchText": "ba",
-  "selectedElementID": 4,
-  "selectedElementIndex": 2,
-}
-`;
-
-exports[`TreeListContext search state should select the next and previous items within the search results: 6: go to first result 1`] = `
-Object {
-  "inspectedElementID": 3,
-  "numElements": 4,
-  "ownerFlatTree": null,
-  "ownerID": null,
-  "ownerSubtreeLeafElementID": null,
-  "searchIndex": 0,
-  "searchResults": Array [
-    3,
-    4,
-    5,
-  ],
-  "searchText": "ba",
-  "selectedElementID": 3,
-  "selectedElementIndex": 1,
-}
-`;
-
-exports[`TreeListContext search state should select the next and previous items within the search results: 7: wrap to last result 1`] = `
-Object {
-  "inspectedElementID": 5,
-  "numElements": 4,
-  "ownerFlatTree": null,
-  "ownerID": null,
-  "ownerSubtreeLeafElementID": null,
-  "searchIndex": 2,
-  "searchResults": Array [
-    3,
-    4,
-    5,
-  ],
-  "searchText": "ba",
-  "selectedElementID": 5,
-  "selectedElementIndex": 3,
-}
-`;
-
-exports[`TreeListContext search state should select the next and previous items within the search results: 8: wrap to first result 1`] = `
-Object {
-  "inspectedElementID": 3,
-  "numElements": 4,
-  "ownerFlatTree": null,
-  "ownerID": null,
-  "ownerSubtreeLeafElementID": null,
-  "searchIndex": 0,
-  "searchResults": Array [
-    3,
-    4,
-    5,
-  ],
-  "searchText": "ba",
-  "selectedElementID": 3,
-  "selectedElementIndex": 1,
-}
-`;
-
-exports[`TreeListContext tree state should clear selection if the selected element is unmounted: 0: mount 1`] = `
-[root]
-  ▾ <Grandparent>
-    ▾ <Parent>
-        <Child>
-        <Child>
-`;
-
-exports[`TreeListContext tree state should clear selection if the selected element is unmounted: 1: initial state 1`] = `
-Object {
-  "inspectedElementID": null,
-  "numElements": 4,
-  "ownerFlatTree": null,
-  "ownerID": null,
-  "ownerSubtreeLeafElementID": null,
-  "searchIndex": null,
-  "searchResults": Array [],
-  "searchText": "",
-  "selectedElementID": null,
-  "selectedElementIndex": null,
-}
-`;
-
-exports[`TreeListContext tree state should clear selection if the selected element is unmounted: 2: select second child 1`] = `
-Object {
-  "inspectedElementID": 5,
-  "numElements": 4,
-  "ownerFlatTree": null,
-  "ownerID": null,
-  "ownerSubtreeLeafElementID": null,
-  "searchIndex": null,
-  "searchResults": Array [],
-  "searchText": "",
-  "selectedElementID": 5,
-  "selectedElementIndex": 3,
-}
-`;
-
-exports[`TreeListContext tree state should clear selection if the selected element is unmounted: 3: remove children (parent should now be selected) 1`] = `
-Object {
-  "inspectedElementID": 3,
-  "numElements": 2,
-  "ownerFlatTree": null,
-  "ownerID": null,
-  "ownerSubtreeLeafElementID": null,
-  "searchIndex": null,
-  "searchResults": Array [],
-  "searchText": "",
-  "selectedElementID": 3,
-  "selectedElementIndex": 1,
-}
-`;
-
-exports[`TreeListContext tree state should clear selection if the selected element is unmounted: 4: unmount root (nothing should be selected) 1`] = `
-Object {
-  "inspectedElementID": null,
-  "numElements": 0,
-  "ownerFlatTree": null,
-  "ownerID": null,
-  "ownerSubtreeLeafElementID": null,
-  "searchIndex": null,
-  "searchResults": Array [],
-  "searchText": "",
-  "selectedElementID": null,
-  "selectedElementIndex": null,
-}
-`;
-
-exports[`TreeListContext tree state should navigate next/previous sibling and skip over children in between: 0: mount 1`] = `
-[root]
-  ▾ <Grandparent>
-    ▾ <Parent>
-        <Child key="0">
-    ▾ <Parent>
-        <Child key="0">
-        <Child key="1">
-        <Child key="2">
-    ▾ <Parent>
-        <Child key="0">
-        <Child key="1">
-`;
-
-exports[`TreeListContext tree state should navigate the owner hierarchy: 0: mount 1`] = `
-[root]
-  ▾ <Grandparent>
-    ▾ <Wrapper>
-      ▾ <Parent>
-          <Child key="0">
-    ▾ <Wrapper>
-      ▾ <Parent>
-          <Child key="0">
-          <Child key="1">
-          <Child key="2">
-    ▾ <Wrapper>
-      ▾ <Parent>
-          <Child key="0">
-          <Child key="1">
-`;
-
-exports[`TreeListContext tree state should select child elements: 0: mount 1`] = `
-[root]
-  ▾ <Grandparent>
-    ▾ <Parent>
-        <Child>
-        <Child>
-    ▾ <Parent>
-        <Child>
-        <Child>
-`;
-
-exports[`TreeListContext tree state should select child elements: 1: initial state 1`] = `
-Object {
-  "inspectedElementID": null,
-  "numElements": 7,
-  "ownerFlatTree": null,
-  "ownerID": null,
-  "ownerSubtreeLeafElementID": null,
-  "searchIndex": null,
-  "searchResults": Array [],
-  "searchText": "",
-  "selectedElementID": null,
-  "selectedElementIndex": null,
-}
-`;
-
-exports[`TreeListContext tree state should select child elements: 2: select first element 1`] = `
-Object {
-  "inspectedElementID": 2,
-  "numElements": 7,
-  "ownerFlatTree": null,
-  "ownerID": null,
-  "ownerSubtreeLeafElementID": null,
-  "searchIndex": null,
-  "searchResults": Array [],
-  "searchText": "",
-  "selectedElementID": 2,
-  "selectedElementIndex": 0,
-}
-`;
-
-exports[`TreeListContext tree state should select child elements: 3: select Parent 1`] = `
-Object {
-  "inspectedElementID": 3,
-  "numElements": 7,
-  "ownerFlatTree": null,
-  "ownerID": null,
-  "ownerSubtreeLeafElementID": null,
-  "searchIndex": null,
-  "searchResults": Array [],
-  "searchText": "",
-  "selectedElementID": 3,
-  "selectedElementIndex": 1,
-}
-`;
-
-exports[`TreeListContext tree state should select child elements: 4: select Child 1`] = `
-Object {
-  "inspectedElementID": 4,
-  "numElements": 7,
-  "ownerFlatTree": null,
-  "ownerID": null,
-  "ownerSubtreeLeafElementID": null,
-  "searchIndex": null,
-  "searchResults": Array [],
-  "searchText": "",
-  "selectedElementID": 4,
-  "selectedElementIndex": 2,
-}
-`;
-
-exports[`TreeListContext tree state should select parent elements and then collapse: 0: mount 1`] = `
-[root]
-  ▾ <Grandparent>
-    ▾ <Parent>
-        <Child>
-        <Child>
-    ▾ <Parent>
-        <Child>
-        <Child>
-`;
-
-exports[`TreeListContext tree state should select parent elements and then collapse: 1: initial state 1`] = `
-Object {
-  "inspectedElementID": null,
-  "numElements": 7,
-  "ownerFlatTree": null,
-  "ownerID": null,
-  "ownerSubtreeLeafElementID": null,
-  "searchIndex": null,
-  "searchResults": Array [],
-  "searchText": "",
-  "selectedElementID": null,
-  "selectedElementIndex": null,
-}
-`;
-
-exports[`TreeListContext tree state should select parent elements and then collapse: 2: select last child 1`] = `
-Object {
-  "inspectedElementID": 8,
-  "numElements": 7,
-  "ownerFlatTree": null,
-  "ownerID": null,
-  "ownerSubtreeLeafElementID": null,
-  "searchIndex": null,
-  "searchResults": Array [],
-  "searchText": "",
-  "selectedElementID": 8,
-  "selectedElementIndex": 6,
-}
-`;
-
-exports[`TreeListContext tree state should select parent elements and then collapse: 3: select Parent 1`] = `
-Object {
-  "inspectedElementID": 6,
-  "numElements": 7,
-  "ownerFlatTree": null,
-  "ownerID": null,
-  "ownerSubtreeLeafElementID": null,
-  "searchIndex": null,
-  "searchResults": Array [],
-  "searchText": "",
-  "selectedElementID": 6,
-  "selectedElementIndex": 4,
-}
-`;
-
-exports[`TreeListContext tree state should select parent elements and then collapse: 4: select Grandparent 1`] = `
-Object {
-  "inspectedElementID": 2,
-  "numElements": 7,
-  "ownerFlatTree": null,
-  "ownerID": null,
-  "ownerSubtreeLeafElementID": null,
-  "searchIndex": null,
-  "searchResults": Array [],
-  "searchText": "",
-  "selectedElementID": 2,
-  "selectedElementIndex": 0,
-}
-`;
-
-exports[`TreeListContext tree state should select the next and previous elements in the tree: 0: mount 1`] = `
-[root]
-  ▾ <Grandparent>
-    ▾ <Parent>
-        <Child>
-        <Child>
-`;
-
-exports[`TreeListContext tree state should select the next and previous elements in the tree: 1: initial state 1`] = `
-Object {
-  "inspectedElementID": null,
-  "numElements": 4,
-  "ownerFlatTree": null,
-  "ownerID": null,
-  "ownerSubtreeLeafElementID": null,
-  "searchIndex": null,
-  "searchResults": Array [],
-  "searchText": "",
-  "selectedElementID": null,
-  "selectedElementIndex": null,
-}
-`;
-
-exports[`TreeListContext tree state should select the next and previous elements in the tree: 2: select first element 1`] = `
-Object {
-  "inspectedElementID": 2,
-  "numElements": 4,
-  "ownerFlatTree": null,
-  "ownerID": null,
-  "ownerSubtreeLeafElementID": null,
-  "searchIndex": null,
-  "searchResults": Array [],
-  "searchText": "",
-  "selectedElementID": 2,
-  "selectedElementIndex": 0,
-}
-`;
-
-exports[`TreeListContext tree state should select the next and previous elements in the tree: 3: select element after (0) 1`] = `
-Object {
-  "inspectedElementID": 3,
-  "numElements": 4,
-  "ownerFlatTree": null,
-  "ownerID": null,
-  "ownerSubtreeLeafElementID": null,
-  "searchIndex": null,
-  "searchResults": Array [],
-  "searchText": "",
-  "selectedElementID": 3,
-  "selectedElementIndex": 1,
-}
-`;
-
-exports[`TreeListContext tree state should select the next and previous elements in the tree: 3: select element after (1) 1`] = `
-Object {
-  "inspectedElementID": 4,
-  "numElements": 4,
-  "ownerFlatTree": null,
-  "ownerID": null,
-  "ownerSubtreeLeafElementID": null,
-  "searchIndex": null,
-  "searchResults": Array [],
-  "searchText": "",
-  "selectedElementID": 4,
-  "selectedElementIndex": 2,
-}
-`;
-
-exports[`TreeListContext tree state should select the next and previous elements in the tree: 3: select element after (2) 1`] = `
-Object {
-  "inspectedElementID": 5,
-  "numElements": 4,
-  "ownerFlatTree": null,
-  "ownerID": null,
-  "ownerSubtreeLeafElementID": null,
-  "searchIndex": null,
-  "searchResults": Array [],
-  "searchText": "",
-  "selectedElementID": 5,
-  "selectedElementIndex": 3,
-}
-`;
-
-exports[`TreeListContext tree state should select the next and previous elements in the tree: 4: select element before (1) 1`] = `
-Object {
-  "inspectedElementID": 2,
-  "numElements": 4,
-  "ownerFlatTree": null,
-  "ownerID": null,
-  "ownerSubtreeLeafElementID": null,
-  "searchIndex": null,
-  "searchResults": Array [],
-  "searchText": "",
-  "selectedElementID": 2,
-  "selectedElementIndex": 0,
-}
-`;
-
-exports[`TreeListContext tree state should select the next and previous elements in the tree: 4: select element before (2) 1`] = `
-Object {
-  "inspectedElementID": 3,
-  "numElements": 4,
-  "ownerFlatTree": null,
-  "ownerID": null,
-  "ownerSubtreeLeafElementID": null,
-  "searchIndex": null,
-  "searchResults": Array [],
-  "searchText": "",
-  "selectedElementID": 3,
-  "selectedElementIndex": 1,
-}
-`;
-
-exports[`TreeListContext tree state should select the next and previous elements in the tree: 4: select element before (3) 1`] = `
-Object {
-  "inspectedElementID": 4,
-  "numElements": 4,
-  "ownerFlatTree": null,
-  "ownerID": null,
-  "ownerSubtreeLeafElementID": null,
-  "searchIndex": null,
-  "searchResults": Array [],
-  "searchText": "",
-  "selectedElementID": 4,
-  "selectedElementIndex": 2,
-}
-`;
-
-exports[`TreeListContext tree state should select the next and previous elements in the tree: 5: select previous wraps around to last 1`] = `
-Object {
-  "inspectedElementID": 5,
-  "numElements": 4,
-  "ownerFlatTree": null,
-  "ownerID": null,
-  "ownerSubtreeLeafElementID": null,
-  "searchIndex": null,
-  "searchResults": Array [],
-  "searchText": "",
-  "selectedElementID": 5,
-  "selectedElementIndex": 3,
-}
-`;
-
-exports[`TreeListContext tree state should select the next and previous elements in the tree: 6: select next wraps around to first 1`] = `
-Object {
-  "inspectedElementID": 2,
-  "numElements": 4,
-  "ownerFlatTree": null,
-  "ownerID": null,
-  "ownerSubtreeLeafElementID": null,
-  "searchIndex": null,
-  "searchResults": Array [],
-  "searchText": "",
-  "selectedElementID": 2,
-  "selectedElementIndex": 0,
-}
-`;
diff --git a/packages/react-devtools-shared/src/__tests__/inspectedElementContext-test.js b/packages/react-devtools-shared/src/__tests__/inspectedElementContext-test.js
index f08a3d9d68074..153bb43801ba2 100644
--- a/packages/react-devtools-shared/src/__tests__/inspectedElementContext-test.js
+++ b/packages/react-devtools-shared/src/__tests__/inspectedElementContext-test.js
@@ -15,6 +15,7 @@ import type {
 } from 'react-devtools-shared/src/devtools/views/Components/InspectedElementContext';
 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', () => {
   let React;
@@ -66,6 +67,10 @@ describe('InspectedElementContext', () => {
       .TreeContextController;
   });
 
+  afterEach(() => {
+    jest.restoreAllMocks();
+  });
+
   const Contexts = ({
     children,
     defaultSelectedElementID = null,
@@ -386,10 +391,10 @@ describe('InspectedElementContext', () => {
   it('should temporarily disable console logging when re-running a component to inspect its hooks', async done => {
     let targetRenderCount = 0;
 
-    const errorSpy = ((console: any).error = jest.fn());
-    const infoSpy = ((console: any).info = jest.fn());
-    const logSpy = ((console: any).log = jest.fn());
-    const warnSpy = ((console: any).warn = jest.fn());
+    jest.spyOn(console, 'error').mockImplementation(() => {});
+    jest.spyOn(console, 'info').mockImplementation(() => {});
+    jest.spyOn(console, 'log').mockImplementation(() => {});
+    jest.spyOn(console, 'warn').mockImplementation(() => {});
 
     const Target = React.memo(props => {
       targetRenderCount++;
@@ -407,14 +412,14 @@ describe('InspectedElementContext', () => {
     );
 
     expect(targetRenderCount).toBe(1);
-    expect(errorSpy).toHaveBeenCalledTimes(1);
-    expect(errorSpy).toHaveBeenCalledWith('error');
-    expect(infoSpy).toHaveBeenCalledTimes(1);
-    expect(infoSpy).toHaveBeenCalledWith('info');
-    expect(logSpy).toHaveBeenCalledTimes(1);
-    expect(logSpy).toHaveBeenCalledWith('log');
-    expect(warnSpy).toHaveBeenCalledTimes(1);
-    expect(warnSpy).toHaveBeenCalledWith('warn');
+    expect(console.error).toHaveBeenCalledTimes(1);
+    expect(console.error).toHaveBeenCalledWith('error');
+    expect(console.info).toHaveBeenCalledTimes(1);
+    expect(console.info).toHaveBeenCalledWith('info');
+    expect(console.log).toHaveBeenCalledTimes(1);
+    expect(console.log).toHaveBeenCalledWith('log');
+    expect(console.warn).toHaveBeenCalledTimes(1);
+    expect(console.warn).toHaveBeenCalledWith('warn');
 
     const id = ((store.getElementIDAtIndex(0): any): number);
 
@@ -442,10 +447,10 @@ describe('InspectedElementContext', () => {
 
     expect(inspectedElement).not.toBe(null);
     expect(targetRenderCount).toBe(2);
-    expect(errorSpy).toHaveBeenCalledTimes(1);
-    expect(infoSpy).toHaveBeenCalledTimes(1);
-    expect(logSpy).toHaveBeenCalledTimes(1);
-    expect(warnSpy).toHaveBeenCalledTimes(1);
+    expect(console.error).toHaveBeenCalledTimes(1);
+    expect(console.info).toHaveBeenCalledTimes(1);
+    expect(console.log).toHaveBeenCalledTimes(1);
+    expect(console.warn).toHaveBeenCalledTimes(1);
 
     done();
   });
@@ -1569,21 +1574,20 @@ describe('InspectedElementContext', () => {
     );
     expect(storeAsGlobal).not.toBeNull();
 
-    const logSpy = jest.fn();
-    spyOn(console, 'log').and.callFake(logSpy);
+    jest.spyOn(console, 'log').mockImplementation(() => {});
 
     // Should store the whole value (not just the hydrated parts)
     storeAsGlobal(id, ['props', 'nestedObject']);
     jest.runOnlyPendingTimers();
-    expect(logSpy).toHaveBeenCalledWith('$reactTemp1');
+    expect(console.log).toHaveBeenCalledWith('$reactTemp1');
     expect(global.$reactTemp1).toBe(nestedObject);
 
-    logSpy.mockReset();
+    console.log.mockReset();
 
     // Should store the nested property specified (not just the outer value)
     storeAsGlobal(id, ['props', 'nestedObject', 'a', 'b']);
     jest.runOnlyPendingTimers();
-    expect(logSpy).toHaveBeenCalledWith('$reactTemp2');
+    expect(console.log).toHaveBeenCalledWith('$reactTemp2');
     expect(global.$reactTemp2).toBe(nestedObject.a.b);
 
     done();
@@ -1805,4 +1809,421 @@ describe('InspectedElementContext', () => {
 
     done();
   });
+
+  describe('inline errors and warnings', () => {
+    // Some actions require the Fiber id.
+    // In those instances you might want to make assertions based on the ID instead of the index.
+    function getErrorsAndWarningsForElement(id: number) {
+      const index = ((store.getIndexOfElementID(id): any): number);
+      return getErrorsAndWarningsForElementAtIndex(index);
+    }
+
+    async function getErrorsAndWarningsForElementAtIndex(index) {
+      const id = ((store.getElementIDAtIndex(index): any): number);
+
+      let errors = null;
+      let warnings = null;
+
+      function Suspender({target}) {
+        const {getInspectedElement} = React.useContext(InspectedElementContext);
+        const inspectedElement = getInspectedElement(id);
+        errors = inspectedElement.errors;
+        warnings = inspectedElement.warnings;
+        return null;
+      }
+
+      let root;
+      await utils.actAsync(() => {
+        root = TestRenderer.create(
+          <Contexts
+            defaultSelectedElementID={id}
+            defaultSelectedElementIndex={index}>
+            <React.Suspense fallback={null}>
+              <Suspender target={id} />
+            </React.Suspense>
+          </Contexts>,
+        );
+      }, false);
+      await utils.actAsync(() => {
+        root.unmount();
+      }, false);
+
+      return {errors, warnings};
+    }
+
+    it('during render get recorded', async () => {
+      const Example = () => {
+        console.error('test-only: render error');
+        console.warn('test-only: render warning');
+        return null;
+      };
+
+      const container = document.createElement('div');
+
+      await withErrorsOrWarningsIgnored(['test-only: '], async () => {
+        await utils.actAsync(() =>
+          ReactDOM.render(<Example repeatWarningCount={1} />, container),
+        );
+      });
+
+      const data = await getErrorsAndWarningsForElementAtIndex(0);
+      expect(data).toMatchInlineSnapshot(`
+        Object {
+          "errors": Array [
+            Array [
+              "test-only: render error",
+              1,
+            ],
+          ],
+          "warnings": Array [
+            Array [
+              "test-only: render warning",
+              1,
+            ],
+          ],
+        }
+      `);
+    });
+
+    it('during render get deduped', async () => {
+      const Example = () => {
+        console.error('test-only: render error');
+        console.error('test-only: render error');
+        console.warn('test-only: render warning');
+        console.warn('test-only: render warning');
+        console.warn('test-only: render warning');
+        return null;
+      };
+
+      const container = document.createElement('div');
+      await utils.withErrorsOrWarningsIgnored(['test-only:'], async () => {
+        await utils.actAsync(() =>
+          ReactDOM.render(<Example repeatWarningCount={1} />, container),
+        );
+      });
+      const data = await getErrorsAndWarningsForElementAtIndex(0);
+      expect(data).toMatchInlineSnapshot(`
+        Object {
+          "errors": Array [
+            Array [
+              "test-only: render error",
+              2,
+            ],
+          ],
+          "warnings": Array [
+            Array [
+              "test-only: render warning",
+              3,
+            ],
+          ],
+        }
+      `);
+    });
+
+    it('during layout (mount) get recorded', async () => {
+      const Example = () => {
+        // Note we only test mount because once the component unmounts,
+        // it is no longer in the store and warnings are ignored.
+        React.useLayoutEffect(() => {
+          console.error('test-only: useLayoutEffect error');
+          console.warn('test-only: useLayoutEffect warning');
+        }, []);
+        return null;
+      };
+
+      const container = document.createElement('div');
+      await utils.withErrorsOrWarningsIgnored(['test-only:'], async () => {
+        await utils.actAsync(() =>
+          ReactDOM.render(<Example repeatWarningCount={1} />, container),
+        );
+      });
+
+      const data = await getErrorsAndWarningsForElementAtIndex(0);
+      expect(data).toMatchInlineSnapshot(`
+        Object {
+          "errors": Array [
+            Array [
+              "test-only: useLayoutEffect error",
+              1,
+            ],
+          ],
+          "warnings": Array [
+            Array [
+              "test-only: useLayoutEffect warning",
+              1,
+            ],
+          ],
+        }
+      `);
+    });
+
+    it('during passive (mount) get recorded', async () => {
+      const Example = () => {
+        // Note we only test mount because once the component unmounts,
+        // it is no longer in the store and warnings are ignored.
+        React.useEffect(() => {
+          console.error('test-only: useEffect error');
+          console.warn('test-only: useEffect warning');
+        }, []);
+        return null;
+      };
+
+      const container = document.createElement('div');
+      await utils.withErrorsOrWarningsIgnored(['test-only:'], async () => {
+        await utils.actAsync(() =>
+          ReactDOM.render(<Example repeatWarningCount={1} />, container),
+        );
+      });
+
+      const data = await getErrorsAndWarningsForElementAtIndex(0);
+      expect(data).toMatchInlineSnapshot(`
+        Object {
+          "errors": Array [
+            Array [
+              "test-only: useEffect error",
+              1,
+            ],
+          ],
+          "warnings": Array [
+            Array [
+              "test-only: useEffect warning",
+              1,
+            ],
+          ],
+        }
+      `);
+    });
+
+    it('from react get recorded without a component stack', async () => {
+      const Example = () => {
+        return [<div />];
+      };
+
+      const container = document.createElement('div');
+      await utils.withErrorsOrWarningsIgnored(
+        ['Warning: Each child in a list should have a unique "key" prop.'],
+        async () => {
+          await utils.actAsync(() =>
+            ReactDOM.render(<Example repeatWarningCount={1} />, container),
+          );
+        },
+      );
+
+      const data = await getErrorsAndWarningsForElementAtIndex(0);
+      expect(data).toMatchInlineSnapshot(`
+        Object {
+          "errors": Array [
+            Array [
+              "Warning: Each child in a list should have a unique \\"key\\" prop. See https://reactjs.org/link/warning-keys for more information.",
+              1,
+            ],
+          ],
+          "warnings": Array [],
+        }
+      `);
+    });
+
+    it('can be cleared for the whole app', async () => {
+      const Example = () => {
+        console.error('test-only: render error');
+        console.warn('test-only: render warning');
+        return null;
+      };
+
+      const container = document.createElement('div');
+      await utils.withErrorsOrWarningsIgnored(['test-only:'], async () => {
+        await utils.actAsync(() =>
+          ReactDOM.render(<Example repeatWarningCount={1} />, container),
+        );
+      });
+
+      store.clearErrorsAndWarnings();
+      // Flush events to the renderer.
+      jest.runOnlyPendingTimers();
+
+      const data = await getErrorsAndWarningsForElementAtIndex(0);
+      expect(data).toMatchInlineSnapshot(`
+        Object {
+          "errors": Array [],
+          "warnings": Array [],
+        }
+      `);
+    });
+
+    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}`);
+        return null;
+      };
+
+      const container = document.createElement('div');
+      await utils.withErrorsOrWarningsIgnored(['test-only:'], async () => {
+        await utils.actAsync(() =>
+          ReactDOM.render(
+            <React.Fragment>
+              <Example id={1} />
+              <Example id={2} />
+            </React.Fragment>,
+            container,
+          ),
+        );
+      });
+
+      store.clearWarningsForElement(2);
+      // Flush events to the renderer.
+      jest.runOnlyPendingTimers();
+
+      let data = [
+        await getErrorsAndWarningsForElement(1),
+        await getErrorsAndWarningsForElement(2),
+      ];
+      expect(data).toMatchInlineSnapshot(`
+        Array [
+          Object {
+            "errors": Array [
+              Array [
+                "test-only: render error #1",
+                1,
+              ],
+            ],
+            "warnings": Array [
+              Array [
+                "test-only: render warning #1",
+                1,
+              ],
+            ],
+          },
+          Object {
+            "errors": Array [
+              Array [
+                "test-only: render error #2",
+                1,
+              ],
+            ],
+            "warnings": Array [],
+          },
+        ]
+      `);
+
+      store.clearWarningsForElement(1);
+      // Flush events to the renderer.
+      jest.runOnlyPendingTimers();
+
+      data = [
+        await getErrorsAndWarningsForElement(1),
+        await getErrorsAndWarningsForElement(2),
+      ];
+      expect(data).toMatchInlineSnapshot(`
+        Array [
+          Object {
+            "errors": Array [
+              Array [
+                "test-only: render error #1",
+                1,
+              ],
+            ],
+            "warnings": Array [],
+          },
+          Object {
+            "errors": Array [
+              Array [
+                "test-only: render error #2",
+                1,
+              ],
+            ],
+            "warnings": Array [],
+          },
+        ]
+      `);
+    });
+
+    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}`);
+        return null;
+      };
+
+      const container = document.createElement('div');
+      await utils.withErrorsOrWarningsIgnored(['test-only:'], async () => {
+        await utils.actAsync(() =>
+          ReactDOM.render(
+            <React.Fragment>
+              <Example id={1} />
+              <Example id={2} />
+            </React.Fragment>,
+            container,
+          ),
+        );
+      });
+
+      store.clearErrorsForElement(2);
+      // Flush events to the renderer.
+      jest.runOnlyPendingTimers();
+
+      let data = [
+        await getErrorsAndWarningsForElement(1),
+        await getErrorsAndWarningsForElement(2),
+      ];
+      expect(data).toMatchInlineSnapshot(`
+        Array [
+          Object {
+            "errors": Array [
+              Array [
+                "test-only: render error #1",
+                1,
+              ],
+            ],
+            "warnings": Array [
+              Array [
+                "test-only: render warning #1",
+                1,
+              ],
+            ],
+          },
+          Object {
+            "errors": Array [],
+            "warnings": Array [
+              Array [
+                "test-only: render warning #2",
+                1,
+              ],
+            ],
+          },
+        ]
+      `);
+
+      store.clearErrorsForElement(1);
+      // Flush events to the renderer.
+      jest.runOnlyPendingTimers();
+
+      data = [
+        await getErrorsAndWarningsForElement(1),
+        await getErrorsAndWarningsForElement(2),
+      ];
+      expect(data).toMatchInlineSnapshot(`
+        Array [
+          Object {
+            "errors": Array [],
+            "warnings": Array [
+              Array [
+                "test-only: render warning #1",
+                1,
+              ],
+            ],
+          },
+          Object {
+            "errors": Array [],
+            "warnings": Array [
+              Array [
+                "test-only: render warning #2",
+                1,
+              ],
+            ],
+          },
+        ]
+      `);
+    });
+  });
 });
diff --git a/packages/react-devtools-shared/src/__tests__/profilerStore-test.js b/packages/react-devtools-shared/src/__tests__/profilerStore-test.js
index 3a6a78597935a..dca154ad170c6 100644
--- a/packages/react-devtools-shared/src/__tests__/profilerStore-test.js
+++ b/packages/react-devtools-shared/src/__tests__/profilerStore-test.js
@@ -120,4 +120,19 @@ describe('ProfilerStore', () => {
     expect(data.commitData).toHaveLength(1);
     expect(data.operations).toHaveLength(1);
   });
+
+  it('should throw if component filters are modified while profiling', () => {
+    utils.act(() => store.profilerStore.startProfiling());
+
+    expect(() => {
+      utils.act(() => {
+        const {
+          ElementTypeHostComponent,
+        } = require('react-devtools-shared/src/types');
+        store.componentFilters = [
+          utils.createElementTypeFilter(ElementTypeHostComponent),
+        ];
+      });
+    }).toThrow('Cannot modify filter preferences while profiling');
+  });
 });
diff --git a/packages/react-devtools-shared/src/__tests__/setupTests.js b/packages/react-devtools-shared/src/__tests__/setupTests.js
index e1f7f13f76b2c..b244c7e0cf17e 100644
--- a/packages/react-devtools-shared/src/__tests__/setupTests.js
+++ b/packages/react-devtools-shared/src/__tests__/setupTests.js
@@ -33,11 +33,24 @@ env.beforeEach(() => {
   const {
     getDefaultComponentFilters,
     saveComponentFilters,
+    setShowInlineWarningsAndErrors,
   } = require('react-devtools-shared/src/utils');
 
   // Fake timers let us flush Bridge operations between setup and assertions.
   jest.useFakeTimers();
 
+  // Use utils.js#withErrorsOrWarningsIgnored instead of directly mutating this array.
+  global._ignoredErrorOrWarningMessages = [];
+  function shouldIgnoreConsoleErrorOrWarn(args) {
+    const firstArg = args[0];
+    if (typeof firstArg !== 'string') {
+      return false;
+    }
+    return global._ignoredErrorOrWarningMessages.some(errorOrWarningMessage => {
+      return firstArg.indexOf(errorOrWarningMessage) !== -1;
+    });
+  }
+
   const originalConsoleError = console.error;
   // $FlowFixMe
   console.error = (...args) => {
@@ -54,14 +67,32 @@ env.beforeEach(() => {
       // DevTools intentionally wraps updates with acts from both DOM and test-renderer,
       // since test updates are expected to impact both renderers.
       return;
+    } else if (shouldIgnoreConsoleErrorOrWarn(args)) {
+      // Allows testing how DevTools behaves when it encounters console.error without cluttering the test output.
+      // Errors can be ignored by running in a special context provided by utils.js#withErrorsOrWarningsIgnored
+      return;
     }
     originalConsoleError.apply(console, args);
   };
+  const originalConsoleWarn = console.warn;
+  // $FlowFixMe
+  console.warn = (...args) => {
+    if (shouldIgnoreConsoleErrorOrWarn(args)) {
+      // Allows testing how DevTools behaves when it encounters console.warn without cluttering the test output.
+      // Warnings can be ignored by running in a special context provided by utils.js#withErrorsOrWarningsIgnored
+      return;
+    }
+    originalConsoleWarn.apply(console, args);
+  };
 
   // Initialize filters to a known good state.
   saveComponentFilters(getDefaultComponentFilters());
   global.__REACT_DEVTOOLS_COMPONENT_FILTERS__ = getDefaultComponentFilters();
 
+  // Also initialize inline warnings so that we can test them.
+  setShowInlineWarningsAndErrors(true);
+  global.__REACT_DEVTOOLS_SHOW_INLINE_WARNINGS_AND_ERRORS__ = true;
+
   installHook(global);
 
   const bridgeListeners = [];
diff --git a/packages/react-devtools-shared/src/__tests__/store-test.js b/packages/react-devtools-shared/src/__tests__/store-test.js
index 92face2f37655..33d3bc0a072a3 100644
--- a/packages/react-devtools-shared/src/__tests__/store-test.js
+++ b/packages/react-devtools-shared/src/__tests__/store-test.js
@@ -14,6 +14,7 @@ describe('Store', () => {
   let act;
   let getRendererID;
   let store;
+  let withErrorsOrWarningsIgnored;
 
   beforeEach(() => {
     agent = global.agent;
@@ -25,6 +26,7 @@ describe('Store', () => {
     const utils = require('./utils');
     act = utils.act;
     getRendererID = utils.getRendererID;
+    withErrorsOrWarningsIgnored = utils.withErrorsOrWarningsIgnored;
   });
 
   it('should not allow a root node to be collapsed', () => {
@@ -1008,4 +1010,316 @@ describe('Store', () => {
       done();
     });
   });
+
+  describe('inline errors and warnings', () => {
+    it('during render are counted', () => {
+      function Example() {
+        console.error('test-only: render error');
+        console.warn('test-only: render warning');
+        return null;
+      }
+      const container = document.createElement('div');
+
+      withErrorsOrWarningsIgnored(['test-only:'], () => {
+        act(() => ReactDOM.render(<Example />, container));
+      });
+
+      expect(store).toMatchInlineSnapshot(`
+        ✕ 1, ⚠ 1
+        [root]
+            <Example> ✕⚠
+      `);
+
+      withErrorsOrWarningsIgnored(['test-only:'], () => {
+        act(() => ReactDOM.render(<Example rerender={1} />, container));
+      });
+
+      expect(store).toMatchInlineSnapshot(`
+        ✕ 2, ⚠ 2
+        [root]
+            <Example> ✕⚠
+      `);
+    });
+
+    it('during layout get counted', () => {
+      function Example() {
+        React.useLayoutEffect(() => {
+          console.error('test-only: layout error');
+          console.warn('test-only: layout warning');
+        });
+        return null;
+      }
+      const container = document.createElement('div');
+
+      withErrorsOrWarningsIgnored(['test-only:'], () => {
+        act(() => ReactDOM.render(<Example />, container));
+      });
+
+      expect(store).toMatchInlineSnapshot(`
+        ✕ 1, ⚠ 1
+        [root]
+            <Example> ✕⚠
+      `);
+
+      withErrorsOrWarningsIgnored(['test-only:'], () => {
+        act(() => ReactDOM.render(<Example rerender={1} />, container));
+      });
+
+      expect(store).toMatchInlineSnapshot(`
+        ✕ 2, ⚠ 2
+        [root]
+            <Example> ✕⚠
+      `);
+    });
+
+    // This is not great, but it seems safer than potentially flushing between commits.
+    // Our logic for determining how to handle e.g. suspended trees or error boundaries
+    // is built on the assumption that we're evaluating the results of a commit, not an in-progress render.
+    it('during passive get counted (but not until the next commit)', () => {
+      function Example() {
+        React.useEffect(() => {
+          console.error('test-only: passive error');
+          console.warn('test-only: passive warning');
+        });
+        return null;
+      }
+      const container = document.createElement('div');
+
+      withErrorsOrWarningsIgnored(['test-only:'], () => {
+        act(() => ReactDOM.render(<Example />, container));
+      });
+
+      expect(store).toMatchInlineSnapshot(`
+        [root]
+            <Example>
+      `);
+
+      withErrorsOrWarningsIgnored(['test-only:'], () => {
+        act(() => ReactDOM.render(<Example rerender={1} />, container));
+      });
+
+      expect(store).toMatchInlineSnapshot(`
+        ✕ 1, ⚠ 1
+        [root]
+            <Example> ✕⚠
+      `);
+
+      act(() => ReactDOM.unmountComponentAtNode(container));
+      expect(store).toMatchInlineSnapshot(``);
+    });
+
+    it('from react get counted', () => {
+      const container = document.createElement('div');
+      function Example() {
+        return [<Child />];
+      }
+      function Child() {
+        return null;
+      }
+
+      withErrorsOrWarningsIgnored(
+        ['Warning: Each child in a list should have a unique "key" prop'],
+        () => {
+          act(() => ReactDOM.render(<Example />, container));
+        },
+      );
+
+      expect(store).toMatchInlineSnapshot(`
+        ✕ 1, ⚠ 0
+        [root]
+          ▾ <Example> ✕
+              <Child>
+      `);
+    });
+
+    it('can be cleared for the whole app', () => {
+      function Example() {
+        console.error('test-only: render error');
+        console.warn('test-only: render warning');
+        return null;
+      }
+      const container = document.createElement('div');
+      withErrorsOrWarningsIgnored(['test-only:'], () => {
+        act(() =>
+          ReactDOM.render(
+            <React.Fragment>
+              <Example />
+              <Example />
+            </React.Fragment>,
+            container,
+          ),
+        );
+      });
+
+      expect(store).toMatchInlineSnapshot(`
+        ✕ 2, ⚠ 2
+        [root]
+            <Example> ✕⚠
+            <Example> ✕⚠
+      `);
+
+      store.clearErrorsAndWarnings();
+      // flush events to the renderer
+      jest.runAllTimers();
+
+      expect(store).toMatchInlineSnapshot(`
+        [root]
+            <Example>
+            <Example>
+      `);
+    });
+
+    it('can be cleared for particular Fiber (only warnings)', () => {
+      function Example() {
+        console.error('test-only: render error');
+        console.warn('test-only: render warning');
+        return null;
+      }
+      const container = document.createElement('div');
+      withErrorsOrWarningsIgnored(['test-only:'], () => {
+        act(() =>
+          ReactDOM.render(
+            <React.Fragment>
+              <Example />
+              <Example />
+            </React.Fragment>,
+            container,
+          ),
+        );
+      });
+
+      expect(store).toMatchInlineSnapshot(`
+        ✕ 2, ⚠ 2
+        [root]
+            <Example> ✕⚠
+            <Example> ✕⚠
+      `);
+
+      store.clearWarningsForElement(2);
+      // Flush events to the renderer.
+      jest.runAllTimers();
+
+      expect(store).toMatchInlineSnapshot(`
+        ✕ 2, ⚠ 1
+        [root]
+            <Example> ✕⚠
+            <Example> ✕
+      `);
+    });
+
+    it('can be cleared for a particular Fiber (only errors)', () => {
+      function Example() {
+        console.error('test-only: render error');
+        console.warn('test-only: render warning');
+        return null;
+      }
+      const container = document.createElement('div');
+      withErrorsOrWarningsIgnored(['test-only:'], () => {
+        act(() =>
+          ReactDOM.render(
+            <React.Fragment>
+              <Example />
+              <Example />
+            </React.Fragment>,
+            container,
+          ),
+        );
+      });
+
+      expect(store).toMatchInlineSnapshot(`
+        ✕ 2, ⚠ 2
+        [root]
+            <Example> ✕⚠
+            <Example> ✕⚠
+      `);
+
+      store.clearErrorsForElement(2);
+      // Flush events to the renderer.
+      jest.runAllTimers();
+
+      expect(store).toMatchInlineSnapshot(`
+        ✕ 1, ⚠ 2
+        [root]
+            <Example> ✕⚠
+            <Example> ⚠
+      `);
+    });
+
+    it('are updated when fibers are removed from the tree', () => {
+      function ComponentWithWarning() {
+        console.warn('test-only: render warning');
+        return null;
+      }
+      function ComponentWithError() {
+        console.error('test-only: render error');
+        return null;
+      }
+      function ComponentWithWarningAndError() {
+        console.error('test-only: render error');
+        console.warn('test-only: render warning');
+        return null;
+      }
+      const container = document.createElement('div');
+      withErrorsOrWarningsIgnored(['test-only:'], () => {
+        act(() =>
+          ReactDOM.render(
+            <React.Fragment>
+              <ComponentWithError />
+              <ComponentWithWarning />
+              <ComponentWithWarningAndError />
+            </React.Fragment>,
+            container,
+          ),
+        );
+      });
+      expect(store).toMatchInlineSnapshot(`
+        ✕ 2, ⚠ 2
+        [root]
+            <ComponentWithError> ✕
+            <ComponentWithWarning> ⚠
+            <ComponentWithWarningAndError> ✕⚠
+      `);
+
+      withErrorsOrWarningsIgnored(['test-only:'], () => {
+        act(() =>
+          ReactDOM.render(
+            <React.Fragment>
+              <ComponentWithWarning />
+              <ComponentWithWarningAndError />
+            </React.Fragment>,
+            container,
+          ),
+        );
+      });
+      expect(store).toMatchInlineSnapshot(`
+        ✕ 1, ⚠ 2
+        [root]
+            <ComponentWithWarning> ⚠
+            <ComponentWithWarningAndError> ✕⚠
+      `);
+
+      withErrorsOrWarningsIgnored(['test-only:'], () => {
+        act(() =>
+          ReactDOM.render(
+            <React.Fragment>
+              <ComponentWithWarning />
+            </React.Fragment>,
+            container,
+          ),
+        );
+      });
+      expect(store).toMatchInlineSnapshot(`
+        ✕ 0, ⚠ 2
+        [root]
+            <ComponentWithWarning> ⚠
+      `);
+
+      withErrorsOrWarningsIgnored(['test-only:'], () => {
+        act(() => ReactDOM.render(<React.Fragment />, container));
+      });
+      expect(store).toMatchInlineSnapshot(`[root]`);
+      expect(store.errorCount).toBe(0);
+      expect(store.warningCount).toBe(0);
+    });
+  });
 });
diff --git a/packages/react-devtools-shared/src/__tests__/storeComponentFilters-test.js b/packages/react-devtools-shared/src/__tests__/storeComponentFilters-test.js
index a8a0e151ef748..4a365acb0ef3c 100644
--- a/packages/react-devtools-shared/src/__tests__/storeComponentFilters-test.js
+++ b/packages/react-devtools-shared/src/__tests__/storeComponentFilters-test.js
@@ -48,22 +48,28 @@ describe('Store component filters', () => {
   });
 
   it('should support filtering by element type', () => {
-    class Root extends React.Component<{|children: React$Node|}> {
+    class ClassComponent extends React.Component<{|children: React$Node|}> {
       render() {
         return <div>{this.props.children}</div>;
       }
     }
-    const Component = () => <div>Hi</div>;
+    const FunctionComponent = () => <div>Hi</div>;
 
     act(() =>
       ReactDOM.render(
-        <Root>
-          <Component />
-        </Root>,
+        <ClassComponent>
+          <FunctionComponent />
+        </ClassComponent>,
         document.createElement('div'),
       ),
     );
-    expect(store).toMatchSnapshot('1: mount');
+    expect(store).toMatchInlineSnapshot(`
+      [root]
+        ▾ <ClassComponent>
+          ▾ <div>
+            ▾ <FunctionComponent>
+                <div>
+    `);
 
     act(
       () =>
@@ -71,8 +77,11 @@ describe('Store component filters', () => {
           utils.createElementTypeFilter(Types.ElementTypeHostComponent),
         ]),
     );
-
-    expect(store).toMatchSnapshot('2: hide host components');
+    expect(store).toMatchInlineSnapshot(`
+      [root]
+        ▾ <ClassComponent>
+            <FunctionComponent>
+    `);
 
     act(
       () =>
@@ -80,8 +89,12 @@ describe('Store component filters', () => {
           utils.createElementTypeFilter(Types.ElementTypeClass),
         ]),
     );
-
-    expect(store).toMatchSnapshot('3: hide class components');
+    expect(store).toMatchInlineSnapshot(`
+      [root]
+        ▾ <div>
+          ▾ <FunctionComponent>
+              <div>
+    `);
 
     act(
       () =>
@@ -90,8 +103,11 @@ describe('Store component filters', () => {
           utils.createElementTypeFilter(Types.ElementTypeFunction),
         ]),
     );
-
-    expect(store).toMatchSnapshot('4: hide class and function components');
+    expect(store).toMatchInlineSnapshot(`
+      [root]
+        ▾ <div>
+            <div>
+    `);
 
     act(
       () =>
@@ -100,15 +116,33 @@ describe('Store component filters', () => {
           utils.createElementTypeFilter(Types.ElementTypeFunction, false),
         ]),
     );
-
-    expect(store).toMatchSnapshot('5: disable all filters');
+    expect(store).toMatchInlineSnapshot(`
+      [root]
+        ▾ <ClassComponent>
+          ▾ <div>
+            ▾ <FunctionComponent>
+                <div>
+    `);
+
+    act(() => (store.componentFilters = []));
+    expect(store).toMatchInlineSnapshot(`
+      [root]
+        ▾ <ClassComponent>
+          ▾ <div>
+            ▾ <FunctionComponent>
+                <div>
+    `);
   });
 
   it('should ignore invalid ElementTypeRoot filter', () => {
-    const Root = () => <div>Hi</div>;
+    const Component = () => <div>Hi</div>;
 
-    act(() => ReactDOM.render(<Root />, document.createElement('div')));
-    expect(store).toMatchSnapshot('1: mount');
+    act(() => ReactDOM.render(<Component />, document.createElement('div')));
+    expect(store).toMatchInlineSnapshot(`
+      [root]
+        ▾ <Component>
+            <div>
+    `);
 
     act(
       () =>
@@ -117,7 +151,11 @@ describe('Store component filters', () => {
         ]),
     );
 
-    expect(store).toMatchSnapshot('2: add invalid filter');
+    expect(store).toMatchInlineSnapshot(`
+      [root]
+        ▾ <Component>
+            <div>
+    `);
   });
 
   it('should filter by display name', () => {
@@ -136,27 +174,59 @@ describe('Store component filters', () => {
         document.createElement('div'),
       ),
     );
-    expect(store).toMatchSnapshot('1: mount');
+    expect(store).toMatchInlineSnapshot(`
+      [root]
+        ▾ <Foo>
+            <Text>
+        ▾ <Bar>
+            <Text>
+        ▾ <Baz>
+            <Text>
+    `);
 
     act(
       () => (store.componentFilters = [utils.createDisplayNameFilter('Foo')]),
     );
-    expect(store).toMatchSnapshot('2: filter "Foo"');
+    expect(store).toMatchInlineSnapshot(`
+      [root]
+          <Text>
+        ▾ <Bar>
+            <Text>
+        ▾ <Baz>
+            <Text>
+    `);
 
     act(() => (store.componentFilters = [utils.createDisplayNameFilter('Ba')]));
-    expect(store).toMatchSnapshot('3: filter "Ba"');
+    expect(store).toMatchInlineSnapshot(`
+      [root]
+        ▾ <Foo>
+            <Text>
+          <Text>
+          <Text>
+    `);
 
     act(
       () => (store.componentFilters = [utils.createDisplayNameFilter('B.z')]),
     );
-    expect(store).toMatchSnapshot('4: filter "B.z"');
+    expect(store).toMatchInlineSnapshot(`
+      [root]
+        ▾ <Foo>
+            <Text>
+        ▾ <Bar>
+            <Text>
+          <Text>
+    `);
   });
 
   it('should filter by path', () => {
     const Component = () => <div>Hi</div>;
 
     act(() => ReactDOM.render(<Component />, document.createElement('div')));
-    expect(store).toMatchSnapshot('1: mount');
+    expect(store).toMatchInlineSnapshot(`
+      [root]
+        ▾ <Component>
+            <div>
+    `);
 
     act(
       () =>
@@ -165,9 +235,7 @@ describe('Store component filters', () => {
         ]),
     );
 
-    expect(store).toMatchSnapshot(
-      '2: hide all components declared within this test filed',
-    );
+    expect(store).toMatchInlineSnapshot(`[root]`);
 
     act(
       () =>
@@ -176,7 +244,11 @@ describe('Store component filters', () => {
         ]),
     );
 
-    expect(store).toMatchSnapshot('3: hide components in a made up fake path');
+    expect(store).toMatchInlineSnapshot(`
+      [root]
+        ▾ <Component>
+            <div>
+    `);
   });
 
   it('should filter HOCs', () => {
@@ -187,15 +259,29 @@ describe('Store component filters', () => {
     Bar.displayName = 'Bar(Foo(Component))';
 
     act(() => ReactDOM.render(<Bar />, document.createElement('div')));
-    expect(store).toMatchSnapshot('1: mount');
+    expect(store).toMatchInlineSnapshot(`
+      [root]
+        ▾ <Component> [Bar][Foo]
+          ▾ <Component> [Foo]
+            ▾ <Component>
+                <div>
+    `);
 
     act(() => (store.componentFilters = [utils.createHOCFilter(true)]));
-
-    expect(store).toMatchSnapshot('2: hide all HOCs');
+    expect(store).toMatchInlineSnapshot(`
+      [root]
+        ▾ <Component>
+            <div>
+    `);
 
     act(() => (store.componentFilters = [utils.createHOCFilter(false)]));
-
-    expect(store).toMatchSnapshot('3: disable HOC filter');
+    expect(store).toMatchInlineSnapshot(`
+      [root]
+        ▾ <Component> [Bar][Foo]
+          ▾ <Component> [Foo]
+            ▾ <Component>
+                <div>
+    `);
   });
 
   it('should not send a bridge update if the set of enabled filters has not changed', () => {
@@ -252,12 +338,117 @@ describe('Store component filters', () => {
 
     const container = document.createElement('div');
     act(() => ReactDOM.render(<Wrapper shouldSuspend={true} />, container));
-    expect(store).toMatchSnapshot('1: suspended');
+    expect(store).toMatchInlineSnapshot(`
+      [root]
+        ▾ <Wrapper>
+          ▾ <Loading>
+              <div>
+    `);
 
     act(() => ReactDOM.render(<Wrapper shouldSuspend={false} />, container));
-    expect(store).toMatchSnapshot('2: resolved');
+    expect(store).toMatchInlineSnapshot(`
+      [root]
+        ▾ <Wrapper>
+            <Component>
+    `);
 
     act(() => ReactDOM.render(<Wrapper shouldSuspend={true} />, container));
-    expect(store).toMatchSnapshot('3: suspended');
+    expect(store).toMatchInlineSnapshot(`
+      [root]
+        ▾ <Wrapper>
+          ▾ <Loading>
+              <div>
+    `);
+  });
+
+  describe('inline errors and warnings', () => {
+    it('only counts for unfiltered components', () => {
+      function ComponentWithWarning() {
+        console.warn('test-only: render warning');
+        return null;
+      }
+      function ComponentWithError() {
+        console.error('test-only: render error');
+        return null;
+      }
+      function ComponentWithWarningAndError() {
+        console.error('test-only: render error');
+        console.warn('test-only: render warning');
+        return null;
+      }
+      const container = document.createElement('div');
+      utils.withErrorsOrWarningsIgnored(['test-only:'], () => {
+        act(
+          () =>
+            (store.componentFilters = [
+              utils.createDisplayNameFilter('Warning'),
+              utils.createDisplayNameFilter('Error'),
+            ]),
+        );
+        act(() =>
+          ReactDOM.render(
+            <React.Fragment>
+              <ComponentWithError />
+              <ComponentWithWarning />
+              <ComponentWithWarningAndError />
+            </React.Fragment>,
+            container,
+          ),
+        );
+      });
+
+      expect(store).toMatchInlineSnapshot(`[root]`);
+      expect(store.errorCount).toBe(0);
+      expect(store.warningCount).toBe(0);
+
+      act(() => (store.componentFilters = []));
+      expect(store).toMatchInlineSnapshot(`
+        ✕ 2, ⚠ 2
+        [root]
+            <ComponentWithError> ✕
+            <ComponentWithWarning> ⚠
+            <ComponentWithWarningAndError> ✕⚠
+      `);
+
+      act(
+        () =>
+          (store.componentFilters = [utils.createDisplayNameFilter('Warning')]),
+      );
+      expect(store).toMatchInlineSnapshot(`
+        ✕ 1, ⚠ 0
+        [root]
+            <ComponentWithError> ✕
+      `);
+
+      act(
+        () =>
+          (store.componentFilters = [utils.createDisplayNameFilter('Error')]),
+      );
+      expect(store).toMatchInlineSnapshot(`
+        ✕ 0, ⚠ 1
+        [root]
+            <ComponentWithWarning> ⚠
+      `);
+
+      act(
+        () =>
+          (store.componentFilters = [
+            utils.createDisplayNameFilter('Warning'),
+            utils.createDisplayNameFilter('Error'),
+          ]),
+      );
+      expect(store).toMatchInlineSnapshot(`[root]`);
+      expect(store.errorCount).toBe(0);
+      expect(store.warningCount).toBe(0);
+
+      act(() => (store.componentFilters = []));
+      expect(store).toMatchInlineSnapshot(`
+        ✕ 2, ⚠ 2
+        [root]
+            <ComponentWithError> ✕
+            <ComponentWithWarning> ⚠
+            <ComponentWithWarningAndError> ✕⚠
+      `);
+    });
   });
 });
diff --git a/packages/react-devtools-shared/src/__tests__/treeContext-test.js b/packages/react-devtools-shared/src/__tests__/treeContext-test.js
index 0b672f91415f6..8c057fb720807 100644
--- a/packages/react-devtools-shared/src/__tests__/treeContext-test.js
+++ b/packages/react-devtools-shared/src/__tests__/treeContext-test.js
@@ -22,6 +22,7 @@ describe('TreeListContext', () => {
   let bridge: FrontendBridge;
   let store: Store;
   let utils;
+  let withErrorsOrWarningsIgnored;
 
   let BridgeContext;
   let StoreContext;
@@ -34,6 +35,8 @@ describe('TreeListContext', () => {
     utils = require('./utils');
     utils.beforeEachProfiling();
 
+    withErrorsOrWarningsIgnored = utils.withErrorsOrWarningsIgnored;
+
     bridge = global.bridge;
     store = global.store;
     store.collapseNodesByDefault = false;
@@ -88,43 +91,111 @@ describe('TreeListContext', () => {
         ReactDOM.render(<Grandparent />, document.createElement('div')),
       );
 
-      expect(store).toMatchSnapshot('0: mount');
-
       let renderer;
       utils.act(() => (renderer = TestRenderer.create(<Contexts />)));
-      expect(state).toMatchSnapshot('1: initial state');
+      expect(state).toMatchInlineSnapshot(`
+        [root]
+           ▾ <Grandparent>
+             ▾ <Parent>
+                 <Child>
+                 <Child>
+      `);
+
+      // Test stepping through to the end
 
       utils.act(() => dispatch({type: 'SELECT_NEXT_ELEMENT_IN_TREE'}));
       utils.act(() => renderer.update(<Contexts />));
-      expect(state).toMatchSnapshot('2: select first element');
+      expect(state).toMatchInlineSnapshot(`
+        [root]
+        →  ▾ <Grandparent>
+             ▾ <Parent>
+                 <Child>
+                 <Child>
+      `);
 
-      while (
-        state.selectedElementIndex !== null &&
-        state.selectedElementIndex < store.numElements - 1
-      ) {
-        const index = ((state.selectedElementIndex: any): number);
-        utils.act(() => dispatch({type: 'SELECT_NEXT_ELEMENT_IN_TREE'}));
-        utils.act(() => renderer.update(<Contexts />));
-        expect(state).toMatchSnapshot(`3: select element after (${index})`);
-      }
+      utils.act(() => dispatch({type: 'SELECT_NEXT_ELEMENT_IN_TREE'}));
+      utils.act(() => renderer.update(<Contexts />));
+      expect(state).toMatchInlineSnapshot(`
+        [root]
+           ▾ <Grandparent>
+        →    ▾ <Parent>
+                 <Child>
+                 <Child>
+      `);
 
-      while (
-        state.selectedElementIndex !== null &&
-        state.selectedElementIndex > 0
-      ) {
-        const index = ((state.selectedElementIndex: any): number);
-        utils.act(() => dispatch({type: 'SELECT_PREVIOUS_ELEMENT_IN_TREE'}));
-        utils.act(() => renderer.update(<Contexts />));
-        expect(state).toMatchSnapshot(`4: select element before (${index})`);
-      }
+      utils.act(() => dispatch({type: 'SELECT_NEXT_ELEMENT_IN_TREE'}));
+      utils.act(() => renderer.update(<Contexts />));
+      expect(state).toMatchInlineSnapshot(`
+        [root]
+           ▾ <Grandparent>
+             ▾ <Parent>
+        →        <Child>
+                 <Child>
+      `);
+
+      utils.act(() => dispatch({type: 'SELECT_NEXT_ELEMENT_IN_TREE'}));
+      utils.act(() => renderer.update(<Contexts />));
+      expect(state).toMatchInlineSnapshot(`
+        [root]
+           ▾ <Grandparent>
+             ▾ <Parent>
+                 <Child>
+        →        <Child>
+      `);
+
+      // Test stepping back to the beginning
+
+      utils.act(() => dispatch({type: 'SELECT_PREVIOUS_ELEMENT_IN_TREE'}));
+      utils.act(() => renderer.update(<Contexts />));
+      expect(state).toMatchInlineSnapshot(`
+        [root]
+           ▾ <Grandparent>
+             ▾ <Parent>
+        →        <Child>
+                 <Child>
+      `);
+
+      utils.act(() => dispatch({type: 'SELECT_PREVIOUS_ELEMENT_IN_TREE'}));
+      utils.act(() => renderer.update(<Contexts />));
+      expect(state).toMatchInlineSnapshot(`
+        [root]
+           ▾ <Grandparent>
+        →    ▾ <Parent>
+                 <Child>
+                 <Child>
+      `);
+
+      utils.act(() => dispatch({type: 'SELECT_PREVIOUS_ELEMENT_IN_TREE'}));
+      utils.act(() => renderer.update(<Contexts />));
+      expect(state).toMatchInlineSnapshot(`
+        [root]
+        →  ▾ <Grandparent>
+             ▾ <Parent>
+                 <Child>
+                 <Child>
+      `);
+
+      // Test wrap around behavior
 
       utils.act(() => dispatch({type: 'SELECT_PREVIOUS_ELEMENT_IN_TREE'}));
       utils.act(() => renderer.update(<Contexts />));
-      expect(state).toMatchSnapshot('5: select previous wraps around to last');
+      expect(state).toMatchInlineSnapshot(`
+        [root]
+           ▾ <Grandparent>
+             ▾ <Parent>
+                 <Child>
+        →        <Child>
+      `);
 
       utils.act(() => dispatch({type: 'SELECT_NEXT_ELEMENT_IN_TREE'}));
       utils.act(() => renderer.update(<Contexts />));
-      expect(state).toMatchSnapshot('6: select next wraps around to first');
+      expect(state).toMatchInlineSnapshot(`
+        [root]
+        →  ▾ <Grandparent>
+             ▾ <Parent>
+                 <Child>
+                 <Child>
+      `);
     });
 
     it('should select child elements', () => {
@@ -146,30 +217,71 @@ describe('TreeListContext', () => {
         ReactDOM.render(<Grandparent />, document.createElement('div')),
       );
 
-      expect(store).toMatchSnapshot('0: mount');
-
       let renderer;
       utils.act(() => (renderer = TestRenderer.create(<Contexts />)));
-      expect(state).toMatchSnapshot('1: initial state');
+      expect(state).toMatchInlineSnapshot(`
+        [root]
+           ▾ <Grandparent>
+             ▾ <Parent>
+                 <Child>
+                 <Child>
+             ▾ <Parent>
+                 <Child>
+                 <Child>
+      `);
 
       utils.act(() => dispatch({type: 'SELECT_ELEMENT_AT_INDEX', payload: 0}));
       utils.act(() => renderer.update(<Contexts />));
-      expect(state).toMatchSnapshot('2: select first element');
+      expect(state).toMatchInlineSnapshot(`
+        [root]
+        →  ▾ <Grandparent>
+             ▾ <Parent>
+                 <Child>
+                 <Child>
+             ▾ <Parent>
+                 <Child>
+                 <Child>
+      `);
 
       utils.act(() => dispatch({type: 'SELECT_CHILD_ELEMENT_IN_TREE'}));
       utils.act(() => renderer.update(<Contexts />));
-      expect(state).toMatchSnapshot('3: select Parent');
+      expect(state).toMatchInlineSnapshot(`
+        [root]
+           ▾ <Grandparent>
+        →    ▾ <Parent>
+                 <Child>
+                 <Child>
+             ▾ <Parent>
+                 <Child>
+                 <Child>
+      `);
 
       utils.act(() => dispatch({type: 'SELECT_CHILD_ELEMENT_IN_TREE'}));
       utils.act(() => renderer.update(<Contexts />));
-      expect(state).toMatchSnapshot('4: select Child');
-
-      const previousState = state;
+      expect(state).toMatchInlineSnapshot(`
+        [root]
+           ▾ <Grandparent>
+             ▾ <Parent>
+        →        <Child>
+                 <Child>
+             ▾ <Parent>
+                 <Child>
+                 <Child>
+      `);
 
       // There are no more children to select, so this should be a no-op
       utils.act(() => dispatch({type: 'SELECT_CHILD_ELEMENT_IN_TREE'}));
       utils.act(() => renderer.update(<Contexts />));
-      expect(state).toEqual(previousState);
+      expect(state).toMatchInlineSnapshot(`
+        [root]
+           ▾ <Grandparent>
+             ▾ <Parent>
+        →        <Child>
+                 <Child>
+             ▾ <Parent>
+                 <Child>
+                 <Child>
+      `);
     });
 
     it('should select parent elements and then collapse', () => {
@@ -191,27 +303,78 @@ describe('TreeListContext', () => {
         ReactDOM.render(<Grandparent />, document.createElement('div')),
       );
 
-      expect(store).toMatchSnapshot('0: mount');
-
       let renderer;
       utils.act(() => (renderer = TestRenderer.create(<Contexts />)));
-      expect(state).toMatchSnapshot('1: initial state');
+      expect(state).toMatchInlineSnapshot(`
+        [root]
+           ▾ <Grandparent>
+             ▾ <Parent>
+                 <Child>
+                 <Child>
+             ▾ <Parent>
+                 <Child>
+                 <Child>
+      `);
 
       const lastChildID = store.getElementIDAtIndex(store.numElements - 1);
 
+      // Select the last child
       utils.act(() =>
         dispatch({type: 'SELECT_ELEMENT_BY_ID', payload: lastChildID}),
       );
       utils.act(() => renderer.update(<Contexts />));
-      expect(state).toMatchSnapshot('2: select last child');
-
+      expect(state).toMatchInlineSnapshot(`
+        [root]
+           ▾ <Grandparent>
+             ▾ <Parent>
+                 <Child>
+                 <Child>
+             ▾ <Parent>
+                 <Child>
+        →        <Child>
+      `);
+
+      // Select its parent
       utils.act(() => dispatch({type: 'SELECT_PARENT_ELEMENT_IN_TREE'}));
       utils.act(() => renderer.update(<Contexts />));
-      expect(state).toMatchSnapshot('3: select Parent');
-
+      expect(state).toMatchInlineSnapshot(`
+        [root]
+           ▾ <Grandparent>
+             ▾ <Parent>
+                 <Child>
+                 <Child>
+        →    ▾ <Parent>
+                 <Child>
+                 <Child>
+      `);
+
+      // Select grandparent
       utils.act(() => dispatch({type: 'SELECT_PARENT_ELEMENT_IN_TREE'}));
       utils.act(() => renderer.update(<Contexts />));
-      expect(state).toMatchSnapshot('4: select Grandparent');
+      expect(state).toMatchInlineSnapshot(`
+        [root]
+        →  ▾ <Grandparent>
+             ▾ <Parent>
+                 <Child>
+                 <Child>
+             ▾ <Parent>
+                 <Child>
+                 <Child>
+      `);
+
+      // No-op
+      utils.act(() => dispatch({type: 'SELECT_PARENT_ELEMENT_IN_TREE'}));
+      utils.act(() => renderer.update(<Contexts />));
+      expect(state).toMatchInlineSnapshot(`
+        [root]
+        →  ▾ <Grandparent>
+             ▾ <Parent>
+                 <Child>
+                 <Child>
+             ▾ <Parent>
+                 <Child>
+                 <Child>
+      `);
 
       const previousState = state;
 
@@ -221,7 +384,7 @@ describe('TreeListContext', () => {
       expect(state).toEqual(previousState);
     });
 
-    it('should clear selection if the selected element is unmounted', async done => {
+    it('should clear selection if the selected element is unmounted', async () => {
       const Grandparent = props => props.children || null;
       const Parent = props => props.children || null;
       const Child = () => null;
@@ -239,16 +402,28 @@ describe('TreeListContext', () => {
         ),
       );
 
-      expect(store).toMatchSnapshot('0: mount');
-
       let renderer;
       utils.act(() => (renderer = TestRenderer.create(<Contexts />)));
-      expect(state).toMatchSnapshot('1: initial state');
-
+      expect(state).toMatchInlineSnapshot(`
+        [root]
+           ▾ <Grandparent>
+             ▾ <Parent>
+                 <Child>
+                 <Child>
+      `);
+
+      // Select the second child
       utils.act(() => dispatch({type: 'SELECT_ELEMENT_AT_INDEX', payload: 3}));
       utils.act(() => renderer.update(<Contexts />));
-      expect(state).toMatchSnapshot('2: select second child');
-
+      expect(state).toMatchInlineSnapshot(`
+        [root]
+           ▾ <Grandparent>
+             ▾ <Parent>
+                 <Child>
+        →        <Child>
+      `);
+
+      // Remove the child (which should auto-select the parent)
       await utils.actAsync(() =>
         ReactDOM.render(
           <Grandparent>
@@ -257,16 +432,15 @@ describe('TreeListContext', () => {
           container,
         ),
       );
-      expect(state).toMatchSnapshot(
-        '3: remove children (parent should now be selected)',
-      );
+      expect(state).toMatchInlineSnapshot(`
+        [root]
+           ▾ <Grandparent>
+        →      <Parent>
+      `);
 
+      // Unmount the root (so that nothing is selected)
       await utils.actAsync(() => ReactDOM.unmountComponentAtNode(container));
-      expect(state).toMatchSnapshot(
-        '4: unmount root (nothing should be selected)',
-      );
-
-      done();
+      expect(state).toMatchInlineSnapshot(``);
     });
 
     it('should navigate next/previous sibling and skip over children in between', () => {
@@ -287,21 +461,6 @@ describe('TreeListContext', () => {
         ReactDOM.render(<Grandparent />, document.createElement('div')),
       );
 
-      /*
-       * 0  ▾ <Grandparent>
-       * 1    ▾ <Parent>
-       * 2        <Child key="0">
-       * 3    ▾ <Parent>
-       * 4        <Child key="0">
-       * 5        <Child key="1">
-       * 6        <Child key="2">
-       * 7    ▾ <Parent>
-       * 8        <Child key="0">
-       * 9        <Child key="1">
-       */
-
-      expect(store).toMatchSnapshot('0: mount');
-
       let renderer;
       utils.act(() => (renderer = TestRenderer.create(<Contexts />)));
 
@@ -311,31 +470,131 @@ describe('TreeListContext', () => {
         dispatch({type: 'SELECT_ELEMENT_BY_ID', payload: firstParentID}),
       );
       utils.act(() => renderer.update(<Contexts />));
-      expect(state.selectedElementIndex).toBe(1);
+      expect(state).toMatchInlineSnapshot(`
+        [root]
+           ▾ <Grandparent>
+        →    ▾ <Parent>
+                 <Child key="0">
+             ▾ <Parent>
+                 <Child key="0">
+                 <Child key="1">
+                 <Child key="2">
+             ▾ <Parent>
+                 <Child key="0">
+                 <Child key="1">
+      `);
 
       utils.act(() => dispatch({type: 'SELECT_NEXT_SIBLING_IN_TREE'}));
       utils.act(() => renderer.update(<Contexts />));
-      expect(state.selectedElementIndex).toBe(3);
+      expect(state).toMatchInlineSnapshot(`
+        [root]
+           ▾ <Grandparent>
+             ▾ <Parent>
+                 <Child key="0">
+        →    ▾ <Parent>
+                 <Child key="0">
+                 <Child key="1">
+                 <Child key="2">
+             ▾ <Parent>
+                 <Child key="0">
+                 <Child key="1">
+      `);
 
       utils.act(() => dispatch({type: 'SELECT_NEXT_SIBLING_IN_TREE'}));
       utils.act(() => renderer.update(<Contexts />));
-      expect(state.selectedElementIndex).toBe(7);
+      expect(state).toMatchInlineSnapshot(`
+        [root]
+           ▾ <Grandparent>
+             ▾ <Parent>
+                 <Child key="0">
+             ▾ <Parent>
+                 <Child key="0">
+                 <Child key="1">
+                 <Child key="2">
+        →    ▾ <Parent>
+                 <Child key="0">
+                 <Child key="1">
+      `);
 
       utils.act(() => dispatch({type: 'SELECT_NEXT_SIBLING_IN_TREE'}));
       utils.act(() => renderer.update(<Contexts />));
-      expect(state.selectedElementIndex).toBe(1);
+      expect(state).toMatchInlineSnapshot(`
+        [root]
+           ▾ <Grandparent>
+        →    ▾ <Parent>
+                 <Child key="0">
+             ▾ <Parent>
+                 <Child key="0">
+                 <Child key="1">
+                 <Child key="2">
+             ▾ <Parent>
+                 <Child key="0">
+                 <Child key="1">
+      `);
+
+      utils.act(() => dispatch({type: 'SELECT_PREVIOUS_SIBLING_IN_TREE'}));
+      utils.act(() => renderer.update(<Contexts />));
+      expect(state).toMatchInlineSnapshot(`
+        [root]
+           ▾ <Grandparent>
+             ▾ <Parent>
+                 <Child key="0">
+             ▾ <Parent>
+                 <Child key="0">
+                 <Child key="1">
+                 <Child key="2">
+        →    ▾ <Parent>
+                 <Child key="0">
+                 <Child key="1">
+      `);
 
       utils.act(() => dispatch({type: 'SELECT_PREVIOUS_SIBLING_IN_TREE'}));
       utils.act(() => renderer.update(<Contexts />));
-      expect(state.selectedElementIndex).toBe(7);
+      expect(state).toMatchInlineSnapshot(`
+        [root]
+           ▾ <Grandparent>
+             ▾ <Parent>
+                 <Child key="0">
+        →    ▾ <Parent>
+                 <Child key="0">
+                 <Child key="1">
+                 <Child key="2">
+             ▾ <Parent>
+                 <Child key="0">
+                 <Child key="1">
+      `);
 
       utils.act(() => dispatch({type: 'SELECT_PREVIOUS_SIBLING_IN_TREE'}));
       utils.act(() => renderer.update(<Contexts />));
-      expect(state.selectedElementIndex).toBe(3);
+      expect(state).toMatchInlineSnapshot(`
+        [root]
+           ▾ <Grandparent>
+        →    ▾ <Parent>
+                 <Child key="0">
+             ▾ <Parent>
+                 <Child key="0">
+                 <Child key="1">
+                 <Child key="2">
+             ▾ <Parent>
+                 <Child key="0">
+                 <Child key="1">
+      `);
 
       utils.act(() => dispatch({type: 'SELECT_PREVIOUS_SIBLING_IN_TREE'}));
       utils.act(() => renderer.update(<Contexts />));
-      expect(state.selectedElementIndex).toBe(1);
+      expect(state).toMatchInlineSnapshot(`
+        [root]
+           ▾ <Grandparent>
+             ▾ <Parent>
+                 <Child key="0">
+             ▾ <Parent>
+                 <Child key="0">
+                 <Child key="1">
+                 <Child key="2">
+        →    ▾ <Parent>
+                 <Child key="0">
+                 <Child key="1">
+      `);
     });
 
     it('should navigate the owner hierarchy', () => {
@@ -363,24 +622,6 @@ describe('TreeListContext', () => {
         ReactDOM.render(<Grandparent />, document.createElement('div')),
       );
 
-      /*
-       *  0  ▾ <Grandparent>
-       *  1    ▾ <Wrapper>
-       *  2      ▾ <Parent>
-       *  3          <Child key="0">
-       *  4    ▾ <Wrapper>
-       *  5      ▾ <Parent>
-       *  6          <Child key="0">
-       *  7          <Child key="1">
-       *  8          <Child key="2">
-       *  9    ▾ <Wrapper>
-       * 10      ▾ <Parent>
-       * 11          <Child key="0">
-       * 12          <Child key="1">
-       */
-
-      expect(store).toMatchSnapshot('0: mount');
-
       let renderer;
       utils.act(() => (renderer = TestRenderer.create(<Contexts />)));
 
@@ -389,86 +630,281 @@ describe('TreeListContext', () => {
         dispatch({type: 'SELECT_ELEMENT_BY_ID', payload: childID}),
       );
       utils.act(() => renderer.update(<Contexts />));
-      expect(state.ownerSubtreeLeafElementID).toBeNull();
-      expect(state.selectedElementIndex).toBe(7);
+      expect(state).toMatchInlineSnapshot(`
+        [root]
+           ▾ <Grandparent>
+             ▾ <Wrapper>
+               ▾ <Parent>
+                   <Child key="0">
+             ▾ <Wrapper>
+               ▾ <Parent>
+                   <Child key="0">
+        →          <Child key="1">
+                   <Child key="2">
+             ▾ <Wrapper>
+               ▾ <Parent>
+                   <Child key="0">
+                   <Child key="1">
+      `);
 
       // Basic navigation test
       utils.act(() =>
         dispatch({type: 'SELECT_OWNER_LIST_PREVIOUS_ELEMENT_IN_TREE'}),
       );
       utils.act(() => renderer.update(<Contexts />));
-      expect(state.ownerSubtreeLeafElementID).toBe(childID);
-      expect(state.selectedElementIndex).toBe(5);
+      expect(state).toMatchInlineSnapshot(`
+        [root]
+           ▾ <Grandparent>
+             ▾ <Wrapper>
+               ▾ <Parent>
+                   <Child key="0">
+             ▾ <Wrapper>
+        →      ▾ <Parent>
+                   <Child key="0">
+                   <Child key="1">
+                   <Child key="2">
+             ▾ <Wrapper>
+               ▾ <Parent>
+                   <Child key="0">
+                   <Child key="1">
+      `);
 
       utils.act(() =>
         dispatch({type: 'SELECT_OWNER_LIST_PREVIOUS_ELEMENT_IN_TREE'}),
       );
       utils.act(() => renderer.update(<Contexts />));
-      expect(state.selectedElementIndex).toBe(0);
-
+      expect(state).toMatchInlineSnapshot(`
+        [root]
+        →  ▾ <Grandparent>
+             ▾ <Wrapper>
+               ▾ <Parent>
+                   <Child key="0">
+             ▾ <Wrapper>
+               ▾ <Parent>
+                   <Child key="0">
+                   <Child key="1">
+                   <Child key="2">
+             ▾ <Wrapper>
+               ▾ <Parent>
+                   <Child key="0">
+                   <Child key="1">
+      `);
+
+      // Noop (since we're at the root already)
       utils.act(() =>
         dispatch({type: 'SELECT_OWNER_LIST_PREVIOUS_ELEMENT_IN_TREE'}),
       );
       utils.act(() => renderer.update(<Contexts />));
-      expect(state.selectedElementIndex).toBe(0); // noop since we're at the top
+      expect(state).toMatchInlineSnapshot(`
+        [root]
+        →  ▾ <Grandparent>
+             ▾ <Wrapper>
+               ▾ <Parent>
+                   <Child key="0">
+             ▾ <Wrapper>
+               ▾ <Parent>
+                   <Child key="0">
+                   <Child key="1">
+                   <Child key="2">
+             ▾ <Wrapper>
+               ▾ <Parent>
+                   <Child key="0">
+                   <Child key="1">
+      `);
 
       utils.act(() =>
         dispatch({type: 'SELECT_OWNER_LIST_NEXT_ELEMENT_IN_TREE'}),
       );
       utils.act(() => renderer.update(<Contexts />));
-      expect(state.selectedElementIndex).toBe(5);
+      expect(state).toMatchInlineSnapshot(`
+        [root]
+           ▾ <Grandparent>
+             ▾ <Wrapper>
+               ▾ <Parent>
+                   <Child key="0">
+             ▾ <Wrapper>
+        →      ▾ <Parent>
+                   <Child key="0">
+                   <Child key="1">
+                   <Child key="2">
+             ▾ <Wrapper>
+               ▾ <Parent>
+                   <Child key="0">
+                   <Child key="1">
+      `);
 
       utils.act(() =>
         dispatch({type: 'SELECT_OWNER_LIST_NEXT_ELEMENT_IN_TREE'}),
       );
       utils.act(() => renderer.update(<Contexts />));
-      expect(state.selectedElementIndex).toBe(7);
-
+      expect(state).toMatchInlineSnapshot(`
+        [root]
+           ▾ <Grandparent>
+             ▾ <Wrapper>
+               ▾ <Parent>
+                   <Child key="0">
+             ▾ <Wrapper>
+               ▾ <Parent>
+                   <Child key="0">
+        →          <Child key="1">
+                   <Child key="2">
+             ▾ <Wrapper>
+               ▾ <Parent>
+                   <Child key="0">
+                   <Child key="1">
+      `);
+
+      // Noop (since we're at the leaf node)
       utils.act(() =>
         dispatch({type: 'SELECT_OWNER_LIST_NEXT_ELEMENT_IN_TREE'}),
       );
       utils.act(() => renderer.update(<Contexts />));
-      expect(state.selectedElementIndex).toBe(7); // noop since we're at the leaf node
+      expect(state).toMatchInlineSnapshot(`
+        [root]
+           ▾ <Grandparent>
+             ▾ <Wrapper>
+               ▾ <Parent>
+                   <Child key="0">
+             ▾ <Wrapper>
+               ▾ <Parent>
+                   <Child key="0">
+        →          <Child key="1">
+                   <Child key="2">
+             ▾ <Wrapper>
+               ▾ <Parent>
+                   <Child key="0">
+                   <Child key="1">
+      `);
 
       // Other navigational actions should clear out the temporary owner chain.
       utils.act(() => dispatch({type: 'SELECT_PREVIOUS_ELEMENT_IN_TREE'}));
       utils.act(() => renderer.update(<Contexts />));
-      expect(state.selectedElementIndex).toBe(6);
-      expect(state.ownerSubtreeLeafElementID).toBeNull();
-
+      expect(state).toMatchInlineSnapshot(`
+        [root]
+           ▾ <Grandparent>
+             ▾ <Wrapper>
+               ▾ <Parent>
+                   <Child key="0">
+             ▾ <Wrapper>
+               ▾ <Parent>
+        →          <Child key="0">
+                   <Child key="1">
+                   <Child key="2">
+             ▾ <Wrapper>
+               ▾ <Parent>
+                   <Child key="0">
+                   <Child key="1">
+      `);
+
+      // Start a new tree on parent
       const parentID = ((store.getElementIDAtIndex(5): any): number);
       utils.act(() =>
         dispatch({type: 'SELECT_ELEMENT_BY_ID', payload: parentID}),
       );
       utils.act(() => renderer.update(<Contexts />));
-      expect(state.ownerSubtreeLeafElementID).toBeNull();
-      expect(state.selectedElementIndex).toBe(5);
+      expect(state).toMatchInlineSnapshot(`
+        [root]
+           ▾ <Grandparent>
+             ▾ <Wrapper>
+               ▾ <Parent>
+                   <Child key="0">
+             ▾ <Wrapper>
+        →      ▾ <Parent>
+                   <Child key="0">
+                   <Child key="1">
+                   <Child key="2">
+             ▾ <Wrapper>
+               ▾ <Parent>
+                   <Child key="0">
+                   <Child key="1">
+      `);
 
-      // It should not be possible to navigate beyond the owner chain leaf.
       utils.act(() =>
         dispatch({type: 'SELECT_OWNER_LIST_PREVIOUS_ELEMENT_IN_TREE'}),
       );
       utils.act(() => renderer.update(<Contexts />));
-      expect(state.ownerSubtreeLeafElementID).toBe(parentID);
-      expect(state.selectedElementIndex).toBe(0);
-
+      expect(state).toMatchInlineSnapshot(`
+        [root]
+        →  ▾ <Grandparent>
+             ▾ <Wrapper>
+               ▾ <Parent>
+                   <Child key="0">
+             ▾ <Wrapper>
+               ▾ <Parent>
+                   <Child key="0">
+                   <Child key="1">
+                   <Child key="2">
+             ▾ <Wrapper>
+               ▾ <Parent>
+                   <Child key="0">
+                   <Child key="1">
+      `);
+
+      // Noop (since we're at the top)
       utils.act(() =>
         dispatch({type: 'SELECT_OWNER_LIST_PREVIOUS_ELEMENT_IN_TREE'}),
       );
       utils.act(() => renderer.update(<Contexts />));
-      expect(state.selectedElementIndex).toBe(0); // noop since we're at the top
+      expect(state).toMatchInlineSnapshot(`
+        [root]
+        →  ▾ <Grandparent>
+             ▾ <Wrapper>
+               ▾ <Parent>
+                   <Child key="0">
+             ▾ <Wrapper>
+               ▾ <Parent>
+                   <Child key="0">
+                   <Child key="1">
+                   <Child key="2">
+             ▾ <Wrapper>
+               ▾ <Parent>
+                   <Child key="0">
+                   <Child key="1">
+      `);
 
       utils.act(() =>
         dispatch({type: 'SELECT_OWNER_LIST_NEXT_ELEMENT_IN_TREE'}),
       );
       utils.act(() => renderer.update(<Contexts />));
-      expect(state.selectedElementIndex).toBe(5);
-
+      expect(state).toMatchInlineSnapshot(`
+        [root]
+           ▾ <Grandparent>
+             ▾ <Wrapper>
+               ▾ <Parent>
+                   <Child key="0">
+             ▾ <Wrapper>
+        →      ▾ <Parent>
+                   <Child key="0">
+                   <Child key="1">
+                   <Child key="2">
+             ▾ <Wrapper>
+               ▾ <Parent>
+                   <Child key="0">
+                   <Child key="1">
+      `);
+
+      // Noop (since we're at the leaf of this owner tree)
+      // It should not be possible to navigate beyond the owner chain leaf.
       utils.act(() =>
         dispatch({type: 'SELECT_OWNER_LIST_NEXT_ELEMENT_IN_TREE'}),
       );
       utils.act(() => renderer.update(<Contexts />));
-      expect(state.selectedElementIndex).toBe(5); // noop since we're at the leaf node
+      expect(state).toMatchInlineSnapshot(`
+        [root]
+           ▾ <Grandparent>
+             ▾ <Wrapper>
+               ▾ <Parent>
+                   <Child key="0">
+             ▾ <Wrapper>
+        →      ▾ <Parent>
+                   <Child key="0">
+                   <Child key="1">
+                   <Child key="2">
+             ▾ <Wrapper>
+               ▾ <Parent>
+                   <Child key="0">
+                   <Child key="1">
+      `);
     });
   });
 
@@ -493,31 +929,59 @@ describe('TreeListContext', () => {
         ),
       );
 
-      expect(store).toMatchSnapshot('0: mount');
-
       let renderer;
       utils.act(() => (renderer = TestRenderer.create(<Contexts />)));
-      expect(state).toMatchSnapshot('1: initial state');
+      expect(state).toMatchInlineSnapshot(`
+        [root]
+             <Foo>
+             <Bar>
+             <Baz>
+             <Qux> [withHOC]
+      `);
 
       // NOTE: multi-match
       utils.act(() => dispatch({type: 'SET_SEARCH_TEXT', payload: 'ba'}));
       utils.act(() => renderer.update(<Contexts />));
-      expect(state).toMatchSnapshot('2: search for "ba"');
+      expect(state).toMatchInlineSnapshot(`
+        [root]
+             <Foo>
+        →    <Bar>
+             <Baz>
+             <Qux> [withHOC]
+      `);
 
       // NOTE: single match
       utils.act(() => dispatch({type: 'SET_SEARCH_TEXT', payload: 'f'}));
       utils.act(() => renderer.update(<Contexts />));
-      expect(state).toMatchSnapshot('3: search for "f"');
+      expect(state).toMatchInlineSnapshot(`
+        [root]
+        →    <Foo>
+             <Bar>
+             <Baz>
+             <Qux> [withHOC]
+      `);
 
       // NOTE: no match
       utils.act(() => dispatch({type: 'SET_SEARCH_TEXT', payload: 'y'}));
       utils.act(() => renderer.update(<Contexts />));
-      expect(state).toMatchSnapshot('4: search for "y"');
+      expect(state).toMatchInlineSnapshot(`
+        [root]
+        →    <Foo>
+             <Bar>
+             <Baz>
+             <Qux> [withHOC]
+      `);
 
       // NOTE: HOC match
       utils.act(() => dispatch({type: 'SET_SEARCH_TEXT', payload: 'w'}));
       utils.act(() => renderer.update(<Contexts />));
-      expect(state).toMatchSnapshot('5: search for "w"');
+      expect(state).toMatchInlineSnapshot(`
+        [root]
+             <Foo>
+             <Bar>
+             <Baz>
+        →    <Qux> [withHOC]
+      `);
     });
 
     it('should select the next and previous items within the search results', () => {
@@ -537,42 +1001,95 @@ describe('TreeListContext', () => {
         ),
       );
 
-      expect(store).toMatchSnapshot('0: mount');
-
       let renderer;
       utils.act(() => (renderer = TestRenderer.create(<Contexts />)));
-      expect(state).toMatchSnapshot('1: initial state');
-
+      expect(state).toMatchInlineSnapshot(`
+        [root]
+             <Foo>
+             <Baz>
+             <Bar>
+             <Baz>
+      `);
+
+      // search for "ba"
       utils.act(() => dispatch({type: 'SET_SEARCH_TEXT', payload: 'ba'}));
       utils.act(() => renderer.update(<Contexts />));
-      expect(state).toMatchSnapshot('2: search for "ba"');
-
+      expect(state).toMatchInlineSnapshot(`
+        [root]
+             <Foo>
+        →    <Baz>
+             <Bar>
+             <Baz>
+      `);
+
+      // go to second result
       utils.act(() => dispatch({type: 'GO_TO_NEXT_SEARCH_RESULT'}));
       utils.act(() => renderer.update(<Contexts />));
-      expect(state).toMatchSnapshot('3: go to second result');
-
+      expect(state).toMatchInlineSnapshot(`
+        [root]
+             <Foo>
+             <Baz>
+        →    <Bar>
+             <Baz>
+      `);
+
+      // go to third result
       utils.act(() => dispatch({type: 'GO_TO_NEXT_SEARCH_RESULT'}));
       utils.act(() => renderer.update(<Contexts />));
-      expect(state).toMatchSnapshot('4: go to third result');
-
+      expect(state).toMatchInlineSnapshot(`
+        [root]
+             <Foo>
+             <Baz>
+             <Bar>
+        →    <Baz>
+      `);
+
+      // go to second result
       utils.act(() => dispatch({type: 'GO_TO_PREVIOUS_SEARCH_RESULT'}));
       utils.act(() => renderer.update(<Contexts />));
-      expect(state).toMatchSnapshot('5: go to second result');
-
+      expect(state).toMatchInlineSnapshot(`
+        [root]
+             <Foo>
+             <Baz>
+        →    <Bar>
+             <Baz>
+      `);
+
+      // go to first result
       utils.act(() => dispatch({type: 'GO_TO_PREVIOUS_SEARCH_RESULT'}));
       utils.act(() => renderer.update(<Contexts />));
-      expect(state).toMatchSnapshot('6: go to first result');
-
+      expect(state).toMatchInlineSnapshot(`
+        [root]
+             <Foo>
+        →    <Baz>
+             <Bar>
+             <Baz>
+      `);
+
+      // wrap to last result
       utils.act(() => dispatch({type: 'GO_TO_PREVIOUS_SEARCH_RESULT'}));
       utils.act(() => renderer.update(<Contexts />));
-      expect(state).toMatchSnapshot('7: wrap to last result');
-
+      expect(state).toMatchInlineSnapshot(`
+        [root]
+             <Foo>
+             <Baz>
+             <Bar>
+        →    <Baz>
+      `);
+
+      // wrap to first result
       utils.act(() => dispatch({type: 'GO_TO_NEXT_SEARCH_RESULT'}));
       utils.act(() => renderer.update(<Contexts />));
-      expect(state).toMatchSnapshot('8: wrap to first result');
+      expect(state).toMatchInlineSnapshot(`
+        [root]
+             <Foo>
+        →    <Baz>
+             <Bar>
+             <Baz>
+      `);
     });
 
-    it('should add newly mounted elements to the search results set if they match the current text', async done => {
+    it('should add newly mounted elements to the search results set if they match the current text', async () => {
       const Foo = () => null;
       const Bar = () => null;
       const Baz = () => null;
@@ -589,15 +1106,21 @@ describe('TreeListContext', () => {
         ),
       );
 
-      expect(store).toMatchSnapshot('0: mount');
-
       let renderer;
       utils.act(() => (renderer = TestRenderer.create(<Contexts />)));
-      expect(state).toMatchSnapshot('1: initial state');
+      expect(state).toMatchInlineSnapshot(`
+        [root]
+             <Foo>
+             <Bar>
+      `);
 
       utils.act(() => dispatch({type: 'SET_SEARCH_TEXT', payload: 'ba'}));
       utils.act(() => renderer.update(<Contexts />));
-      expect(state).toMatchSnapshot('2: search for "ba"');
+      expect(state).toMatchInlineSnapshot(`
+        [root]
+             <Foo>
+        →    <Bar>
+      `);
 
       await utils.actAsync(() =>
         ReactDOM.render(
@@ -610,12 +1133,24 @@ describe('TreeListContext', () => {
         ),
       );
       utils.act(() => renderer.update(<Contexts />));
-      expect(state).toMatchSnapshot('3: mount Baz');
+      expect(state).toMatchInlineSnapshot(`
+        [root]
+             <Foo>
+        →    <Bar>
+             <Baz>
+      `);
 
-      done();
+      utils.act(() => dispatch({type: 'GO_TO_NEXT_SEARCH_RESULT'}));
+      utils.act(() => renderer.update(<Contexts />));
+      expect(state).toMatchInlineSnapshot(`
+        [root]
+             <Foo>
+             <Bar>
+        →    <Baz>
+      `);
     });
 
-    it('should remove unmounted elements from the search results set', async done => {
+    it('should remove unmounted elements from the search results set', async () => {
       const Foo = () => null;
       const Bar = () => null;
       const Baz = () => null;
@@ -633,19 +1168,32 @@ describe('TreeListContext', () => {
         ),
       );
 
-      expect(store).toMatchSnapshot('0: mount');
-
       let renderer;
       utils.act(() => (renderer = TestRenderer.create(<Contexts />)));
-      expect(state).toMatchSnapshot('1: initial state');
+      expect(state).toMatchInlineSnapshot(`
+        [root]
+             <Foo>
+             <Bar>
+             <Baz>
+      `);
 
       utils.act(() => dispatch({type: 'SET_SEARCH_TEXT', payload: 'ba'}));
       utils.act(() => renderer.update(<Contexts />));
-      expect(state).toMatchSnapshot('2: search for "ba"');
+      expect(state).toMatchInlineSnapshot(`
+        [root]
+             <Foo>
+        →    <Bar>
+             <Baz>
+      `);
 
       utils.act(() => dispatch({type: 'GO_TO_NEXT_SEARCH_RESULT'}));
       utils.act(() => renderer.update(<Contexts />));
-      expect(state).toMatchSnapshot('3: go to second result');
+      expect(state).toMatchInlineSnapshot(`
+        [root]
+             <Foo>
+             <Bar>
+        →    <Baz>
+      `);
 
       await utils.actAsync(() =>
         ReactDOM.render(
@@ -657,9 +1205,28 @@ describe('TreeListContext', () => {
         ),
       );
       utils.act(() => renderer.update(<Contexts />));
-      expect(state).toMatchSnapshot('4: unmount Baz');
+      expect(state).toMatchInlineSnapshot(`
+        [root]
+             <Foo>
+             <Bar>
+      `);
+
+      utils.act(() => dispatch({type: 'GO_TO_NEXT_SEARCH_RESULT'}));
+      utils.act(() => renderer.update(<Contexts />));
+      expect(state).toMatchInlineSnapshot(`
+        [root]
+             <Foo>
+        →    <Bar>
+      `);
 
-      done();
+      // Noop since the list is now one item long
+      utils.act(() => dispatch({type: 'GO_TO_NEXT_SEARCH_RESULT'}));
+      utils.act(() => renderer.update(<Contexts />));
+      expect(state).toMatchInlineSnapshot(`
+        [root]
+             <Foo>
+        →    <Bar>
+      `);
     });
   });
 
@@ -678,23 +1245,38 @@ describe('TreeListContext', () => {
         ReactDOM.render(<Grandparent />, document.createElement('div')),
       );
 
-      expect(store).toMatchSnapshot('0: mount');
-
       let renderer;
       utils.act(() => (renderer = TestRenderer.create(<Contexts />)));
-      expect(state).toMatchSnapshot('1: initial state');
+      expect(state).toMatchInlineSnapshot(`
+        [root]
+           ▾ <Grandparent>
+             ▾ <Parent>
+                 <Child>
+                 <Child>
+      `);
 
       const parentID = ((store.getElementIDAtIndex(1): any): number);
       utils.act(() => dispatch({type: 'SELECT_OWNER', payload: parentID}));
       utils.act(() => renderer.update(<Contexts />));
-      expect(state).toMatchSnapshot('2: parent owners tree');
+      expect(state).toMatchInlineSnapshot(`
+        [owners]
+        →  ▾ <Parent>
+               <Child>
+               <Child>
+      `);
 
       utils.act(() => dispatch({type: 'RESET_OWNER_STACK'}));
       utils.act(() => renderer.update(<Contexts />));
-      expect(state).toMatchSnapshot('3: final state');
+      expect(state).toMatchInlineSnapshot(`
+        [root]
+           ▾ <Grandparent>
+        →    ▾ <Parent>
+                 <Child>
+                 <Child>
+      `);
     });
 
-    it('should remove an element from the owners list if it is unmounted', async done => {
+    it('should remove an element from the owners list if it is unmounted', async () => {
       const Grandparent = ({count}) => <Parent count={count} />;
       const Parent = ({count}) =>
         new Array(count).fill(true).map((_, index) => <Child key={index} />);
@@ -703,31 +1285,45 @@ describe('TreeListContext', () => {
       const container = document.createElement('div');
       utils.act(() => ReactDOM.render(<Grandparent count={2} />, container));
 
-      expect(store).toMatchSnapshot('0: mount');
-
       let renderer;
       utils.act(() => (renderer = TestRenderer.create(<Contexts />)));
-      expect(state).toMatchSnapshot('1: initial state');
+      expect(state).toMatchInlineSnapshot(`
+        [root]
+           ▾ <Grandparent>
+             ▾ <Parent>
+                 <Child key="0">
+                 <Child key="1">
+      `);
 
       const parentID = ((store.getElementIDAtIndex(1): any): number);
       utils.act(() => dispatch({type: 'SELECT_OWNER', payload: parentID}));
       utils.act(() => renderer.update(<Contexts />));
-      expect(state).toMatchSnapshot('2: parent owners tree');
+      expect(state).toMatchInlineSnapshot(`
+        [owners]
+        →  ▾ <Parent>
+               <Child key="0">
+               <Child key="1">
+      `);
 
       await utils.actAsync(() =>
         ReactDOM.render(<Grandparent count={1} />, container),
       );
-      expect(state).toMatchSnapshot('3: remove second child');
+      expect(state).toMatchInlineSnapshot(`
+        [owners]
+        →  ▾ <Parent>
+               <Child key="0">
+      `);
 
       await utils.actAsync(() =>
         ReactDOM.render(<Grandparent count={0} />, container),
       );
-      expect(state).toMatchSnapshot('4: remove first child');
-
-      done();
+      expect(state).toMatchInlineSnapshot(`
+        [owners]
+        →    <Parent>
+      `);
     });
 
-    it('should exit the owners list if the current owner is unmounted', async done => {
+    it('should exit the owners list if the current owner is unmounted', async () => {
       const Parent = props => props.children || null;
       const Child = () => null;
 
@@ -741,29 +1337,38 @@ describe('TreeListContext', () => {
         ),
       );
 
-      expect(store).toMatchSnapshot('0: mount');
-
       let renderer;
       utils.act(() => (renderer = TestRenderer.create(<Contexts />)));
-      expect(state).toMatchSnapshot('1: initial state');
+      expect(state).toMatchInlineSnapshot(`
+        [root]
+           ▾ <Parent>
+               <Child>
+      `);
 
       const childID = ((store.getElementIDAtIndex(1): any): number);
       utils.act(() => dispatch({type: 'SELECT_OWNER', payload: childID}));
       utils.act(() => renderer.update(<Contexts />));
-      expect(state).toMatchSnapshot('2: child owners tree');
+      expect(state).toMatchInlineSnapshot(`
+        [owners]
+        →    <Child>
+      `);
 
       await utils.actAsync(() => ReactDOM.render(<Parent />, container));
-      expect(state).toMatchSnapshot('3: remove child');
+      expect(state).toMatchInlineSnapshot(`
+        [root]
+        →    <Parent>
+      `);
 
       const parentID = ((store.getElementIDAtIndex(0): any): number);
       utils.act(() => dispatch({type: 'SELECT_OWNER', payload: parentID}));
       utils.act(() => renderer.update(<Contexts />));
-      expect(state).toMatchSnapshot('4: parent owners tree');
+      expect(state).toMatchInlineSnapshot(`
+        [owners]
+        →    <Parent>
+      `);
 
       await utils.actAsync(() => ReactDOM.unmountComponentAtNode(container));
-      expect(state).toMatchSnapshot('5: unmount root');
-
-      done();
+      expect(state).toMatchInlineSnapshot(``);
     });
 
     // This tests ensures support for toggling Suspense boundaries outside of the active owners list.
@@ -783,11 +1388,16 @@ describe('TreeListContext', () => {
       const container = document.createElement('div');
       utils.act(() => ReactDOM.render(<Parent />, container));
 
-      expect(store).toMatchSnapshot('0: mount');
-
       let renderer;
       utils.act(() => (renderer = TestRenderer.create(<Contexts />)));
-      expect(state).toMatchSnapshot('1: initial state');
+      expect(state).toMatchInlineSnapshot(`
+        [root]
+           ▾ <Parent>
+             ▾ <Suspense>
+               ▾ <Child>
+                 ▾ <Suspense>
+                     <Grandchild>
+      `);
 
       const outerSuspenseID = ((store.getElementIDAtIndex(1): any): number);
       const childID = ((store.getElementIDAtIndex(2): any): number);
@@ -795,21 +1405,1212 @@ describe('TreeListContext', () => {
 
       utils.act(() => dispatch({type: 'SELECT_OWNER', payload: childID}));
       utils.act(() => renderer.update(<Contexts />));
-      expect(state).toMatchSnapshot('2: child owners tree');
+      expect(state).toMatchInlineSnapshot(`
+        [owners]
+        →  ▾ <Child>
+             ▾ <Suspense>
+                 <Grandchild>
+      `);
 
       // Toggling a Suspense boundary inside of the flat list should update selected index
       utils.act(() =>
         dispatch({type: 'SELECT_ELEMENT_BY_ID', payload: innerSuspenseID}),
       );
       utils.act(() => renderer.update(<Contexts />));
-      expect(state).toMatchSnapshot('3: child owners tree');
+      expect(state).toMatchInlineSnapshot(`
+        [owners]
+           ▾ <Child>
+        →    ▾ <Suspense>
+                 <Grandchild>
+      `);
 
       // Toggling a Suspense boundary outside of the flat list should exit owners list and update index
       utils.act(() =>
         dispatch({type: 'SELECT_ELEMENT_BY_ID', payload: outerSuspenseID}),
       );
       utils.act(() => renderer.update(<Contexts />));
-      expect(state).toMatchSnapshot('4: main tree');
+      expect(state).toMatchInlineSnapshot(`
+        [root]
+           ▾ <Parent>
+        →    ▾ <Suspense>
+               ▾ <Child>
+                 ▾ <Suspense>
+                     <Grandchild>
+      `);
+    });
+  });
+
+  describe('inline errors/warnings state', () => {
+    function clearAllErrors() {
+      utils.act(() => store.clearErrorsAndWarnings());
+      // flush events to the renderer
+      jest.runAllTimers();
+    }
+
+    function clearErrorsForElement(id) {
+      utils.act(() => store.clearErrorsForElement(id));
+      // flush events to the renderer
+      jest.runAllTimers();
+    }
+
+    function clearWarningsForElement(id) {
+      utils.act(() => store.clearWarningsForElement(id));
+      // flush events to the renderer
+      jest.runAllTimers();
+    }
+
+    function selectNextErrorOrWarning() {
+      utils.act(() =>
+        dispatch({type: 'SELECT_NEXT_ELEMENT_WITH_ERROR_OR_WARNING_IN_TREE'}),
+      );
+    }
+
+    function selectPreviousErrorOrWarning() {
+      utils.act(() =>
+        dispatch({
+          type: 'SELECT_PREVIOUS_ELEMENT_WITH_ERROR_OR_WARNING_IN_TREE',
+        }),
+      );
+    }
+
+    function Child({logError = false, logWarning = false}) {
+      if (logError === true) {
+        console.error('test-only: error');
+      }
+      if (logWarning === true) {
+        console.warn('test-only: warning');
+      }
+      return null;
+    }
+
+    it('should handle when there are no errors/warnings', () => {
+      utils.act(() =>
+        ReactDOM.render(
+          <React.Fragment>
+            <Child />
+            <Child />
+            <Child />
+          </React.Fragment>,
+          document.createElement('div'),
+        ),
+      );
+
+      utils.act(() => TestRenderer.create(<Contexts />));
+
+      expect(state).toMatchInlineSnapshot(`
+        [root]
+             <Child>
+             <Child>
+             <Child>
+      `);
+
+      // Next/previous errors should be a no-op
+      selectPreviousErrorOrWarning();
+      expect(state).toMatchInlineSnapshot(`
+        [root]
+             <Child>
+             <Child>
+             <Child>
+      `);
+      selectNextErrorOrWarning();
+      expect(state).toMatchInlineSnapshot(`
+        [root]
+             <Child>
+             <Child>
+             <Child>
+      `);
+
+      utils.act(() => dispatch({type: 'SELECT_ELEMENT_AT_INDEX', payload: 0}));
+      expect(state).toMatchInlineSnapshot(`
+        [root]
+        →    <Child>
+             <Child>
+             <Child>
+      `);
+
+      // Next/previous errors should still be a no-op
+      selectPreviousErrorOrWarning();
+      expect(state).toMatchInlineSnapshot(`
+        [root]
+        →    <Child>
+             <Child>
+             <Child>
+      `);
+      selectNextErrorOrWarning();
+      expect(state).toMatchInlineSnapshot(`
+        [root]
+        →    <Child>
+             <Child>
+             <Child>
+      `);
+    });
+
+    it('should cycle through the next errors/warnings and wrap around', () => {
+      withErrorsOrWarningsIgnored(['test-only:'], () =>
+        utils.act(() =>
+          ReactDOM.render(
+            <React.Fragment>
+              <Child />
+              <Child logWarning={true} />
+              <Child />
+              <Child logError={true} />
+              <Child />
+            </React.Fragment>,
+            document.createElement('div'),
+          ),
+        ),
+      );
+
+      utils.act(() => TestRenderer.create(<Contexts />));
+      expect(state).toMatchInlineSnapshot(`
+        ✕ 1, ⚠ 1
+        [root]
+             <Child>
+             <Child> ⚠
+             <Child>
+             <Child> ✕
+             <Child>
+      `);
+
+      selectNextErrorOrWarning();
+      expect(state).toMatchInlineSnapshot(`
+        ✕ 1, ⚠ 1
+        [root]
+             <Child>
+        →    <Child> ⚠
+             <Child>
+             <Child> ✕
+             <Child>
+      `);
+
+      selectNextErrorOrWarning();
+      expect(state).toMatchInlineSnapshot(`
+        ✕ 1, ⚠ 1
+        [root]
+             <Child>
+             <Child> ⚠
+             <Child>
+        →    <Child> ✕
+             <Child>
+      `);
+
+      selectNextErrorOrWarning();
+      expect(state).toMatchInlineSnapshot(`
+        ✕ 1, ⚠ 1
+        [root]
+             <Child>
+        →    <Child> ⚠
+             <Child>
+             <Child> ✕
+             <Child>
+      `);
+    });
+
+    it('should cycle through the previous errors/warnings and wrap around', () => {
+      withErrorsOrWarningsIgnored(['test-only:'], () =>
+        utils.act(() =>
+          ReactDOM.render(
+            <React.Fragment>
+              <Child />
+              <Child logWarning={true} />
+              <Child />
+              <Child logError={true} />
+              <Child />
+            </React.Fragment>,
+            document.createElement('div'),
+          ),
+        ),
+      );
+
+      utils.act(() => TestRenderer.create(<Contexts />));
+      expect(state).toMatchInlineSnapshot(`
+        ✕ 1, ⚠ 1
+        [root]
+             <Child>
+             <Child> ⚠
+             <Child>
+             <Child> ✕
+             <Child>
+      `);
+
+      selectPreviousErrorOrWarning();
+      expect(state).toMatchInlineSnapshot(`
+        ✕ 1, ⚠ 1
+        [root]
+             <Child>
+             <Child> ⚠
+             <Child>
+        →    <Child> ✕
+             <Child>
+      `);
+
+      selectPreviousErrorOrWarning();
+      expect(state).toMatchInlineSnapshot(`
+        ✕ 1, ⚠ 1
+        [root]
+             <Child>
+        →    <Child> ⚠
+             <Child>
+             <Child> ✕
+             <Child>
+      `);
+
+      selectPreviousErrorOrWarning();
+      expect(state).toMatchInlineSnapshot(`
+        ✕ 1, ⚠ 1
+        [root]
+             <Child>
+             <Child> ⚠
+             <Child>
+        →    <Child> ✕
+             <Child>
+      `);
+    });
+
+    it('should cycle through the next errors/warnings and wrap around with multiple roots', () => {
+      withErrorsOrWarningsIgnored(['test-only:'], () => {
+        utils.act(() => {
+          ReactDOM.render(
+            <React.Fragment>
+              <Child />
+              <Child logWarning={true} />,
+            </React.Fragment>,
+            document.createElement('div'),
+          );
+          ReactDOM.render(
+            <React.Fragment>
+              <Child />
+              <Child logError={true} />
+              <Child />
+            </React.Fragment>,
+            document.createElement('div'),
+          );
+        });
+      });
+
+      utils.act(() => TestRenderer.create(<Contexts />));
+      expect(state).toMatchInlineSnapshot(`
+        ✕ 1, ⚠ 1
+        [root]
+             <Child>
+             <Child> ⚠
+        [root]
+             <Child>
+             <Child> ✕
+             <Child>
+      `);
+
+      selectNextErrorOrWarning();
+      expect(state).toMatchInlineSnapshot(`
+        ✕ 1, ⚠ 1
+        [root]
+             <Child>
+        →    <Child> ⚠
+        [root]
+             <Child>
+             <Child> ✕
+             <Child>
+      `);
+
+      selectNextErrorOrWarning();
+      expect(state).toMatchInlineSnapshot(`
+        ✕ 1, ⚠ 1
+        [root]
+             <Child>
+             <Child> ⚠
+        [root]
+             <Child>
+        →    <Child> ✕
+             <Child>
+      `);
+
+      selectNextErrorOrWarning();
+      expect(state).toMatchInlineSnapshot(`
+        ✕ 1, ⚠ 1
+        [root]
+             <Child>
+        →    <Child> ⚠
+        [root]
+             <Child>
+             <Child> ✕
+             <Child>
+      `);
+    });
+
+    it('should cycle through the previous errors/warnings and wrap around with multiple roots', () => {
+      withErrorsOrWarningsIgnored(['test-only:'], () => {
+        utils.act(() => {
+          ReactDOM.render(
+            <React.Fragment>
+              <Child />
+              <Child logWarning={true} />,
+            </React.Fragment>,
+            document.createElement('div'),
+          );
+          ReactDOM.render(
+            <React.Fragment>
+              <Child />
+              <Child logError={true} />
+              <Child />
+            </React.Fragment>,
+            document.createElement('div'),
+          );
+        });
+      });
+
+      utils.act(() => TestRenderer.create(<Contexts />));
+      expect(state).toMatchInlineSnapshot(`
+        ✕ 1, ⚠ 1
+        [root]
+             <Child>
+             <Child> ⚠
+        [root]
+             <Child>
+             <Child> ✕
+             <Child>
+      `);
+
+      selectPreviousErrorOrWarning();
+      expect(state).toMatchInlineSnapshot(`
+        ✕ 1, ⚠ 1
+        [root]
+             <Child>
+             <Child> ⚠
+        [root]
+             <Child>
+        →    <Child> ✕
+             <Child>
+      `);
+
+      selectPreviousErrorOrWarning();
+      expect(state).toMatchInlineSnapshot(`
+        ✕ 1, ⚠ 1
+        [root]
+             <Child>
+        →    <Child> ⚠
+        [root]
+             <Child>
+             <Child> ✕
+             <Child>
+      `);
+
+      selectPreviousErrorOrWarning();
+      expect(state).toMatchInlineSnapshot(`
+        ✕ 1, ⚠ 1
+        [root]
+             <Child>
+             <Child> ⚠
+        [root]
+             <Child>
+        →    <Child> ✕
+             <Child>
+      `);
+    });
+
+    it('should select the next or previous element relative to the current selection', () => {
+      withErrorsOrWarningsIgnored(['test-only:'], () =>
+        utils.act(() =>
+          ReactDOM.render(
+            <React.Fragment>
+              <Child />
+              <Child logWarning={true} />
+              <Child />
+              <Child logError={true} />
+              <Child />
+            </React.Fragment>,
+            document.createElement('div'),
+          ),
+        ),
+      );
+
+      utils.act(() => TestRenderer.create(<Contexts />));
+      utils.act(() => dispatch({type: 'SELECT_ELEMENT_AT_INDEX', payload: 2}));
+      expect(state).toMatchInlineSnapshot(`
+        ✕ 1, ⚠ 1
+        [root]
+             <Child>
+             <Child> ⚠
+        →    <Child>
+             <Child> ✕
+             <Child>
+      `);
+
+      selectNextErrorOrWarning();
+      expect(state).toMatchInlineSnapshot(`
+        ✕ 1, ⚠ 1
+        [root]
+             <Child>
+             <Child> ⚠
+             <Child>
+        →    <Child> ✕
+             <Child>
+      `);
+
+      utils.act(() => dispatch({type: 'SELECT_ELEMENT_AT_INDEX', payload: 2}));
+      expect(state).toMatchInlineSnapshot(`
+        ✕ 1, ⚠ 1
+        [root]
+             <Child>
+             <Child> ⚠
+        →    <Child>
+             <Child> ✕
+             <Child>
+      `);
+
+      selectPreviousErrorOrWarning();
+      expect(state).toMatchInlineSnapshot(`
+        ✕ 1, ⚠ 1
+        [root]
+             <Child>
+        →    <Child> ⚠
+             <Child>
+             <Child> ✕
+             <Child>
+      `);
+    });
+
+    it('should update correctly when errors/warnings are cleared for a fiber in the list', () => {
+      withErrorsOrWarningsIgnored(['test-only:'], () =>
+        utils.act(() =>
+          ReactDOM.render(
+            <React.Fragment>
+              <Child logWarning={true} />
+              <Child logError={true} />
+              <Child logError={true} />
+              <Child logWarning={true} />
+            </React.Fragment>,
+            document.createElement('div'),
+          ),
+        ),
+      );
+
+      utils.act(() => TestRenderer.create(<Contexts />));
+      expect(state).toMatchInlineSnapshot(`
+        ✕ 2, ⚠ 2
+        [root]
+             <Child> ⚠
+             <Child> ✕
+             <Child> ✕
+             <Child> ⚠
+      `);
+
+      // Select the first item in the list
+      selectNextErrorOrWarning();
+      expect(state).toMatchInlineSnapshot(`
+        ✕ 2, ⚠ 2
+        [root]
+        →    <Child> ⚠
+             <Child> ✕
+             <Child> ✕
+             <Child> ⚠
+      `);
+
+      // Clear warnings (but the next Fiber has only errors)
+      clearWarningsForElement(store.getElementIDAtIndex(1));
+      selectNextErrorOrWarning();
+      expect(state).toMatchInlineSnapshot(`
+        ✕ 2, ⚠ 2
+        [root]
+             <Child> ⚠
+        →    <Child> ✕
+             <Child> ✕
+             <Child> ⚠
+      `);
+
+      clearErrorsForElement(store.getElementIDAtIndex(2));
+
+      // Should step to the (now) next one in the list.
+      selectNextErrorOrWarning();
+      expect(state).toMatchInlineSnapshot(`
+        ✕ 1, ⚠ 2
+        [root]
+             <Child> ⚠
+             <Child> ✕
+             <Child>
+        →    <Child> ⚠
+      `);
+
+      // Should skip over the (now) cleared Fiber
+      selectPreviousErrorOrWarning();
+      expect(state).toMatchInlineSnapshot(`
+        ✕ 1, ⚠ 2
+        [root]
+             <Child> ⚠
+        →    <Child> ✕
+             <Child>
+             <Child> ⚠
+      `);
+    });
+
+    it('should update correctly when errors/warnings are cleared for the currently selected fiber', () => {
+      withErrorsOrWarningsIgnored(['test-only:'], () =>
+        utils.act(() =>
+          ReactDOM.render(
+            <React.Fragment>
+              <Child logWarning={true} />
+              <Child logError={true} />
+            </React.Fragment>,
+            document.createElement('div'),
+          ),
+        ),
+      );
+
+      utils.act(() => TestRenderer.create(<Contexts />));
+      expect(state).toMatchInlineSnapshot(`
+        ✕ 1, ⚠ 1
+        [root]
+             <Child> ⚠
+             <Child> ✕
+      `);
+
+      selectNextErrorOrWarning();
+      expect(state).toMatchInlineSnapshot(`
+        ✕ 1, ⚠ 1
+        [root]
+        →    <Child> ⚠
+             <Child> ✕
+      `);
+
+      clearWarningsForElement(store.getElementIDAtIndex(0));
+      selectNextErrorOrWarning();
+      expect(state).toMatchInlineSnapshot(`
+        ✕ 1, ⚠ 0
+        [root]
+             <Child>
+        →    <Child> ✕
+      `);
+    });
+
+    it('should update correctly when new errors/warnings are added', () => {
+      const container = document.createElement('div');
+
+      withErrorsOrWarningsIgnored(['test-only:'], () =>
+        utils.act(() =>
+          ReactDOM.render(
+            <React.Fragment>
+              <Child logWarning={true} />
+              <Child />
+              <Child />
+              <Child logError={true} />
+            </React.Fragment>,
+            container,
+          ),
+        ),
+      );
+
+      utils.act(() => TestRenderer.create(<Contexts />));
+      expect(state).toMatchInlineSnapshot(`
+        ✕ 1, ⚠ 1
+        [root]
+             <Child> ⚠
+             <Child>
+             <Child>
+             <Child> ✕
+      `);
+
+      selectNextErrorOrWarning();
+      expect(state).toMatchInlineSnapshot(`
+        ✕ 1, ⚠ 1
+        [root]
+        →    <Child> ⚠
+             <Child>
+             <Child>
+             <Child> ✕
+      `);
+
+      withErrorsOrWarningsIgnored(['test-only:'], () =>
+        utils.act(() =>
+          ReactDOM.render(
+            <React.Fragment>
+              <Child />
+              <Child logWarning={true} />
+              <Child />
+              <Child />
+            </React.Fragment>,
+            container,
+          ),
+        ),
+      );
+
+      selectNextErrorOrWarning();
+      expect(state).toMatchInlineSnapshot(`
+        ✕ 1, ⚠ 2
+        [root]
+             <Child> ⚠
+        →    <Child> ⚠
+             <Child>
+             <Child> ✕
+      `);
+
+      selectNextErrorOrWarning();
+      expect(state).toMatchInlineSnapshot(`
+        ✕ 1, ⚠ 2
+        [root]
+             <Child> ⚠
+             <Child> ⚠
+             <Child>
+        →    <Child> ✕
+      `);
+
+      selectNextErrorOrWarning();
+      expect(state).toMatchInlineSnapshot(`
+        ✕ 1, ⚠ 2
+        [root]
+        →    <Child> ⚠
+             <Child> ⚠
+             <Child>
+             <Child> ✕
+      `);
+    });
+
+    it('should update correctly when all errors/warnings are cleared', () => {
+      withErrorsOrWarningsIgnored(['test-only:'], () =>
+        utils.act(() =>
+          ReactDOM.render(
+            <React.Fragment>
+              <Child logWarning={true} />
+              <Child logError={true} />
+            </React.Fragment>,
+            document.createElement('div'),
+          ),
+        ),
+      );
+
+      utils.act(() => TestRenderer.create(<Contexts />));
+      expect(state).toMatchInlineSnapshot(`
+        ✕ 1, ⚠ 1
+        [root]
+             <Child> ⚠
+             <Child> ✕
+      `);
+
+      selectNextErrorOrWarning();
+      expect(state).toMatchInlineSnapshot(`
+        ✕ 1, ⚠ 1
+        [root]
+        →    <Child> ⚠
+             <Child> ✕
+      `);
+
+      clearAllErrors();
+
+      selectNextErrorOrWarning();
+      expect(state).toMatchInlineSnapshot(`
+        [root]
+        →    <Child>
+             <Child>
+      `);
+
+      selectPreviousErrorOrWarning();
+      expect(state).toMatchInlineSnapshot(`
+        [root]
+        →    <Child>
+             <Child>
+      `);
+    });
+
+    it('should update select and auto-expand parts components within hidden parts of the tree', () => {
+      const Wrapper = ({children}) => children;
+
+      store.collapseNodesByDefault = true;
+
+      withErrorsOrWarningsIgnored(['test-only:'], () =>
+        utils.act(() =>
+          ReactDOM.render(
+            <React.Fragment>
+              <Wrapper>
+                <Child logWarning={true} />
+              </Wrapper>
+              <Wrapper>
+                <Wrapper>
+                  <Child logWarning={true} />
+                </Wrapper>
+              </Wrapper>
+            </React.Fragment>,
+            document.createElement('div'),
+          ),
+        ),
+      );
+
+      utils.act(() => TestRenderer.create(<Contexts />));
+      expect(state).toMatchInlineSnapshot(`
+        ✕ 0, ⚠ 2
+        [root]
+           ▸ <Wrapper>
+           ▸ <Wrapper>
+      `);
+
+      selectNextErrorOrWarning();
+      expect(state).toMatchInlineSnapshot(`
+        ✕ 0, ⚠ 2
+        [root]
+           ▾ <Wrapper>
+        →      <Child> ⚠
+           ▸ <Wrapper>
+      `);
+
+      selectNextErrorOrWarning();
+      expect(state).toMatchInlineSnapshot(`
+        ✕ 0, ⚠ 2
+        [root]
+           ▾ <Wrapper>
+               <Child> ⚠
+           ▾ <Wrapper>
+             ▾ <Wrapper>
+        →        <Child> ⚠
+      `);
+    });
+
+    it('should properly handle when components filters are updated', () => {
+      const Wrapper = ({children}) => children;
+
+      withErrorsOrWarningsIgnored(['test-only:'], () =>
+        utils.act(() =>
+          ReactDOM.render(
+            <React.Fragment>
+              <Wrapper>
+                <Child logWarning={true} />
+              </Wrapper>
+              <Wrapper>
+                <Wrapper>
+                  <Child logWarning={true} />
+                </Wrapper>
+              </Wrapper>
+            </React.Fragment>,
+            document.createElement('div'),
+          ),
+        ),
+      );
+
+      utils.act(() => TestRenderer.create(<Contexts />));
+      expect(state).toMatchInlineSnapshot(`
+        ✕ 0, ⚠ 2
+        [root]
+           ▾ <Wrapper>
+               <Child> ⚠
+           ▾ <Wrapper>
+             ▾ <Wrapper>
+                 <Child> ⚠
+      `);
+
+      selectNextErrorOrWarning();
+      expect(state).toMatchInlineSnapshot(`
+        ✕ 0, ⚠ 2
+        [root]
+           ▾ <Wrapper>
+        →      <Child> ⚠
+           ▾ <Wrapper>
+             ▾ <Wrapper>
+                 <Child> ⚠
+      `);
+
+      utils.act(() => {
+        store.componentFilters = [utils.createDisplayNameFilter('Wrapper')];
+      });
+      expect(state).toMatchInlineSnapshot(`
+        ✕ 0, ⚠ 2
+        [root]
+        →    <Child> ⚠
+             <Child> ⚠
+      `);
+
+      selectNextErrorOrWarning();
+      expect(state).toMatchInlineSnapshot(`
+        ✕ 0, ⚠ 2
+        [root]
+             <Child> ⚠
+        →    <Child> ⚠
+      `);
+
+      utils.act(() => {
+        store.componentFilters = [];
+      });
+      expect(state).toMatchInlineSnapshot(`
+        ✕ 0, ⚠ 2
+        [root]
+           ▾ <Wrapper>
+               <Child> ⚠
+           ▾ <Wrapper>
+             ▾ <Wrapper>
+        →        <Child> ⚠
+      `);
+
+      selectPreviousErrorOrWarning();
+      expect(state).toMatchInlineSnapshot(`
+        ✕ 0, ⚠ 2
+        [root]
+           ▾ <Wrapper>
+        →      <Child> ⚠
+           ▾ <Wrapper>
+             ▾ <Wrapper>
+                 <Child> ⚠
+      `);
+    });
+
+    it('should preserve errors for fibers even if they are filtered out of the tree initially', () => {
+      const Wrapper = ({children}) => children;
+
+      withErrorsOrWarningsIgnored(['test-only:'], () =>
+        utils.act(() =>
+          ReactDOM.render(
+            <React.Fragment>
+              <Wrapper>
+                <Child logWarning={true} />
+              </Wrapper>
+              <Wrapper>
+                <Wrapper>
+                  <Child logWarning={true} />
+                </Wrapper>
+              </Wrapper>
+            </React.Fragment>,
+            document.createElement('div'),
+          ),
+        ),
+      );
+
+      store.componentFilters = [utils.createDisplayNameFilter('Child')];
+
+      utils.act(() => TestRenderer.create(<Contexts />));
+      expect(state).toMatchInlineSnapshot(`
+        [root]
+             <Wrapper>
+           ▾ <Wrapper>
+               <Wrapper>
+      `);
+
+      utils.act(() => {
+        store.componentFilters = [];
+      });
+      expect(state).toMatchInlineSnapshot(`
+        ✕ 0, ⚠ 2
+        [root]
+           ▾ <Wrapper>
+               <Child> ⚠
+           ▾ <Wrapper>
+             ▾ <Wrapper>
+                 <Child> ⚠
+      `);
+
+      selectNextErrorOrWarning();
+      expect(state).toMatchInlineSnapshot(`
+        ✕ 0, ⚠ 2
+        [root]
+           ▾ <Wrapper>
+        →      <Child> ⚠
+           ▾ <Wrapper>
+             ▾ <Wrapper>
+                 <Child> ⚠
+      `);
+    });
+
+    describe('suspense', () => {
+      // This verifies that we don't flush before the tree has been committed.
+      it('should properly handle errors/warnings from components inside of delayed Suspense', async () => {
+        const NeverResolves = React.lazy(() => new Promise(() => {}));
+
+        withErrorsOrWarningsIgnored(['test-only:'], () =>
+          utils.act(() =>
+            ReactDOM.render(
+              <React.Suspense fallback={null}>
+                <Child logWarning={true} />
+                <NeverResolves />
+              </React.Suspense>,
+              document.createElement('div'),
+            ),
+          ),
+        );
+        utils.act(() => TestRenderer.create(<Contexts />));
+
+        jest.runAllTimers();
+
+        expect(state).toMatchInlineSnapshot(`
+                  [root]
+                       <Suspense>
+              `);
+
+        selectNextErrorOrWarning();
+
+        expect(state).toMatchInlineSnapshot(`
+                  [root]
+                       <Suspense>
+              `);
+      });
+
+      it('should properly handle errors/warnings from components that dont mount because of Suspense', async () => {
+        async function fakeImport(result) {
+          return {default: result};
+        }
+        const LazyComponent = React.lazy(() => fakeImport(Child));
+
+        const container = document.createElement('div');
+
+        withErrorsOrWarningsIgnored(['test-only:'], () =>
+          utils.act(() =>
+            ReactDOM.render(
+              <React.Suspense fallback={null}>
+                <Child logWarning={true} />
+                <LazyComponent />
+              </React.Suspense>,
+              container,
+            ),
+          ),
+        );
+        utils.act(() => TestRenderer.create(<Contexts />));
+
+        expect(state).toMatchInlineSnapshot(`
+                  [root]
+                       <Suspense>
+              `);
+
+        await Promise.resolve();
+        withErrorsOrWarningsIgnored(['test-only:'], () =>
+          utils.act(() =>
+            ReactDOM.render(
+              <React.Suspense fallback={null}>
+                <Child logWarning={true} />
+                <LazyComponent />
+              </React.Suspense>,
+              container,
+            ),
+          ),
+        );
+
+        expect(state).toMatchInlineSnapshot(`
+          ✕ 0, ⚠ 1
+          [root]
+             ▾ <Suspense>
+                 <Child> ⚠
+                 <Child>
+        `);
+      });
+
+      it('should properly show errors/warnings from components in the Suspense fallback tree', async () => {
+        async function fakeImport(result) {
+          return {default: result};
+        }
+        const LazyComponent = React.lazy(() => fakeImport(Child));
+
+        const Fallback = () => <Child logError={true} />;
+
+        const container = document.createElement('div');
+
+        withErrorsOrWarningsIgnored(['test-only:'], () =>
+          utils.act(() =>
+            ReactDOM.render(
+              <React.Suspense fallback={<Fallback />}>
+                <LazyComponent />
+              </React.Suspense>,
+              container,
+            ),
+          ),
+        );
+        utils.act(() => TestRenderer.create(<Contexts />));
+
+        expect(state).toMatchInlineSnapshot(`
+          ✕ 1, ⚠ 0
+          [root]
+             ▾ <Suspense>
+               ▾ <Fallback>
+                   <Child> ✕
+        `);
+
+        await Promise.resolve();
+        withErrorsOrWarningsIgnored(['test-only:'], () =>
+          utils.act(() =>
+            ReactDOM.render(
+              <React.Suspense fallback={<Fallback />}>
+                <LazyComponent />
+              </React.Suspense>,
+              container,
+            ),
+          ),
+        );
+
+        expect(state).toMatchInlineSnapshot(`
+                  [root]
+                     ▾ <Suspense>
+                         <Child>
+              `);
+      });
+    });
+
+    describe('error boundaries', () => {
+      it('should properly handle errors/warnings from components that dont mount because of an error', () => {
+        class ErrorBoundary extends React.Component {
+          state = {error: null};
+          static getDerivedStateFromError(error) {
+            return {error};
+          }
+          render() {
+            if (this.state.error) {
+              return null;
+            }
+            return this.props.children;
+          }
+        }
+
+        class BadRender extends React.Component {
+          render() {
+            console.error('test-only: I am about to throw!');
+            throw new Error('test-only: Oops!');
+          }
+        }
+
+        const container = document.createElement('div');
+        withErrorsOrWarningsIgnored(
+          ['test-only:', 'React will try to recreate this component tree'],
+          () => {
+            utils.act(() =>
+              ReactDOM.render(
+                <ErrorBoundary>
+                  <BadRender />
+                </ErrorBoundary>,
+                container,
+              ),
+            );
+          },
+        );
+
+        utils.act(() => TestRenderer.create(<Contexts />));
+
+        expect(store).toMatchInlineSnapshot(`
+          ✕ 1, ⚠ 0
+          [root]
+              <ErrorBoundary> ✕
+        `);
+
+        selectNextErrorOrWarning();
+        expect(state).toMatchInlineSnapshot(`
+          ✕ 1, ⚠ 0
+          [root]
+          →    <ErrorBoundary> ✕
+        `);
+
+        utils.act(() => ReactDOM.unmountComponentAtNode(container));
+        expect(state).toMatchInlineSnapshot(``);
+
+        // Should be a noop
+        selectNextErrorOrWarning();
+        expect(state).toMatchInlineSnapshot(``);
+      });
+
+      it('should properly handle errors/warnings from components that dont mount because of an error', () => {
+        class ErrorBoundary extends React.Component {
+          state = {error: null};
+          static getDerivedStateFromError(error) {
+            return {error};
+          }
+          render() {
+            if (this.state.error) {
+              return null;
+            }
+            return this.props.children;
+          }
+        }
+
+        class LogsWarning extends React.Component {
+          render() {
+            console.warn('test-only: I am about to throw!');
+            return <ThrowsError />;
+          }
+        }
+        class ThrowsError extends React.Component {
+          render() {
+            throw new Error('test-only: Oops!');
+          }
+        }
+
+        const container = document.createElement('div');
+        withErrorsOrWarningsIgnored(
+          ['test-only:', 'React will try to recreate this component tree'],
+          () => {
+            utils.act(() =>
+              ReactDOM.render(
+                <ErrorBoundary>
+                  <LogsWarning />
+                </ErrorBoundary>,
+                container,
+              ),
+            );
+          },
+        );
+
+        utils.act(() => TestRenderer.create(<Contexts />));
+
+        expect(store).toMatchInlineSnapshot(`
+          ✕ 1, ⚠ 0
+          [root]
+              <ErrorBoundary> ✕
+        `);
+
+        selectNextErrorOrWarning();
+        expect(state).toMatchInlineSnapshot(`
+          ✕ 1, ⚠ 0
+          [root]
+          →    <ErrorBoundary> ✕
+        `);
+
+        utils.act(() => ReactDOM.unmountComponentAtNode(container));
+        expect(state).toMatchInlineSnapshot(``);
+
+        // Should be a noop
+        selectNextErrorOrWarning();
+        expect(state).toMatchInlineSnapshot(``);
+      });
+
+      it('should properly handle errors/warnings from inside of an error boundary', () => {
+        class ErrorBoundary extends React.Component {
+          state = {error: null};
+          static getDerivedStateFromError(error) {
+            return {error};
+          }
+          render() {
+            if (this.state.error) {
+              return <Child logError={true} />;
+            }
+            return this.props.children;
+          }
+        }
+
+        class BadRender extends React.Component {
+          render() {
+            console.error('test-only: I am about to throw!');
+            throw new Error('test-only: Oops!');
+          }
+        }
+
+        const container = document.createElement('div');
+        withErrorsOrWarningsIgnored(
+          ['test-only:', 'React will try to recreate this component tree'],
+          () => {
+            utils.act(() =>
+              ReactDOM.render(
+                <ErrorBoundary>
+                  <BadRender />
+                </ErrorBoundary>,
+                container,
+              ),
+            );
+          },
+        );
+
+        utils.act(() => TestRenderer.create(<Contexts />));
+
+        expect(store).toMatchInlineSnapshot(`
+          ✕ 2, ⚠ 0
+          [root]
+            ▾ <ErrorBoundary> ✕
+                <Child> ✕
+        `);
+
+        selectNextErrorOrWarning();
+        expect(state).toMatchInlineSnapshot(`
+          ✕ 2, ⚠ 0
+          [root]
+          →  ▾ <ErrorBoundary> ✕
+                 <Child> ✕
+        `);
+      });
     });
   });
 });
diff --git a/packages/react-devtools-shared/src/__tests__/treeContextStateSerializer.js b/packages/react-devtools-shared/src/__tests__/treeContextStateSerializer.js
new file mode 100644
index 0000000000000..866d4ed4c3662
--- /dev/null
+++ b/packages/react-devtools-shared/src/__tests__/treeContextStateSerializer.js
@@ -0,0 +1,24 @@
+import {printStore} from 'react-devtools-shared/src/devtools/utils';
+
+// test() is part of Jest's serializer API
+export function test(maybeState) {
+  if (maybeState === null || typeof maybeState !== 'object') {
+    return false;
+  }
+
+  // Duck typing at its finest.
+  return (
+    maybeState.hasOwnProperty('inspectedElementID') &&
+    maybeState.hasOwnProperty('ownerFlatTree') &&
+    maybeState.hasOwnProperty('ownerSubtreeLeafElementID')
+  );
+}
+
+// print() is part of Jest's serializer API
+export function print(state, serialize, indent) {
+  // This is a big of a hack but it works around having to pass in a meta object e.g. {store, state}.
+  // DevTools tests depend on a global Store object anyway (initialized via setupTest).
+  const store = global.store;
+
+  return printStore(store, false, state);
+}
diff --git a/packages/react-devtools-shared/src/__tests__/utils.js b/packages/react-devtools-shared/src/__tests__/utils.js
index 2f4325f8ca835..2fb0664a24855 100644
--- a/packages/react-devtools-shared/src/__tests__/utils.js
+++ b/packages/react-devtools-shared/src/__tests__/utils.js
@@ -213,3 +213,37 @@ export function exportImportHelper(bridge: FrontendBridge, store: Store): void {
     profilerStore.profilingData = profilingDataFrontend;
   });
 }
+
+/**
+ * Runs `fn` while preventing console error and warnings that partially match any given `errorOrWarningMessages` from appearing in the console.
+ * @param errorOrWarningMessages Messages are matched partially (i.e. indexOf), pre-formatting.
+ * @param fn
+ */
+export function withErrorsOrWarningsIgnored<T: void | Promise<void>>(
+  errorOrWarningMessages: string[],
+  fn: () => T,
+): T {
+  let resetIgnoredErrorOrWarningMessages = true;
+  try {
+    global._ignoredErrorOrWarningMessages = errorOrWarningMessages;
+    const maybeThenable = fn();
+    if (
+      maybeThenable !== undefined &&
+      typeof maybeThenable.then === 'function'
+    ) {
+      resetIgnoredErrorOrWarningMessages = false;
+      return maybeThenable.then(
+        () => {
+          global._ignoredErrorOrWarningMessages = [];
+        },
+        () => {
+          global._ignoredErrorOrWarningMessages = [];
+        },
+      );
+    }
+  } finally {
+    if (resetIgnoredErrorOrWarningMessages) {
+      global._ignoredErrorOrWarningMessages = [];
+    }
+  }
+}
diff --git a/packages/react-devtools-shared/src/backend/agent.js b/packages/react-devtools-shared/src/backend/agent.js
index 1e0ffe336f685..326b0fb2e340f 100644
--- a/packages/react-devtools-shared/src/backend/agent.js
+++ b/packages/react-devtools-shared/src/backend/agent.js
@@ -169,6 +169,9 @@ export default class Agent extends EventEmitter<{|
 
     this._bridge = bridge;
 
+    bridge.addListener('clearErrorsAndWarnings', this.clearErrorsAndWarnings);
+    bridge.addListener('clearErrorsForFiberID', this.clearErrorsForFiberID);
+    bridge.addListener('clearWarningsForFiberID', this.clearWarningsForFiberID);
     bridge.addListener('copyElementPath', this.copyElementPath);
     bridge.addListener('deletePath', this.deletePath);
     bridge.addListener('getProfilingData', this.getProfilingData);
@@ -226,6 +229,33 @@ export default class Agent extends EventEmitter<{|
     return this._rendererInterfaces;
   }
 
+  clearErrorsAndWarnings = ({rendererID}: {|rendererID: RendererID|}) => {
+    const renderer = this._rendererInterfaces[rendererID];
+    if (renderer == null) {
+      console.warn(`Invalid renderer id "${rendererID}"`);
+    } else {
+      renderer.clearErrorsAndWarnings();
+    }
+  };
+
+  clearErrorsForFiberID = ({id, rendererID}: ElementAndRendererID) => {
+    const renderer = this._rendererInterfaces[rendererID];
+    if (renderer == null) {
+      console.warn(`Invalid renderer id "${rendererID}"`);
+    } else {
+      renderer.clearErrorsForFiberID(id);
+    }
+  };
+
+  clearWarningsForFiberID = ({id, rendererID}: ElementAndRendererID) => {
+    const renderer = this._rendererInterfaces[rendererID];
+    if (renderer == null) {
+      console.warn(`Invalid renderer id "${rendererID}"`);
+    } else {
+      renderer.clearWarningsForFiberID(id);
+    }
+  };
+
   copyElementPath = ({id, path, rendererID}: CopyElementParams) => {
     const renderer = this._rendererInterfaces[rendererID];
     if (renderer == null) {
@@ -571,16 +601,22 @@ export default class Agent extends EventEmitter<{|
   updateConsolePatchSettings = ({
     appendComponentStack,
     breakOnConsoleErrors,
+    showInlineWarningsAndErrors,
   }: {|
     appendComponentStack: boolean,
     breakOnConsoleErrors: boolean,
+    showInlineWarningsAndErrors: boolean,
   |}) => {
     // If the frontend preference has change,
     // or in the case of React Native- if the backend is just finding out the preference-
     // then install or uninstall the console overrides.
     // It's safe to call these methods multiple times, so we don't need to worry about that.
     if (appendComponentStack || breakOnConsoleErrors) {
-      patchConsole({appendComponentStack, breakOnConsoleErrors});
+      patchConsole({
+        appendComponentStack,
+        breakOnConsoleErrors,
+        showInlineWarningsAndErrors,
+      });
     } else {
       unpatchConsole();
     }
diff --git a/packages/react-devtools-shared/src/backend/console.js b/packages/react-devtools-shared/src/backend/console.js
index 9a29d5cb0582e..cc9eff11f6841 100644
--- a/packages/react-devtools-shared/src/backend/console.js
+++ b/packages/react-devtools-shared/src/backend/console.js
@@ -22,11 +22,22 @@ const PREFIX_REGEX = /\s{4}(in|at)\s{1}/;
 // but we can fallback to looking for location info (e.g. "foo.js:12:345")
 const ROW_COLUMN_NUMBER_REGEX = /:\d+:\d+(\n|$)/;
 
+export function isStringComponentStack(text: string): boolean {
+  return PREFIX_REGEX.test(text) || ROW_COLUMN_NUMBER_REGEX.test(text);
+}
+
+type OnErrorOrWarning = (
+  fiber: Fiber,
+  type: 'error' | 'warn',
+  args: Array<any>,
+) => void;
+
 const injectedRenderers: Map<
   ReactRenderer,
   {|
     currentDispatcherRef: CurrentDispatcherRef,
     getCurrentFiber: () => Fiber | null,
+    onErrorOrWarning: ?OnErrorOrWarning,
     workTagMap: WorkTagMap,
   |},
 > = new Map();
@@ -54,7 +65,10 @@ export function dangerous_setTargetConsoleForTesting(
 // v16 renderers should use this method to inject internals necessary to generate a component stack.
 // These internals will be used if the console is patched.
 // Injecting them separately allows the console to easily be patched or un-patched later (at runtime).
-export function registerRenderer(renderer: ReactRenderer): void {
+export function registerRenderer(
+  renderer: ReactRenderer,
+  onErrorOrWarning?: OnErrorOrWarning,
+): void {
   const {
     currentDispatcherRef,
     getCurrentFiber,
@@ -76,6 +90,7 @@ export function registerRenderer(renderer: ReactRenderer): void {
       currentDispatcherRef,
       getCurrentFiber,
       workTagMap: ReactTypeOfWork,
+      onErrorOrWarning,
     });
   }
 }
@@ -83,6 +98,7 @@ export function registerRenderer(renderer: ReactRenderer): void {
 const consoleSettingsRef = {
   appendComponentStack: false,
   breakOnConsoleErrors: false,
+  showInlineWarningsAndErrors: false,
 };
 
 // Patches console methods to append component stack for the current fiber.
@@ -90,14 +106,17 @@ const consoleSettingsRef = {
 export function patch({
   appendComponentStack,
   breakOnConsoleErrors,
+  showInlineWarningsAndErrors,
 }: {
   appendComponentStack: boolean,
   breakOnConsoleErrors: boolean,
+  showInlineWarningsAndErrors: boolean,
 }): void {
   // Settings may change after we've patched the console.
   // Using a shared ref allows the patch function to read the latest values.
   consoleSettingsRef.appendComponentStack = appendComponentStack;
   consoleSettingsRef.breakOnConsoleErrors = breakOnConsoleErrors;
+  consoleSettingsRef.showInlineWarningsAndErrors = showInlineWarningsAndErrors;
 
   if (unpatchFn !== null) {
     // Don't patch twice.
@@ -121,32 +140,51 @@ export function patch({
         targetConsole[method]);
 
       const overrideMethod = (...args) => {
-        const latestAppendComponentStack =
-          consoleSettingsRef.appendComponentStack;
-        const latestBreakOnConsoleErrors =
-          consoleSettingsRef.breakOnConsoleErrors;
-
-        if (latestAppendComponentStack) {
-          try {
-            // If we are ever called with a string that already has a component stack, e.g. a React error/warning,
-            // don't append a second stack.
-            const lastArg = args.length > 0 ? args[args.length - 1] : null;
-            const alreadyHasComponentStack =
-              lastArg !== null &&
-              (PREFIX_REGEX.test(lastArg) ||
-                ROW_COLUMN_NUMBER_REGEX.test(lastArg));
-
-            if (!alreadyHasComponentStack) {
-              // If there's a component stack for at least one of the injected renderers, append it.
-              // We don't handle the edge case of stacks for more than one (e.g. interleaved renderers?)
-              // eslint-disable-next-line no-for-of-loops/no-for-of-loops
-              for (const {
-                currentDispatcherRef,
-                getCurrentFiber,
-                workTagMap,
-              } of injectedRenderers.values()) {
-                const current: ?Fiber = getCurrentFiber();
-                if (current != null) {
+        const lastArg = args.length > 0 ? args[args.length - 1] : null;
+        const alreadyHasComponentStack =
+          lastArg !== null && isStringComponentStack(lastArg);
+
+        let shouldAppendWarningStack = false;
+        if (consoleSettingsRef.appendComponentStack) {
+          // If we are ever called with a string that already has a component stack,
+          // e.g. a React error/warning, don't append a second stack.
+          shouldAppendWarningStack = !alreadyHasComponentStack;
+        }
+
+        const shouldShowInlineWarningsAndErrors =
+          consoleSettingsRef.showInlineWarningsAndErrors &&
+          (method === 'error' || method === 'warn');
+
+        if (shouldAppendWarningStack || shouldShowInlineWarningsAndErrors) {
+          // Search for the first renderer that has a current Fiber.
+          // We don't handle the edge case of stacks for more than one (e.g. interleaved renderers?)
+          // eslint-disable-next-line no-for-of-loops/no-for-of-loops
+          for (const {
+            currentDispatcherRef,
+            getCurrentFiber,
+            onErrorOrWarning,
+            workTagMap,
+          } of injectedRenderers.values()) {
+            const current: ?Fiber = getCurrentFiber();
+            if (current != null) {
+              try {
+                if (shouldShowInlineWarningsAndErrors) {
+                  // patch() is called by two places: (1) the hook and (2) the renderer backend.
+                  // The backend is what impliments a message queue, so it's the only one that injects onErrorOrWarning.
+                  if (typeof onErrorOrWarning === 'function') {
+                    onErrorOrWarning(
+                      current,
+                      ((method: any): 'error' | 'warn'),
+                      // Copy args before we mutate them (e.g. adding the component stack)
+                      alreadyHasComponentStack
+                        ? // Replace component stack with an empty string in case there's a string placeholder for it.
+                          [...args.slice(0, -1), '']
+                        : args.slice(),
+                    );
+                  }
+                }
+
+                if (shouldAppendWarningStack) {
                   const componentStack = getStackByFiberInDevAndProd(
                     workTagMap,
                     current,
@@ -155,16 +193,17 @@ export function patch({
                   if (componentStack !== '') {
                     args.push(componentStack);
                   }
-                  break;
                 }
+              } catch (error) {
+                // Don't let a DevTools or React internal error interfere with logging.
+              } finally {
+                break;
               }
             }
-          } catch (error) {
-            // Don't let a DevTools or React internal error interfere with logging.
           }
         }
 
-        if (latestBreakOnConsoleErrors) {
+        if (consoleSettingsRef.breakOnConsoleErrors) {
           // --- Welcome to debugging with React DevTools ---
           // This debugger statement means that you've enabled the "break on warnings" feature.
           // Use the browser's Call Stack panel to step out of this override function-
@@ -177,6 +216,7 @@ export function patch({
       };
 
       overrideMethod.__REACT_DEVTOOLS_ORIGINAL_METHOD__ = originalMethod;
+      originalMethod.__REACT_DEVTOOLS_OVERRIDE_METHOD__ = overrideMethod;
 
       // $FlowFixMe property error|warn is not writable.
       targetConsole[method] = overrideMethod;
diff --git a/packages/react-devtools-shared/src/backend/legacy/renderer.js b/packages/react-devtools-shared/src/backend/legacy/renderer.js
index 86d987c8f2e26..90f0f3ad12b46 100644
--- a/packages/react-devtools-shared/src/backend/legacy/renderer.js
+++ b/packages/react-devtools-shared/src/backend/legacy/renderer.js
@@ -779,6 +779,10 @@ export function attach(
       state = publicInstance.state || null;
     }
 
+    // Not implemented
+    const errors = [];
+    const warnings = [];
+
     return {
       id,
 
@@ -812,6 +816,8 @@ export function attach(
       hooks: null,
       props,
       state,
+      errors,
+      warnings,
 
       // List of owners
       owners,
@@ -1040,7 +1046,22 @@ export function attach(
     return null;
   }
 
+  function clearErrorsAndWarnings() {
+    // Not implemented
+  }
+
+  function clearErrorsForFiberID(id: number) {
+    // Not implemented
+  }
+
+  function clearWarningsForFiberID(id: number) {
+    // Not implemented
+  }
+
   return {
+    clearErrorsAndWarnings,
+    clearErrorsForFiberID,
+    clearWarningsForFiberID,
     cleanup,
     copyElementPath,
     deletePath,
diff --git a/packages/react-devtools-shared/src/backend/renderer.js b/packages/react-devtools-shared/src/backend/renderer.js
index 983a17f51dc9f..11beef37f76d4 100644
--- a/packages/react-devtools-shared/src/backend/renderer.js
+++ b/packages/react-devtools-shared/src/backend/renderer.js
@@ -49,7 +49,9 @@ import {
   SESSION_STORAGE_RECORD_CHANGE_DESCRIPTIONS_KEY,
   TREE_OPERATION_ADD,
   TREE_OPERATION_REMOVE,
+  TREE_OPERATION_REMOVE_ROOT,
   TREE_OPERATION_REORDER_CHILDREN,
+  TREE_OPERATION_UPDATE_ERRORS_OR_WARNINGS,
   TREE_OPERATION_UPDATE_TREE_BASE_DURATION,
 } from '../constants';
 import {inspectHooksOfFiber} from 'react-debug-tools';
@@ -76,6 +78,7 @@ import {
   MEMO_NUMBER,
   MEMO_SYMBOL_STRING,
 } from './ReactSymbols';
+import {format} from './utils';
 
 import type {Fiber} from 'react-reconciler/src/ReactInternalTypes';
 import type {
@@ -117,6 +120,7 @@ type ReactTypeOfSideEffectType = {|
   NoFlags: number,
   PerformedWork: number,
   Placement: number,
+  Incomplete: number,
 |};
 
 function getFiberFlags(fiber: Fiber): number {
@@ -143,6 +147,7 @@ export function getInternalReactConstants(
     NoFlags: 0b00,
     PerformedWork: 0b01,
     Placement: 0b10,
+    Incomplete: 0b10000000000000,
   };
 
   // **********************************************************
@@ -479,7 +484,7 @@ export function attach(
     ReactTypeOfWork,
     ReactTypeOfSideEffect,
   } = getInternalReactConstants(renderer.version);
-  const {NoFlags, PerformedWork, Placement} = ReactTypeOfSideEffect;
+  const {Incomplete, NoFlags, PerformedWork, Placement} = ReactTypeOfSideEffect;
   const {
     FunctionComponent,
     ClassComponent,
@@ -522,13 +527,104 @@ export function attach(
     typeof setSuspenseHandler === 'function' &&
     typeof scheduleUpdate === 'function';
 
+  // Set of Fibers (IDs) with recently changed number of error/warning messages.
+  const fibersWithChangedErrorOrWarningCounts: Set<number> = new Set();
+
+  // Mapping of fiber IDs to error/warning messages and counts.
+  const fiberToErrorsMap: Map<number, Map<string, number>> = new Map();
+  const fiberToWarningsMap: Map<number, Map<string, number>> = new Map();
+
+  function clearErrorsAndWarnings() {
+    // eslint-disable-next-line no-for-of-loops/no-for-of-loops
+    for (const id of fiberToErrorsMap.keys()) {
+      fibersWithChangedErrorOrWarningCounts.add(id);
+      updateMostRecentlyInspectedElementIfNecessary(id);
+    }
+
+    // eslint-disable-next-line no-for-of-loops/no-for-of-loops
+    for (const id of fiberToWarningsMap.keys()) {
+      fibersWithChangedErrorOrWarningCounts.add(id);
+      updateMostRecentlyInspectedElementIfNecessary(id);
+    }
+
+    fiberToErrorsMap.clear();
+    fiberToWarningsMap.clear();
+
+    flushPendingEvents();
+  }
+
+  function clearErrorsForFiberID(id: number) {
+    if (fiberToErrorsMap.has(id)) {
+      fiberToErrorsMap.delete(id);
+      fibersWithChangedErrorOrWarningCounts.add(id);
+      flushPendingEvents();
+    }
+
+    updateMostRecentlyInspectedElementIfNecessary(id);
+  }
+
+  function clearWarningsForFiberID(id: number) {
+    if (fiberToWarningsMap.has(id)) {
+      fiberToWarningsMap.delete(id);
+      fibersWithChangedErrorOrWarningCounts.add(id);
+      flushPendingEvents();
+    }
+
+    updateMostRecentlyInspectedElementIfNecessary(id);
+  }
+
+  function updateMostRecentlyInspectedElementIfNecessary(
+    fiberID: number,
+  ): void {
+    if (
+      mostRecentlyInspectedElement !== null &&
+      mostRecentlyInspectedElement.id === fiberID
+    ) {
+      hasElementUpdatedSinceLastInspected = true;
+    }
+  }
+
+  // Called when an error or warning is logged during render, commit, or passive (including unmount functions).
+  function onErrorOrWarning(
+    fiber: Fiber,
+    type: 'error' | 'warn',
+    args: $ReadOnlyArray<any>,
+  ): void {
+    const message = format(...args);
+
+    // Note that by calling these functions we may be creating the ID for the first time.
+    // If the Fiber is then never mounted, we are responsible for cleaning up after ourselves.
+    // This is important because getPrimaryFiber() stores a Fiber in the primaryFibers Set.
+    // If a Fiber never mounts, and we don't clean up after this code, we could leak.
+    // Fortunately we would only leak Fibers that have errors/warnings associated with them,
+    // which is hopefully only a small set and only in DEV mode– but this is still not great.
+    // We should clean up Fibers like this when flushing; see recordPendingErrorsAndWarnings().
+    const fiberID = getFiberID(getPrimaryFiber(fiber));
+
+    // Mark this Fiber as needed its warning/error count updated during the next flush.
+    fibersWithChangedErrorOrWarningCounts.add(fiberID);
+
+    // Update the error/warning messages and counts for the Fiber.
+    const fiberMap = type === 'error' ? fiberToErrorsMap : fiberToWarningsMap;
+    const messageMap = fiberMap.get(fiberID);
+    if (messageMap != null) {
+      const count = messageMap.get(message) || 0;
+      messageMap.set(message, count + 1);
+    } else {
+      fiberMap.set(fiberID, new Map([[message, 1]]));
+    }
+
+    // If this Fiber is currently being inspected, mark it as needing an udpate as well.
+    updateMostRecentlyInspectedElementIfNecessary(fiberID);
+  }
+
   // Patching the console enables DevTools to do a few useful things:
   // * Append component stacks to warnings and error messages
   // * Disable logging during re-renders to inspect hooks (see inspectHooksOfFiber)
   //
   // Don't patch in test environments because we don't want to interfere with Jest's own console overrides.
   if (process.env.NODE_ENV !== 'test') {
-    registerRendererWithConsole(renderer);
+    registerRendererWithConsole(renderer, onErrorOrWarning);
 
     // The renderer interface can't read these preferences directly,
     // because it is stored in localStorage within the context of the extension.
@@ -537,10 +633,13 @@ export function attach(
       window.__REACT_DEVTOOLS_APPEND_COMPONENT_STACK__ !== false;
     const breakOnConsoleErrors =
       window.__REACT_DEVTOOLS_BREAK_ON_CONSOLE_ERRORS__ === true;
+    const showInlineWarningsAndErrors =
+      window.__REACT_DEVTOOLS_SHOW_INLINE_WARNINGS_AND_ERRORS__ === true;
     if (appendComponentStack || breakOnConsoleErrors) {
       patchConsole({
         appendComponentStack,
         breakOnConsoleErrors,
+        showInlineWarningsAndErrors,
       });
     }
   }
@@ -652,8 +751,11 @@ export function attach(
     // Recursively unmount all roots.
     hook.getFiberRoots(rendererID).forEach(root => {
       currentRootID = getFiberID(getPrimaryFiber(root.current));
-      unmountFiberChildrenRecursively(root.current);
-      recordUnmount(root.current, false);
+      // The TREE_OPERATION_REMOVE_ROOT operation serves two purposes:
+      // 1. It avoids sending unnecessary bridge traffic to clear a root.
+      // 2. It preserves Fiber IDs when remounting (below) which in turn ID to error/warning mapping.
+      pushOperation(TREE_OPERATION_REMOVE_ROOT);
+      flushPendingEvents(root);
       currentRootID = -1;
     });
 
@@ -670,6 +772,10 @@ export function attach(
       flushPendingEvents(root);
       currentRootID = -1;
     });
+
+    // Also re-evaluate all error and warning counts given the new filters.
+    reevaluateErrorsAndWarnings();
+    flushPendingEvents();
   }
 
   // NOTICE Keep in sync with get*ForFiber methods
@@ -1055,7 +1161,64 @@ export function attach(
     pendingOperations.push(op);
   }
 
+  function reevaluateErrorsAndWarnings() {
+    fibersWithChangedErrorOrWarningCounts.clear();
+    fiberToErrorsMap.forEach((countMap, fiberID) => {
+      fibersWithChangedErrorOrWarningCounts.add(fiberID);
+    });
+    fiberToWarningsMap.forEach((countMap, fiberID) => {
+      fibersWithChangedErrorOrWarningCounts.add(fiberID);
+    });
+    recordPendingErrorsAndWarnings();
+  }
+
+  function recordPendingErrorsAndWarnings() {
+    fibersWithChangedErrorOrWarningCounts.forEach(fiberID => {
+      const fiber = idToFiberMap.get(fiberID);
+      if (fiber != null) {
+        // Don't send updates for Fibers that didn't mount due to e.g. Suspense or an error boundary.
+        // We may also need to clean up after ourselves to avoid leaks.
+        // See inline comments in onErrorOrWarning() for more info.
+        if (isFiberMountedImpl(fiber) !== MOUNTED) {
+          fiberToIDMap.delete(fiber);
+          idToFiberMap.delete(fiberID);
+          primaryFibers.delete(fiber);
+          return;
+        }
+
+        let errorCount = 0;
+        let warningCount = 0;
+
+        if (!shouldFilterFiber(fiber)) {
+          const errorCountsMap = fiberToErrorsMap.get(fiberID);
+          const warningCountsMap = fiberToWarningsMap.get(fiberID);
+
+          if (errorCountsMap != null) {
+            errorCountsMap.forEach(count => {
+              errorCount += count;
+            });
+          }
+          if (warningCountsMap != null) {
+            warningCountsMap.forEach(count => {
+              warningCount += count;
+            });
+          }
+        }
+
+        pushOperation(TREE_OPERATION_UPDATE_ERRORS_OR_WARNINGS);
+        pushOperation(fiberID);
+        pushOperation(errorCount);
+        pushOperation(warningCount);
+      }
+    });
+    fibersWithChangedErrorOrWarningCounts.clear();
+  }
+
   function flushPendingEvents(root: Object): void {
+    // Add any pending errors and warnings to the operations array.
+    // We do this just before flushing, so we can ignore errors for no-longer-mounted Fibers.
+    recordPendingErrorsAndWarnings();
+
     if (
       pendingOperations.length === 0 &&
       pendingRealUnmountedIDs.length === 0 &&
@@ -2028,17 +2191,42 @@ export function attach(
   // https://github.com/facebook/react/blob/master/packages/react-reconciler/src/ReactFiberTreeReflection.js
   function isFiberMountedImpl(fiber: Fiber): number {
     let node = fiber;
+    let prevNode = null;
     if (!fiber.alternate) {
       // If there is no alternate, this might be a new tree that isn't inserted
       // yet. If it is, then it will have a pending insertion effect on it.
       if ((getFiberFlags(node) & Placement) !== NoFlags) {
         return MOUNTING;
       }
+      // This indicates an error during render.
+      if ((getFiberFlags(node) & Incomplete) !== NoFlags) {
+        return UNMOUNTED;
+      }
       while (node.return) {
+        prevNode = node;
         node = node.return;
+
         if ((getFiberFlags(node) & Placement) !== NoFlags) {
           return MOUNTING;
         }
+        // This indicates an error during render.
+        if ((getFiberFlags(node) & Incomplete) !== NoFlags) {
+          return UNMOUNTED;
+        }
+
+        // If this node is inside of a timed out suspense subtree, we should also ignore errors/warnings.
+        const isTimedOutSuspense =
+          node.tag === SuspenseComponent && node.memoizedState !== null;
+        if (isTimedOutSuspense) {
+          // Note that this does not include errors/warnings in the Fallback tree though!
+          const primaryChildFragment = node.child;
+          const fallbackChildFragment = primaryChildFragment
+            ? primaryChildFragment.sibling
+            : null;
+          if (prevNode !== fallbackChildFragment) {
+            return UNMOUNTED;
+          }
+        }
       }
     } else {
       while (node.return) {
@@ -2455,6 +2643,9 @@ export function attach(
       rootType = fiberRoot._debugRootType;
     }
 
+    const errors = fiberToErrorsMap.get(id) || new Map();
+    const warnings = fiberToWarningsMap.get(id) || new Map();
+
     return {
       id,
 
@@ -2497,6 +2688,8 @@ export function attach(
       hooks,
       props: memoizedProps,
       state: usesHooks ? null : memoizedState,
+      errors: Array.from(errors.entries()),
+      warnings: Array.from(warnings.entries()),
 
       // List of owners
       owners,
@@ -3425,6 +3618,9 @@ export function attach(
 
   return {
     cleanup,
+    clearErrorsAndWarnings,
+    clearErrorsForFiberID,
+    clearWarningsForFiberID,
     copyElementPath,
     deletePath,
     findNativeNodesForFiberID,
diff --git a/packages/react-devtools-shared/src/backend/types.js b/packages/react-devtools-shared/src/backend/types.js
index d87c2fdf97b2e..93ac3c7827e8b 100644
--- a/packages/react-devtools-shared/src/backend/types.js
+++ b/packages/react-devtools-shared/src/backend/types.js
@@ -234,6 +234,8 @@ export type InspectedElement = {|
   props: Object | null,
   state: Object | null,
   key: number | string | null,
+  errors: Array<[string, number]>,
+  warnings: Array<[string, number]>,
 
   // List of owners
   owners: Array<Owner> | null,
@@ -294,6 +296,9 @@ type Type = 'props' | 'hooks' | 'state' | 'context';
 
 export type RendererInterface = {
   cleanup: () => void,
+  clearErrorsAndWarnings: () => void,
+  clearErrorsForFiberID: (id: number) => void,
+  clearWarningsForFiberID: (id: number) => void,
   copyElementPath: (id: number, path: Array<string | number>) => void,
   deletePath: (
     type: Type,
diff --git a/packages/react-devtools-shared/src/backend/utils.js b/packages/react-devtools-shared/src/backend/utils.js
index d0ff4192d6edb..06329eff72150 100644
--- a/packages/react-devtools-shared/src/backend/utils.js
+++ b/packages/react-devtools-shared/src/backend/utils.js
@@ -134,3 +134,52 @@ export function serializeToString(data: any): string {
     return value;
   });
 }
+
+// based on https://github.com/tmpfs/format-util/blob/0e62d430efb0a1c51448709abd3e2406c14d8401/format.js#L1
+// based on https://developer.mozilla.org/en-US/docs/Web/API/console#Using_string_substitutions
+// Implements s, d, i and f placeholders
+export function format(
+  maybeMessage: any,
+  ...inputArgs: $ReadOnlyArray<any>
+): string {
+  if (typeof maybeMessage !== 'string') {
+    return [maybeMessage, ...inputArgs].join(' ');
+  }
+
+  const re = /(%?)(%([jds]))/g;
+  const args = inputArgs.slice();
+  let formatted: string = maybeMessage;
+
+  if (args.length) {
+    formatted = formatted.replace(re, (match, escaped, ptn, flag) => {
+      let arg = args.shift();
+      switch (flag) {
+        case 's':
+          arg += '';
+          break;
+        case 'd':
+        case 'i':
+          arg = parseInt(arg, 10).toString();
+          break;
+        case 'f':
+          arg = parseFloat(arg).toString();
+          break;
+      }
+      if (!escaped) {
+        return arg;
+      }
+      args.unshift(arg);
+      return match;
+    });
+  }
+
+  // arguments remain after formatting
+  if (args.length) {
+    formatted += ' ' + args.join(' ');
+  }
+
+  // update escaped %% values
+  formatted = formatted.replace(/%{2,2}/g, '%');
+
+  return '' + formatted;
+}
diff --git a/packages/react-devtools-shared/src/bridge.js b/packages/react-devtools-shared/src/bridge.js
index 2bde2aed8b071..1da4d8eb3e549 100644
--- a/packages/react-devtools-shared/src/bridge.js
+++ b/packages/react-devtools-shared/src/bridge.js
@@ -114,6 +114,7 @@ type NativeStyleEditor_SetValueParams = {|
 type UpdateConsolePatchSettingsParams = {|
   appendComponentStack: boolean,
   breakOnConsoleErrors: boolean,
+  showInlineWarningsAndErrors: boolean,
 |};
 
 type BackendEvents = {|
@@ -141,7 +142,10 @@ type BackendEvents = {|
 |};
 
 type FrontendEvents = {|
+  clearErrorsAndWarnings: [{|rendererID: RendererID|}],
+  clearErrorsForFiberID: [ElementAndRendererID],
   clearNativeElementHighlight: [],
+  clearWarningsForFiberID: [ElementAndRendererID],
   copyElementPath: [CopyElementPathParams],
   deletePath: [DeletePath],
   getOwnersList: [ElementAndRendererID],
diff --git a/packages/react-devtools-shared/src/constants.js b/packages/react-devtools-shared/src/constants.js
index c5223f7cb43d9..f8868111e095d 100644
--- a/packages/react-devtools-shared/src/constants.js
+++ b/packages/react-devtools-shared/src/constants.js
@@ -14,6 +14,8 @@ export const TREE_OPERATION_ADD = 1;
 export const TREE_OPERATION_REMOVE = 2;
 export const TREE_OPERATION_REORDER_CHILDREN = 3;
 export const TREE_OPERATION_UPDATE_TREE_BASE_DURATION = 4;
+export const TREE_OPERATION_UPDATE_ERRORS_OR_WARNINGS = 5;
+export const TREE_OPERATION_REMOVE_ROOT = 6;
 
 export const LOCAL_STORAGE_FILTER_PREFERENCES_KEY =
   'React::DevTools::componentFilters';
@@ -33,6 +35,9 @@ export const LOCAL_STORAGE_SHOULD_BREAK_ON_CONSOLE_ERRORS =
 export const LOCAL_STORAGE_SHOULD_PATCH_CONSOLE_KEY =
   'React::DevTools::appendComponentStack';
 
+export const LOCAL_STORAGE_SHOW_INLINE_WARNINGS_AND_ERRORS_KEY =
+  'React::DevTools::showInlineWarningsAndErrors';
+
 export const LOCAL_STORAGE_TRACE_UPDATES_ENABLED_KEY =
   'React::DevTools::traceUpdatesEnabled';
 
diff --git a/packages/react-devtools-shared/src/devtools/store.js b/packages/react-devtools-shared/src/devtools/store.js
index cd829bb3bf3ab..6b1b83e524838 100644
--- a/packages/react-devtools-shared/src/devtools/store.js
+++ b/packages/react-devtools-shared/src/devtools/store.js
@@ -12,7 +12,9 @@ import {inspect} from 'util';
 import {
   TREE_OPERATION_ADD,
   TREE_OPERATION_REMOVE,
+  TREE_OPERATION_REMOVE_ROOT,
   TREE_OPERATION_REORDER_CHILDREN,
+  TREE_OPERATION_UPDATE_ERRORS_OR_WARNINGS,
   TREE_OPERATION_UPDATE_TREE_BASE_DURATION,
 } from '../constants';
 import {ElementTypeRoot} from '../types';
@@ -78,11 +80,22 @@ export default class Store extends EventEmitter<{|
 |}> {
   _bridge: FrontendBridge;
 
+  // Computed whenever _errorsAndWarnings Map changes.
+  _cachedErrorCount: number = 0;
+  _cachedWarningCount: number = 0;
+  _cachedErrorAndWarningTuples: Array<{|id: number, index: number|}> = [];
+
   // Should new nodes be collapsed by default when added to the tree?
   _collapseNodesByDefault: boolean = true;
 
   _componentFilters: Array<ComponentFilter>;
 
+  // Map of ID to number of recorded error and warning message IDs.
+  _errorsAndWarnings: Map<
+    number,
+    {|errorCount: number, warningCount: number|},
+  > = new Map();
+
   // At least one of the injected renderers contains (DEV only) owner metadata.
   _hasOwnerMetadata: boolean = false;
 
@@ -289,6 +302,10 @@ export default class Store extends EventEmitter<{|
     this.emit('componentFilters');
   }
 
+  get errorCount(): number {
+    return this._cachedErrorCount;
+  }
+
   get hasOwnerMetadata(): boolean {
     return this._hasOwnerMetadata;
   }
@@ -357,6 +374,46 @@ export default class Store extends EventEmitter<{|
     return this._unsupportedRendererVersionDetected;
   }
 
+  get warningCount(): number {
+    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;
   }
@@ -425,6 +482,17 @@ export default class Store extends EventEmitter<{|
     return element;
   }
 
+  // Returns a tuple of [id, index]
+  getElementsWithErrorsAndWarnings(): Array<{|id: number, index: number|}> {
+    return this._cachedErrorAndWarningTuples;
+  }
+
+  getErrorAndWarningCountForElementID(
+    id: number,
+  ): {|errorCount: number, warningCount: number|} {
+    return this._errorsAndWarnings.get(id) || {errorCount: 0, warningCount: 0};
+  }
+
   getIndexOfElementID(id: number): number | null {
     const element = this.getElementByID(id);
 
@@ -709,6 +777,7 @@ export default class Store extends EventEmitter<{|
     }
 
     let haveRootsChanged = false;
+    let haveErrorsOrWarningsChanged = false;
 
     // The first two values are always rendererID and rootID
     const rendererID = operations[0];
@@ -910,7 +979,41 @@ export default class Store extends EventEmitter<{|
                 set.delete(id);
               }
             }
+
+            if (this._errorsAndWarnings.has(id)) {
+              this._errorsAndWarnings.delete(id);
+              haveErrorsOrWarningsChanged = true;
+            }
+          }
+          break;
+        }
+        case TREE_OPERATION_REMOVE_ROOT: {
+          i += 1;
+
+          const id = operations[1];
+
+          if (__DEBUG__) {
+            debug(`Remove root ${id}`);
           }
+
+          const recursivelyDeleteElements = elementID => {
+            const element = this._idToElement.get(elementID);
+            this._idToElement.delete(elementID);
+            if (element) {
+              // Mostly for Flow's sake
+              for (let index = 0; index < element.children.length; index++) {
+                recursivelyDeleteElements(element.children[index]);
+              }
+            }
+          };
+
+          const root = ((this._idToElement.get(id): any): Element);
+          recursivelyDeleteElements(id);
+
+          this._rootIDToCapabilities.delete(id);
+          this._rootIDToRendererID.delete(id);
+          this._roots = this._roots.filter(rootID => rootID !== id);
+          this._weightAcrossRoots -= root.weight;
           break;
         }
         case TREE_OPERATION_REORDER_CHILDREN: {
@@ -958,6 +1061,20 @@ export default class Store extends EventEmitter<{|
           // The profiler UI uses them lazily in order to generate the tree.
           i += 3;
           break;
+        case TREE_OPERATION_UPDATE_ERRORS_OR_WARNINGS:
+          const id = operations[i + 1];
+          const errorCount = operations[i + 2];
+          const warningCount = operations[i + 3];
+
+          i += 4;
+
+          if (errorCount > 0 || warningCount > 0) {
+            this._errorsAndWarnings.set(id, {errorCount, warningCount});
+          } else if (this._errorsAndWarnings.has(id)) {
+            this._errorsAndWarnings.delete(id);
+          }
+          haveErrorsOrWarningsChanged = true;
+          break;
         default:
           throw Error(`Unsupported Bridge operation ${operation}`);
       }
@@ -965,6 +1082,41 @@ export default class Store extends EventEmitter<{|
 
     this._revision++;
 
+    if (haveErrorsOrWarningsChanged) {
+      let errorCount = 0;
+      let warningCount = 0;
+
+      this._errorsAndWarnings.forEach(entry => {
+        errorCount += entry.errorCount;
+        warningCount += entry.warningCount;
+      });
+
+      this._cachedErrorCount = errorCount;
+      this._cachedWarningCount = warningCount;
+
+      const errorAndWarningTuples: Array<{|id: number, index: number|}> = [];
+
+      this._errorsAndWarnings.forEach((_, id) => {
+        const index = this.getIndexOfElementID(id);
+        if (index !== null) {
+          let low = 0;
+          let high = errorAndWarningTuples.length;
+          while (low < high) {
+            const mid = (low + high) >> 1;
+            if (errorAndWarningTuples[mid].index > index) {
+              high = mid;
+            } else {
+              low = mid + 1;
+            }
+          }
+
+          errorAndWarningTuples.splice(low, 0, {id, index});
+        }
+      });
+
+      this._cachedErrorAndWarningTuples = errorAndWarningTuples;
+    }
+
     if (haveRootsChanged) {
       const prevSupportsProfiling = this._supportsProfiling;
 
diff --git a/packages/react-devtools-shared/src/devtools/utils.js b/packages/react-devtools-shared/src/devtools/utils.js
index 46b15d18a1159..4e71d70bd68f5 100644
--- a/packages/react-devtools-shared/src/devtools/utils.js
+++ b/packages/react-devtools-shared/src/devtools/utils.js
@@ -10,6 +10,7 @@
 import JSON5 from 'json5';
 
 import type {Element} from './views/Components/types';
+import type {StateContext} from './views/Components/TreeContext';
 import type Store from './store';
 
 export function printElement(element: Element, includeWeight: boolean = false) {
@@ -49,40 +50,96 @@ export function printOwnersList(
     .join('\n');
 }
 
-export function printStore(store: Store, includeWeight: boolean = false) {
+export function printStore(
+  store: Store,
+  includeWeight: boolean = false,
+  state: StateContext | null = null,
+) {
   const snapshotLines = [];
 
   let rootWeight = 0;
 
-  store.roots.forEach(rootID => {
-    const {weight} = ((store.getElementByID(rootID): any): Element);
+  function printSelectedMarker(index: number): string {
+    if (state === null) {
+      return '';
+    }
+    return state.selectedElementIndex === index ? `→` : ' ';
+  }
+
+  function printErrorsAndWarnings(element: Element): string {
+    const {
+      errorCount,
+      warningCount,
+    } = store.getErrorAndWarningCountForElementID(element.id);
+    if (errorCount === 0 && warningCount === 0) {
+      return '';
+    }
+    return ` ${errorCount > 0 ? '✕' : ''}${warningCount > 0 ? '⚠' : ''}`;
+  }
+
+  const ownerFlatTree = state !== null ? state.ownerFlatTree : null;
+  if (ownerFlatTree !== null) {
+    snapshotLines.push(
+      '[owners]' + (includeWeight ? ` (${ownerFlatTree.length})` : ''),
+    );
+    ownerFlatTree.forEach((element, index) => {
+      const printedSelectedMarker = printSelectedMarker(index);
+      const printedElement = printElement(element, false);
+      const printedErrorsAndWarnings = printErrorsAndWarnings(element);
+      snapshotLines.push(
+        `${printedSelectedMarker}${printedElement}${printedErrorsAndWarnings}`,
+      );
+    });
+  } else {
+    const errorsAndWarnings = store._errorsAndWarnings;
+    if (errorsAndWarnings.size > 0) {
+      let errorCount = 0;
+      let warningCount = 0;
+      errorsAndWarnings.forEach(entry => {
+        errorCount += entry.errorCount;
+        warningCount += entry.warningCount;
+      });
+
+      snapshotLines.push(`✕ ${errorCount}, ⚠ ${warningCount}`);
+    }
+
+    store.roots.forEach(rootID => {
+      const {weight} = ((store.getElementByID(rootID): any): Element);
+      const maybeWeightLabel = includeWeight ? ` (${weight})` : '';
+
+      // Store does not (yet) expose a way to get errors/warnings per root.
+      snapshotLines.push(`[root]${maybeWeightLabel}`);
 
-    snapshotLines.push('[root]' + (includeWeight ? ` (${weight})` : ''));
+      for (let i = rootWeight; i < rootWeight + weight; i++) {
+        const element = store.getElementAtIndex(i);
 
-    for (let i = rootWeight; i < rootWeight + weight; i++) {
-      const element = store.getElementAtIndex(i);
+        if (element == null) {
+          throw Error(`Could not find element at index ${i}`);
+        }
 
-      if (element == null) {
-        throw Error(`Could not find element at index ${i}`);
+        const printedSelectedMarker = printSelectedMarker(i);
+        const printedElement = printElement(element, includeWeight);
+        const printedErrorsAndWarnings = printErrorsAndWarnings(element);
+        snapshotLines.push(
+          `${printedSelectedMarker}${printedElement}${printedErrorsAndWarnings}`,
+        );
       }
 
-      snapshotLines.push(printElement(element, includeWeight));
-    }
+      rootWeight += weight;
+    });
 
-    rootWeight += weight;
-  });
+    // Make sure the pretty-printed test align with the Store's reported number of total rows.
+    if (rootWeight !== store.numElements) {
+      throw Error(
+        `Inconsistent Store state. Individual root weights (${rootWeight}) do not match total weight (${store.numElements})`,
+      );
+    }
 
-  // Make sure the pretty-printed test align with the Store's reported number of total rows.
-  if (rootWeight !== store.numElements) {
-    throw Error(
-      `Inconsistent Store state. Individual root weights (${rootWeight}) do not match total weight (${store.numElements})`,
-    );
+    // If roots have been unmounted, verify that they've been removed from maps.
+    // This helps ensure the Store doesn't leak memory.
+    store.assertExpectedRootMapSizes();
   }
 
-  // If roots have been unmounted, verify that they've been removed from maps.
-  // This helps ensure the Store doesn't leak memory.
-  store.assertExpectedRootMapSizes();
-
   return snapshotLines.join('\n');
 }
 
diff --git a/packages/react-devtools-shared/src/devtools/views/Components/Element.css b/packages/react-devtools-shared/src/devtools/views/Components/Element.css
index cd92bd3937d37..a21b303a3504a 100644
--- a/packages/react-devtools-shared/src/devtools/views/Components/Element.css
+++ b/packages/react-devtools-shared/src/devtools/views/Components/Element.css
@@ -73,3 +73,21 @@
 .Badge {
   margin-left: 0.25rem;
 }
+
+.ErrorIcon,
+.ErrorIconContrast,
+.WarningIcon,
+.WarningIconContrast {
+  height: 0.75rem !important;
+  width: 0.75rem !important;
+  margin-left: 0.25rem;
+}
+.ErrorIcon {
+  color: var(--color-console-error-icon);
+}
+.WarningIcon {
+  color: var(--color-console-warning-icon);
+}
+.ErrorIconContrast, .WarningIconContrast {
+  color: var(--color-component-name);
+}
diff --git a/packages/react-devtools-shared/src/devtools/views/Components/Element.js b/packages/react-devtools-shared/src/devtools/views/Components/Element.js
index a209a55ace781..73789a9bd62ff 100644
--- a/packages/react-devtools-shared/src/devtools/views/Components/Element.js
+++ b/packages/react-devtools-shared/src/devtools/views/Components/Element.js
@@ -14,12 +14,15 @@ import Badge from './Badge';
 import ButtonIcon from '../ButtonIcon';
 import {createRegExp} from '../utils';
 import {TreeDispatcherContext, TreeStateContext} from './TreeContext';
+import {SettingsContext} from '../Settings/SettingsContext';
 import {StoreContext} from '../context';
+import {useSubscription} from '../hooks';
 
 import type {ItemData} from './Tree';
-import type {Element} from './types';
+import type {Element as ElementType} from './types';
 
 import styles from './Element.css';
+import Icon from '../Icon';
 
 type Props = {
   data: ItemData,
@@ -28,12 +31,13 @@ type Props = {
   ...
 };
 
-export default function ElementView({data, index, style}: Props) {
+export default function Element({data, index, style}: Props) {
   const store = useContext(StoreContext);
   const {ownerFlatTree, ownerID, selectedElementID} = useContext(
     TreeStateContext,
   );
   const dispatch = useContext(TreeDispatcherContext);
+  const {showInlineWarningsAndErrors} = React.useContext(SettingsContext);
 
   const element =
     ownerFlatTree !== null
@@ -46,6 +50,24 @@ export default function ElementView({data, index, style}: Props) {
   const id = element === null ? null : element.id;
   const isSelected = selectedElementID === id;
 
+  const errorsAndWarningsSubscription = useMemo(
+    () => ({
+      getCurrentValue: () =>
+        element === null
+          ? {errorCount: 0, warningCount: 0}
+          : store.getErrorAndWarningCountForElementID(element.id),
+      subscribe: (callback: Function) => {
+        store.addListener('mutated', callback);
+        return () => store.removeListener('mutated', callback);
+      },
+    }),
+    [store, element],
+  );
+  const {errorCount, warningCount} = useSubscription<{|
+    errorCount: number,
+    warningCount: number,
+  |}>(errorsAndWarningsSubscription);
+
   const handleDoubleClick = () => {
     if (id !== null) {
       dispatch({type: 'SELECT_OWNER', payload: id});
@@ -81,7 +103,7 @@ export default function ElementView({data, index, style}: Props) {
 
   // Handle elements that are removed from the tree while an async render is in progress.
   if (element == null) {
-    console.warn(`<ElementView> Could not find element at index ${index}`);
+    console.warn(`<Element> Could not find element at index ${index}`);
 
     // This return needs to happen after hooks, since hooks can't be conditional.
     return null;
@@ -93,7 +115,7 @@ export default function ElementView({data, index, style}: Props) {
     hocDisplayNames,
     key,
     type,
-  } = ((element: any): Element);
+  } = ((element: any): ElementType);
 
   let className = styles.Element;
   if (isSelected) {
@@ -148,6 +170,26 @@ export default function ElementView({data, index, style}: Props) {
             />
           </Badge>
         ) : null}
+        {showInlineWarningsAndErrors && errorCount > 0 && (
+          <Icon
+            type="error"
+            className={
+              isSelected && treeFocused
+                ? styles.ErrorIconContrast
+                : styles.ErrorIcon
+            }
+          />
+        )}
+        {showInlineWarningsAndErrors && warningCount > 0 && (
+          <Icon
+            type="warning"
+            className={
+              isSelected && treeFocused
+                ? styles.WarningIconContrast
+                : styles.WarningIcon
+            }
+          />
+        )}
       </div>
     </div>
   );
@@ -160,7 +202,7 @@ const swallowDoubleClick = event => {
 };
 
 type ExpandCollapseToggleProps = {|
-  element: Element,
+  element: ElementType,
   store: Store,
 |};
 
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 f80f099f52479..01b05a9fa33a1 100644
--- a/packages/react-devtools-shared/src/devtools/views/Components/InspectedElementContext.js
+++ b/packages/react-devtools-shared/src/devtools/views/Components/InspectedElementContext.js
@@ -51,10 +51,13 @@ export type GetInspectedElement = (
   id: number,
 ) => InspectedElementFrontend | null;
 
+type RefreshInspectedElement = () => void;
+
 export type InspectedElementContextType = {|
   copyInspectedElementPath: CopyInspectedElementPath,
   getInspectedElementPath: GetInspectedElementPath,
   getInspectedElement: GetInspectedElement,
+  refreshInspectedElement: RefreshInspectedElement,
   storeAsGlobal: StoreAsGlobal,
 |};
 
@@ -159,6 +162,15 @@ function InspectedElementContextController({children}: Props) {
   // 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,
@@ -217,6 +229,8 @@ function InspectedElementContextController({children}: Props) {
             rootType,
             state,
             key,
+            errors,
+            warnings,
           } = ((data.value: any): InspectedElementBackend);
 
           const inspectedElement: InspectedElementFrontend = {
@@ -257,6 +271,8 @@ function InspectedElementContextController({children}: Props) {
             hooks: hydrateHelper(hooks),
             props: hydrateHelper(props),
             state: hydrateHelper(state),
+            errors,
+            warnings,
           };
 
           element = store.getElementByID(id);
@@ -343,6 +359,7 @@ function InspectedElementContextController({children}: Props) {
       copyInspectedElementPath,
       getInspectedElement,
       getInspectedElementPath,
+      refreshInspectedElement,
       storeAsGlobal,
     }),
     // InspectedElement is used to invalidate the cache and schedule an update with React.
@@ -351,6 +368,7 @@ function InspectedElementContextController({children}: Props) {
       currentlyInspectedElement,
       getInspectedElement,
       getInspectedElementPath,
+      refreshInspectedElement,
       storeAsGlobal,
     ],
   );
diff --git a/packages/react-devtools-shared/src/devtools/views/Components/InspectedElementErrorsAndWarningsTree.css b/packages/react-devtools-shared/src/devtools/views/Components/InspectedElementErrorsAndWarningsTree.css
new file mode 100644
index 0000000000000..6ef5583b8957c
--- /dev/null
+++ b/packages/react-devtools-shared/src/devtools/views/Components/InspectedElementErrorsAndWarningsTree.css
@@ -0,0 +1,60 @@
+.ErrorTree, .WarningTree {
+  padding: 0.25rem 0 0 0;
+}
+
+.HeaderRow {
+  padding: 0 0.25rem;
+}
+
+.HeaderRow {
+  padding: 0 0.25rem;
+}
+
+.Error, .Warning {
+  padding: 0 0.5rem;
+  display: flex;
+  align-items: center;
+}
+
+.Error {
+  border-top: 1px solid var(--color-console-error-border);
+  background-color: var(--color-console-error-background);
+  color: var(--color-error-text);
+  padding: 0 0.5rem;
+}
+
+.Warning {
+  border-top: 1px solid var(--color-console-warning-border);
+  background-color: var(--color-console-warning-background);
+  color: var(--color-warning-text);
+  padding: 0 0.5rem;
+}
+
+.Message {
+  overflow-x: hidden;
+  text-overflow: ellipsis;
+  white-space: nowrap;
+}
+
+.ErrorBadge,
+.WarningBadge {
+  display: inline-block;
+  width: 0.75rem;
+  height: 0.75rem;
+  flex: 0 0 0.75rem;
+  line-height: 0.75rem;
+  text-align: center;
+  border-radius: 0.25rem;
+  margin-right: 0.25rem;
+  font-size: var(--font-size-monospace-small);
+}
+
+.ErrorBadge {
+  background-color: var(--color-console-error-icon);
+  color: var(--color-console-error-badge-text);
+}
+
+.WarningBadge {
+  background-color: var(--color-console-warning-icon);
+  color: var(--color-console-warning-badge-text);
+}
diff --git a/packages/react-devtools-shared/src/devtools/views/Components/InspectedElementErrorsAndWarningsTree.js b/packages/react-devtools-shared/src/devtools/views/Components/InspectedElementErrorsAndWarningsTree.js
new file mode 100644
index 0000000000000..1f161c98836b6
--- /dev/null
+++ b/packages/react-devtools-shared/src/devtools/views/Components/InspectedElementErrorsAndWarningsTree.js
@@ -0,0 +1,157 @@
+/**
+ * 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 {useContext} 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 type {InspectedElement} from './types';
+import type {FrontendBridge} from 'react-devtools-shared/src/bridge';
+
+type Props = {|
+  bridge: FrontendBridge,
+  inspectedElement: InspectedElement,
+  store: Store,
+|};
+
+export default function InspectedElementErrorsAndWarningsTree({
+  bridge,
+  inspectedElement,
+  store,
+}: Props) {
+  const {refreshInspectedElement} = useContext(InspectedElementContext);
+
+  const {showInlineWarningsAndErrors} = useContext(SettingsContext);
+  if (!showInlineWarningsAndErrors) {
+    return null;
+  }
+
+  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 && (
+        <Tree
+          badgeClassName={styles.ErrorBadge}
+          bridge={bridge}
+          className={styles.ErrorTree}
+          clearMessages={clearErrors}
+          entries={errors}
+          label="errors"
+          messageClassName={styles.Error}
+        />
+      )}
+      {warnings.length > 0 && (
+        <Tree
+          badgeClassName={styles.WarningBadge}
+          bridge={bridge}
+          className={styles.WarningTree}
+          clearMessages={clearWarnings}
+          entries={warnings}
+          label="warnings"
+          messageClassName={styles.Warning}
+        />
+      )}
+    </React.Fragment>
+  );
+}
+
+type TreeProps = {|
+  badgeClassName: string,
+  actions: React$Node,
+  className: string,
+  clearMessages: () => {},
+  entries: Array<[string, number]>,
+  label: string,
+  messageClassName: string,
+|};
+
+function Tree({
+  badgeClassName,
+  actions,
+  className,
+  clearMessages,
+  entries,
+  label,
+  messageClassName,
+}: TreeProps) {
+  if (entries.length === 0) {
+    return null;
+  }
+  return (
+    <div className={`${sharedStyles.InspectedElementTree} ${className}`}>
+      <div className={`${sharedStyles.HeaderRow} ${styles.HeaderRow}`}>
+        <div className={sharedStyles.Header}>{label}</div>
+        <Button
+          onClick={clearMessages}
+          title={`Clear all ${label} for this component`}>
+          <ButtonIcon type="clear" />
+        </Button>
+      </div>
+      {entries.map(([message, count], index) => (
+        <ErrorOrWarningView
+          key={`${label}-${index}`}
+          badgeClassName={badgeClassName}
+          className={messageClassName}
+          count={count}
+          message={message}
+        />
+      ))}
+    </div>
+  );
+}
+
+type ErrorOrWarningViewProps = {|
+  badgeClassName: string,
+  className: string,
+  count: number,
+  message: string,
+|};
+
+function ErrorOrWarningView({
+  className,
+  badgeClassName,
+  count,
+  message,
+}: ErrorOrWarningViewProps) {
+  return (
+    <div className={className}>
+      {count > 1 && <div className={badgeClassName}>{count}</div>}
+      <div className={styles.Message} title={message}>
+        {message}
+      </div>
+    </div>
+  );
+}
diff --git a/packages/react-devtools-shared/src/devtools/views/Components/InspectedElementSharedStyles.css b/packages/react-devtools-shared/src/devtools/views/Components/InspectedElementSharedStyles.css
index 2da0a11286bf6..0e76706bcf138 100644
--- a/packages/react-devtools-shared/src/devtools/views/Components/InspectedElementSharedStyles.css
+++ b/packages/react-devtools-shared/src/devtools/views/Components/InspectedElementSharedStyles.css
@@ -12,6 +12,8 @@
 }
 
 .Header {
+  display: flex;
+  align-items: center;
   flex: 1 1;
   font-family: var(--font-family-sans);
 }
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 fd2c514e57823..a1c77b9b37b18 100644
--- a/packages/react-devtools-shared/src/devtools/views/Components/InspectedElementView.js
+++ b/packages/react-devtools-shared/src/devtools/views/Components/InspectedElementView.js
@@ -19,6 +19,7 @@ import ButtonIcon from '../ButtonIcon';
 import Icon from '../Icon';
 import HocBadges from './HocBadges';
 import InspectedElementContextTree from './InspectedElementContextTree';
+import InspectedElementErrorsAndWarningsTree from './InspectedElementErrorsAndWarningsTree';
 import InspectedElementHooksTree from './InspectedElementHooksTree';
 import InspectedElementPropsTree from './InspectedElementPropsTree';
 import InspectedElementStateTree from './InspectedElementStateTree';
@@ -120,6 +121,13 @@ export default function InspectedElementView({
           store={store}
         />
 
+        <InspectedElementErrorsAndWarningsTree
+          bridge={bridge}
+          getInspectedElementPath={getInspectedElementPath}
+          inspectedElement={inspectedElement}
+          store={store}
+        />
+
         <NativeStyleEditor />
 
         {showRenderedBy && (
diff --git a/packages/react-devtools-shared/src/devtools/views/Components/SearchInput.js b/packages/react-devtools-shared/src/devtools/views/Components/SearchInput.js
index 0fe2ccaa78ea3..736d39fc75244 100644
--- a/packages/react-devtools-shared/src/devtools/views/Components/SearchInput.js
+++ b/packages/react-devtools-shared/src/devtools/views/Components/SearchInput.js
@@ -85,42 +85,44 @@ export default function SearchInput(props: Props) {
         value={searchText}
       />
       {!!searchText && (
-        <span className={styles.IndexLabel}>
-          {Math.min(searchIndex + 1, searchResults.length)} |{' '}
-          {searchResults.length}
-        </span>
+        <React.Fragment>
+          <span className={styles.IndexLabel}>
+            {Math.min(searchIndex + 1, searchResults.length)} |{' '}
+            {searchResults.length}
+          </span>
+          <div className={styles.LeftVRule} />
+          <Button
+            className={styles.IconButton}
+            disabled={!searchText}
+            onClick={() => dispatch({type: 'GO_TO_PREVIOUS_SEARCH_RESULT'})}
+            title={
+              <React.Fragment>
+                Scroll to previous search result (<kbd>Shift</kbd> +{' '}
+                <kbd>Enter</kbd>)
+              </React.Fragment>
+            }>
+            <ButtonIcon type="up" />
+          </Button>
+          <Button
+            className={styles.IconButton}
+            disabled={!searchText}
+            onClick={() => dispatch({type: 'GO_TO_NEXT_SEARCH_RESULT'})}
+            title={
+              <React.Fragment>
+                Scroll to next search result (<kbd>Enter</kbd>)
+              </React.Fragment>
+            }>
+            <ButtonIcon type="down" />
+          </Button>
+          <Button
+            className={styles.IconButton}
+            disabled={!searchText}
+            onClick={resetSearch}
+            title="Reset search">
+            <ButtonIcon type="close" />
+          </Button>
+        </React.Fragment>
       )}
-      <div className={styles.LeftVRule} />
-      <Button
-        className={styles.IconButton}
-        disabled={!searchText}
-        onClick={() => dispatch({type: 'GO_TO_PREVIOUS_SEARCH_RESULT'})}
-        title={
-          <React.Fragment>
-            Scroll to previous search result (<kbd>Shift</kbd> +{' '}
-            <kbd>Enter</kbd>)
-          </React.Fragment>
-        }>
-        <ButtonIcon type="up" />
-      </Button>
-      <Button
-        className={styles.IconButton}
-        disabled={!searchText}
-        onClick={() => dispatch({type: 'GO_TO_NEXT_SEARCH_RESULT'})}
-        title={
-          <React.Fragment>
-            Scroll to next search result (<kbd>Enter</kbd>)
-          </React.Fragment>
-        }>
-        <ButtonIcon type="down" />
-      </Button>
-      <Button
-        className={styles.IconButton}
-        disabled={!searchText}
-        onClick={resetSearch}
-        title="Reset search">
-        <ButtonIcon type="close" />
-      </Button>
     </div>
   );
 }
diff --git a/packages/react-devtools-shared/src/devtools/views/Components/Tree.css b/packages/react-devtools-shared/src/devtools/views/Components/Tree.css
index 64a5983ee06d9..2036695a50e34 100644
--- a/packages/react-devtools-shared/src/devtools/views/Components/Tree.css
+++ b/packages/react-devtools-shared/src/devtools/views/Components/Tree.css
@@ -44,6 +44,7 @@
 .VRule {
   height: 20px;
   width: 1px;
+  flex: 0 0 1px;
   margin: 0 0.5rem;
   background-color: var(--color-border);
 }
@@ -58,3 +59,23 @@
   font-size: var(--font-size-sans-large);
   color: var(--color-dim);
 }
+
+.IconAndCount {
+  display: flex;
+  align-items: center;
+  font-size: var(--font-size-sans-normal);
+}
+
+.ErrorIcon, .WarningIcon {
+  width: 0.75rem;
+  height: 0.75rem;
+  margin-left: 0.25rem;
+  margin-right: 0.25rem;
+  flex: 0 0 auto;
+}
+.ErrorIcon {
+  color: var(--color-console-error-icon);
+}
+.WarningIcon {
+  color: var(--color-console-warning-icon);
+}
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 fe5221a66f166..467af81b77f3d 100644
--- a/packages/react-devtools-shared/src/devtools/views/Components/Tree.js
+++ b/packages/react-devtools-shared/src/devtools/views/Components/Tree.js
@@ -21,18 +21,21 @@ import {
 import AutoSizer from 'react-virtualized-auto-sizer';
 import {FixedSizeList} from 'react-window';
 import {TreeDispatcherContext, TreeStateContext} from './TreeContext';
+import Icon from '../Icon';
 import {SettingsContext} from '../Settings/SettingsContext';
 import {BridgeContext, StoreContext} from '../context';
-import ElementView from './Element';
+import Element from './Element';
 import InspectHostNodesToggle from './InspectHostNodesToggle';
 import OwnersStack from './OwnersStack';
 import SearchInput from './SearchInput';
 import SettingsModalContextToggle from 'react-devtools-shared/src/devtools/views/Settings/SettingsModalContextToggle';
 import SelectedTreeHighlight from './SelectedTreeHighlight';
 import TreeFocusedContext from './TreeFocusedContext';
-import {useHighlightNativeElement} from '../hooks';
+import {useHighlightNativeElement, useSubscription} from '../hooks';
 
 import styles from './Tree.css';
+import ButtonIcon from '../ButtonIcon';
+import Button from '../Button';
 
 // Never indent more than this number of pixels (even if we have the room).
 const DEFAULT_INDENTATION_SIZE = 12;
@@ -71,7 +74,7 @@ export default function Tree(props: Props) {
 
   const [treeFocused, setTreeFocused] = useState<boolean>(false);
 
-  const {lineHeight} = useContext(SettingsContext);
+  const {lineHeight, showInlineWarningsAndErrors} = useContext(SettingsContext);
 
   // Make sure a newly selected element is visible in the list.
   // This is helpful for things like the owners list and search.
@@ -301,6 +304,29 @@ export default function Tree(props: Props) {
     [store],
   );
 
+  const handlePreviousErrorOrWarningClick = React.useCallback(() => {
+    dispatch({type: 'SELECT_PREVIOUS_ELEMENT_WITH_ERROR_OR_WARNING_IN_TREE'});
+  }, []);
+
+  const handleNextErrorOrWarningClick = React.useCallback(() => {
+    dispatch({type: 'SELECT_NEXT_ELEMENT_WITH_ERROR_OR_WARNING_IN_TREE'});
+  }, []);
+
+  const errorsOrWarningsSubscription = useMemo(
+    () => ({
+      getCurrentValue: () => ({
+        errors: store.errorCount,
+        warnings: store.warningCount,
+      }),
+      subscribe: (callback: Function) => {
+        store.addListener('mutated', callback);
+        return () => store.removeListener('mutated', callback);
+      },
+    }),
+    [store],
+  );
+  const {errors, warnings} = useSubscription(errorsOrWarningsSubscription);
+
   return (
     <TreeFocusedContext.Provider value={treeFocused}>
       <div className={styles.Tree} ref={treeRef}>
@@ -315,6 +341,40 @@ export default function Tree(props: Props) {
             {ownerID !== null ? <OwnersStack /> : <SearchInput />}
           </Suspense>
           <div className={styles.VRule} />
+          {showInlineWarningsAndErrors &&
+            ownerID === null &&
+            (errors > 0 || warnings > 0) && (
+              <React.Fragment>
+                {errors > 0 && (
+                  <div className={styles.IconAndCount}>
+                    <Icon className={styles.ErrorIcon} type="error" />
+                    {errors}
+                  </div>
+                )}
+                {warnings > 0 && (
+                  <div className={styles.IconAndCount}>
+                    <Icon className={styles.WarningIcon} type="warning" />
+                    {warnings}
+                  </div>
+                )}
+                <Button
+                  onClick={handlePreviousErrorOrWarningClick}
+                  title="Scroll to previous error or warning">
+                  <ButtonIcon type="up" />
+                </Button>
+                <Button
+                  onClick={handleNextErrorOrWarningClick}
+                  title="Scroll to next error or warning">
+                  <ButtonIcon type="down" />
+                </Button>
+                <Button
+                  onClick={() => store.clearErrorsAndWarnings()}
+                  title="Clear all errors and warnings">
+                  <ButtonIcon type="clear" />
+                </Button>
+                <div className={styles.VRule} />
+              </React.Fragment>
+            )}
           <SettingsModalContextToggle />
         </div>
         <div
@@ -339,7 +399,7 @@ export default function Tree(props: Props) {
                 itemSize={lineHeight}
                 ref={listCallbackRef}
                 width={width}>
-                {ElementView}
+                {Element}
               </FixedSizeList>
             )}
           </AutoSizer>
diff --git a/packages/react-devtools-shared/src/devtools/views/Components/TreeContext.js b/packages/react-devtools-shared/src/devtools/views/Components/TreeContext.js
index f748a9f3548af..a824bd493ff84 100644
--- a/packages/react-devtools-shared/src/devtools/views/Components/TreeContext.js
+++ b/packages/react-devtools-shared/src/devtools/views/Components/TreeContext.js
@@ -93,6 +93,9 @@ type ACTION_SELECT_ELEMENT_BY_ID = {|
 type ACTION_SELECT_NEXT_ELEMENT_IN_TREE = {|
   type: 'SELECT_NEXT_ELEMENT_IN_TREE',
 |};
+type ACTION_SELECT_NEXT_ELEMENT_WITH_ERROR_OR_WARNING_IN_TREE = {|
+  type: 'SELECT_NEXT_ELEMENT_WITH_ERROR_OR_WARNING_IN_TREE',
+|};
 type ACTION_SELECT_NEXT_SIBLING_IN_TREE = {|
   type: 'SELECT_NEXT_SIBLING_IN_TREE',
 |};
@@ -106,6 +109,9 @@ type ACTION_SELECT_PARENT_ELEMENT_IN_TREE = {|
 type ACTION_SELECT_PREVIOUS_ELEMENT_IN_TREE = {|
   type: 'SELECT_PREVIOUS_ELEMENT_IN_TREE',
 |};
+type ACTION_SELECT_PREVIOUS_ELEMENT_WITH_ERROR_OR_WARNING_IN_TREE = {|
+  type: 'SELECT_PREVIOUS_ELEMENT_WITH_ERROR_OR_WARNING_IN_TREE',
+|};
 type ACTION_SELECT_PREVIOUS_SIBLING_IN_TREE = {|
   type: 'SELECT_PREVIOUS_SIBLING_IN_TREE',
 |};
@@ -132,10 +138,12 @@ type Action =
   | ACTION_SELECT_ELEMENT_AT_INDEX
   | ACTION_SELECT_ELEMENT_BY_ID
   | ACTION_SELECT_NEXT_ELEMENT_IN_TREE
+  | ACTION_SELECT_NEXT_ELEMENT_WITH_ERROR_OR_WARNING_IN_TREE
   | ACTION_SELECT_NEXT_SIBLING_IN_TREE
   | ACTION_SELECT_OWNER
   | ACTION_SELECT_PARENT_ELEMENT_IN_TREE
   | ACTION_SELECT_PREVIOUS_ELEMENT_IN_TREE
+  | ACTION_SELECT_PREVIOUS_ELEMENT_WITH_ERROR_OR_WARNING_IN_TREE
   | ACTION_SELECT_PREVIOUS_SIBLING_IN_TREE
   | ACTION_SELECT_OWNER_LIST_NEXT_ELEMENT_IN_TREE
   | ACTION_SELECT_OWNER_LIST_PREVIOUS_ELEMENT_IN_TREE
@@ -372,6 +380,83 @@ function reduceTreeState(store: Store, state: State, action: Action): State {
           }
         }
         break;
+      case 'SELECT_PREVIOUS_ELEMENT_WITH_ERROR_OR_WARNING_IN_TREE': {
+        if (store.errorCount === 0 && store.warningCount === 0) {
+          return state;
+        }
+
+        const elementIndicesWithErrorsOrWarnings = store.getElementsWithErrorsAndWarnings();
+
+        let flatIndex = 0;
+        if (selectedElementIndex !== null) {
+          // Resume from the current position in the list.
+          // Otherwise step to the previous item, relative to the current selection.
+          for (
+            let i = elementIndicesWithErrorsOrWarnings.length - 1;
+            i >= 0;
+            i--
+          ) {
+            const {index} = elementIndicesWithErrorsOrWarnings[i];
+            if (index >= selectedElementIndex) {
+              flatIndex = i;
+            } else {
+              break;
+            }
+          }
+        }
+
+        let prevEntry;
+        if (flatIndex === 0) {
+          prevEntry =
+            elementIndicesWithErrorsOrWarnings[
+              elementIndicesWithErrorsOrWarnings.length - 1
+            ];
+          selectedElementID = prevEntry.id;
+          selectedElementIndex = prevEntry.index;
+        } else {
+          prevEntry = elementIndicesWithErrorsOrWarnings[flatIndex - 1];
+          selectedElementID = prevEntry.id;
+          selectedElementIndex = prevEntry.index;
+        }
+
+        lookupIDForIndex = false;
+        break;
+      }
+      case 'SELECT_NEXT_ELEMENT_WITH_ERROR_OR_WARNING_IN_TREE': {
+        if (store.errorCount === 0 && store.warningCount === 0) {
+          return state;
+        }
+
+        const elementIndicesWithErrorsOrWarnings = store.getElementsWithErrorsAndWarnings();
+
+        let flatIndex = -1;
+        if (selectedElementIndex !== null) {
+          // Resume from the current position in the list.
+          // Otherwise step to the next item, relative to the current selection.
+          for (let i = 0; i < elementIndicesWithErrorsOrWarnings.length; i++) {
+            const {index} = elementIndicesWithErrorsOrWarnings[i];
+            if (index <= selectedElementIndex) {
+              flatIndex = i;
+            } else {
+              break;
+            }
+          }
+        }
+
+        let nextEntry;
+        if (flatIndex >= elementIndicesWithErrorsOrWarnings.length - 1) {
+          nextEntry = elementIndicesWithErrorsOrWarnings[0];
+          selectedElementID = nextEntry.id;
+          selectedElementIndex = nextEntry.index;
+        } else {
+          nextEntry = elementIndicesWithErrorsOrWarnings[flatIndex + 1];
+          selectedElementID = nextEntry.id;
+          selectedElementIndex = nextEntry.index;
+        }
+
+        lookupIDForIndex = false;
+        break;
+      }
       default:
         // React can bailout of no-op updates.
         return state;
@@ -776,11 +861,13 @@ function TreeContextController({
         case 'SELECT_ELEMENT_BY_ID':
         case 'SELECT_CHILD_ELEMENT_IN_TREE':
         case 'SELECT_NEXT_ELEMENT_IN_TREE':
+        case 'SELECT_NEXT_ELEMENT_WITH_ERROR_OR_WARNING_IN_TREE':
         case 'SELECT_NEXT_SIBLING_IN_TREE':
         case 'SELECT_OWNER_LIST_NEXT_ELEMENT_IN_TREE':
         case 'SELECT_OWNER_LIST_PREVIOUS_ELEMENT_IN_TREE':
         case 'SELECT_PARENT_ELEMENT_IN_TREE':
         case 'SELECT_PREVIOUS_ELEMENT_IN_TREE':
+        case 'SELECT_PREVIOUS_ELEMENT_WITH_ERROR_OR_WARNING_IN_TREE':
         case 'SELECT_PREVIOUS_SIBLING_IN_TREE':
         case 'SELECT_OWNER':
         case 'UPDATE_INSPECTED_ELEMENT_ID':
diff --git a/packages/react-devtools-shared/src/devtools/views/Components/types.js b/packages/react-devtools-shared/src/devtools/views/Components/types.js
index cccaba1c9b151..efdb894636885 100644
--- a/packages/react-devtools-shared/src/devtools/views/Components/types.js
+++ b/packages/react-devtools-shared/src/devtools/views/Components/types.js
@@ -84,6 +84,8 @@ export type InspectedElement = {|
   props: Object | null,
   state: Object | null,
   key: number | string | null,
+  errors: Array<[string, number]>,
+  warnings: Array<[string, number]>,
 
   // List of owners
   owners: Array<Owner> | null,
diff --git a/packages/react-devtools-shared/src/devtools/views/Icon.js b/packages/react-devtools-shared/src/devtools/views/Icon.js
index db54b7091e425..ce02611127f31 100644
--- a/packages/react-devtools-shared/src/devtools/views/Icon.js
+++ b/packages/react-devtools-shared/src/devtools/views/Icon.js
@@ -16,13 +16,15 @@ export type IconType =
   | 'code'
   | 'components'
   | 'copy'
+  | 'error'
   | 'flame-chart'
   | 'interactions'
   | 'profiler'
   | 'ranked-chart'
   | 'search'
   | 'settings'
-  | 'store-as-global-variable';
+  | 'store-as-global-variable'
+  | 'warning';
 
 type Props = {|
   className?: string,
@@ -47,6 +49,9 @@ export default function Icon({className = '', type}: Props) {
     case 'copy':
       pathData = PATH_COPY;
       break;
+    case 'error':
+      pathData = PATH_ERROR;
+      break;
     case 'flame-chart':
       pathData = PATH_FLAME_CHART;
       break;
@@ -68,6 +73,9 @@ export default function Icon({className = '', type}: Props) {
     case 'store-as-global-variable':
       pathData = PATH_STORE_AS_GLOBAL_VARIABLE;
       break;
+    case 'warning':
+      pathData = PATH_WARNING;
+      break;
     default:
       console.warn(`Unsupported type "${type}" specified for Icon`);
       break;
@@ -107,6 +115,8 @@ const PATH_COPY = `
   2v10a2 2 0 0 0 2 2h10c1.1 0 2-.9 2-2V5c0-1.1-.9-2-2-2zm0 12H9V5h10v10zm-8 6h2v-2h-2v2zm-4 0h2v-2H7v2z
 `;
 
+const PATH_ERROR = `M16.971 0h-9.942l-7.029 7.029v9.941l7.029 7.03h9.941l7.03-7.029v-9.942l-7.029-7.029zm-1.402 16.945l-3.554-3.521-3.518 3.568-1.418-1.418 3.507-3.566-3.586-3.472 1.418-1.417 3.581 3.458 3.539-3.583 1.431 1.431-3.535 3.568 3.566 3.522-1.431 1.43z`;
+
 const PATH_FLAME_CHART = `
   M10.0650893,21.5040462 C7.14020814,20.6850349 5,18.0558698 5,14.9390244 C5,14.017627
   5,9.81707317 7.83333333,7.37804878 C7.83333333,7.37804878 7.58333333,11.199187 10,
@@ -154,3 +164,5 @@ const PATH_STORE_AS_GLOBAL_VARIABLE = `
   1.79 2.97 3 5.19 3s4.15-1.21 5.19-3H20v-2h-2.09c.05-.33.09-.66.09-1v-1h2v-2h-2v-1c0-.34-.04-.67-.09-1H20V8zm-6
   8h-4v-2h4v2zm0-4h-4v-2h4v2z
 `;
+
+const PATH_WARNING = `M12 1l-12 22h24l-12-22zm-1 8h2v7h-2v-7zm1 11.25c-.69 0-1.25-.56-1.25-1.25s.56-1.25 1.25-1.25 1.25.56 1.25 1.25-.56 1.25-1.25 1.25z`;
diff --git a/packages/react-devtools-shared/src/devtools/views/Profiler/CommitTreeBuilder.js b/packages/react-devtools-shared/src/devtools/views/Profiler/CommitTreeBuilder.js
index 55779509bfd17..85b5c875e4739 100644
--- a/packages/react-devtools-shared/src/devtools/views/Profiler/CommitTreeBuilder.js
+++ b/packages/react-devtools-shared/src/devtools/views/Profiler/CommitTreeBuilder.js
@@ -11,8 +11,10 @@ import {
   __DEBUG__,
   TREE_OPERATION_ADD,
   TREE_OPERATION_REMOVE,
+  TREE_OPERATION_REMOVE_ROOT,
   TREE_OPERATION_REORDER_CHILDREN,
   TREE_OPERATION_UPDATE_TREE_BASE_DURATION,
+  TREE_OPERATION_UPDATE_ERRORS_OR_WARNINGS,
 } from 'react-devtools-shared/src/constants';
 import {utfDecodeString} from 'react-devtools-shared/src/utils';
 import {ElementTypeRoot} from 'react-devtools-shared/src/types';
@@ -294,6 +296,9 @@ function updateTree(
         }
         break;
       }
+      case TREE_OPERATION_REMOVE_ROOT: {
+        throw Error('Operation REMOVE_ROOT is not supported while profiling.');
+      }
       case TREE_OPERATION_REORDER_CHILDREN: {
         id = ((operations[i + 1]: any): number);
         const numChildren = ((operations[i + 2]: any): number);
@@ -329,6 +334,21 @@ function updateTree(
         i += 3;
         break;
       }
+      case TREE_OPERATION_UPDATE_ERRORS_OR_WARNINGS:
+        id = operations[i + 1];
+        const numErrors = operations[i + 2];
+        const numWarnings = operations[i + 3];
+
+        i += 4;
+
+        if (__DEBUG__) {
+          debug(
+            'Warnings and Errors update',
+            `fiber ${id} has ${numErrors} errors and ${numWarnings} warnings`,
+          );
+        }
+        break;
+
       default:
         throw Error(`Unsupported Bridge operation ${operation}`);
     }
diff --git a/packages/react-devtools-shared/src/devtools/views/Settings/DebuggingSettings.js b/packages/react-devtools-shared/src/devtools/views/Settings/DebuggingSettings.js
index bc26ead4fc150..8d5da190be457 100644
--- a/packages/react-devtools-shared/src/devtools/views/Settings/DebuggingSettings.js
+++ b/packages/react-devtools-shared/src/devtools/views/Settings/DebuggingSettings.js
@@ -19,6 +19,8 @@ export default function DebuggingSettings(_: {||}) {
     breakOnConsoleErrors,
     setAppendComponentStack,
     setBreakOnConsoleErrors,
+    setShowInlineWarningsAndErrors,
+    showInlineWarningsAndErrors,
   } = useContext(SettingsContext);
 
   return (
@@ -36,6 +38,19 @@ export default function DebuggingSettings(_: {||}) {
         </label>
       </div>
 
+      <div className={styles.Setting}>
+        <label>
+          <input
+            type="checkbox"
+            checked={showInlineWarningsAndErrors}
+            onChange={({currentTarget}) =>
+              setShowInlineWarningsAndErrors(currentTarget.checked)
+            }
+          />{' '}
+          Show inline warnings and errors.
+        </label>
+      </div>
+
       <div className={styles.Setting}>
         <label>
           <input
@@ -48,6 +63,10 @@ export default function DebuggingSettings(_: {||}) {
           Break on warnings
         </label>
       </div>
+
+      <div className={styles.ConsoleAPIWarning}>
+        These settings require DevTools to override native console APIs.
+      </div>
     </div>
   );
 }
diff --git a/packages/react-devtools-shared/src/devtools/views/Settings/SettingsContext.js b/packages/react-devtools-shared/src/devtools/views/Settings/SettingsContext.js
index 2a89a9fe679b7..f63a901410fb6 100644
--- a/packages/react-devtools-shared/src/devtools/views/Settings/SettingsContext.js
+++ b/packages/react-devtools-shared/src/devtools/views/Settings/SettingsContext.js
@@ -21,6 +21,7 @@ import {
   LOCAL_STORAGE_SHOULD_BREAK_ON_CONSOLE_ERRORS,
   LOCAL_STORAGE_SHOULD_PATCH_CONSOLE_KEY,
   LOCAL_STORAGE_TRACE_UPDATES_ENABLED_KEY,
+  LOCAL_STORAGE_SHOW_INLINE_WARNINGS_AND_ERRORS_KEY,
 } from 'react-devtools-shared/src/constants';
 import {useLocalStorage} from '../hooks';
 import {BridgeContext} from '../context';
@@ -44,6 +45,9 @@ type Context = {|
   breakOnConsoleErrors: boolean,
   setBreakOnConsoleErrors: (value: boolean) => void,
 
+  showInlineWarningsAndErrors: boolean,
+  setShowInlineWarningsAndErrors: (value: boolean) => void,
+
   theme: Theme,
   setTheme(value: Theme): void,
 
@@ -90,6 +94,13 @@ function SettingsContextController({
     LOCAL_STORAGE_SHOULD_BREAK_ON_CONSOLE_ERRORS,
     false,
   );
+  const [
+    showInlineWarningsAndErrors,
+    setShowInlineWarningsAndErrors,
+  ] = useLocalStorage<boolean>(
+    LOCAL_STORAGE_SHOW_INLINE_WARNINGS_AND_ERRORS_KEY,
+    true,
+  );
   const [
     traceUpdatesEnabled,
     setTraceUpdatesEnabled,
@@ -147,8 +158,14 @@ function SettingsContextController({
     bridge.send('updateConsolePatchSettings', {
       appendComponentStack,
       breakOnConsoleErrors,
+      showInlineWarningsAndErrors,
     });
-  }, [bridge, appendComponentStack, breakOnConsoleErrors]);
+  }, [
+    bridge,
+    appendComponentStack,
+    breakOnConsoleErrors,
+    showInlineWarningsAndErrors,
+  ]);
 
   useEffect(() => {
     bridge.send('setTraceUpdatesEnabled', traceUpdatesEnabled);
@@ -168,6 +185,8 @@ function SettingsContextController({
       setDisplayDensity,
       setTheme,
       setTraceUpdatesEnabled,
+      setShowInlineWarningsAndErrors,
+      showInlineWarningsAndErrors,
       theme,
       traceUpdatesEnabled,
     }),
@@ -180,6 +199,8 @@ function SettingsContextController({
       setDisplayDensity,
       setTheme,
       setTraceUpdatesEnabled,
+      setShowInlineWarningsAndErrors,
+      showInlineWarningsAndErrors,
       theme,
       traceUpdatesEnabled,
     ],
@@ -324,6 +345,25 @@ export function updateThemeVariables(
     'color-component-badge-count-inverted',
     documentElements,
   );
+  updateStyleHelper(theme, 'color-console-error-badge-text', documentElements);
+  updateStyleHelper(theme, 'color-console-error-background', documentElements);
+  updateStyleHelper(theme, 'color-console-error-border', documentElements);
+  updateStyleHelper(theme, 'color-console-error-icon', documentElements);
+  updateStyleHelper(theme, 'color-console-error-text', documentElements);
+  updateStyleHelper(
+    theme,
+    'color-console-warning-badge-text',
+    documentElements,
+  );
+  updateStyleHelper(
+    theme,
+    'color-console-warning-background',
+    documentElements,
+  );
+  updateStyleHelper(theme, 'color-console-warning-border', documentElements);
+  updateStyleHelper(theme, 'color-console-warning-icon', documentElements);
+  updateStyleHelper(theme, 'color-console-warning-text', documentElements);
+  updateStyleHelper(theme, 'color-context-border', documentElements);
   updateStyleHelper(theme, 'color-context-background', documentElements);
   updateStyleHelper(theme, 'color-context-background-hover', documentElements);
   updateStyleHelper(
diff --git a/packages/react-devtools-shared/src/devtools/views/Settings/SettingsModal.css b/packages/react-devtools-shared/src/devtools/views/Settings/SettingsModal.css
index af400ba9161d0..7c1e6b55b4494 100644
--- a/packages/react-devtools-shared/src/devtools/views/Settings/SettingsModal.css
+++ b/packages/react-devtools-shared/src/devtools/views/Settings/SettingsModal.css
@@ -42,4 +42,4 @@
   padding: 0.5rem;
   flex: 0 1 auto;
   overflow: auto;
-}
+}
\ No newline at end of file
diff --git a/packages/react-devtools-shared/src/devtools/views/Settings/SettingsShared.css b/packages/react-devtools-shared/src/devtools/views/Settings/SettingsShared.css
index bcdc427668df3..8bf8b3de1fddf 100644
--- a/packages/react-devtools-shared/src/devtools/views/Settings/SettingsShared.css
+++ b/packages/react-devtools-shared/src/devtools/views/Settings/SettingsShared.css
@@ -128,7 +128,8 @@
   background-color: var(--color-toggle-text);
 }
 
-.ReleaseNotes {
+.ReleaseNotes,
+.ConsoleAPIWarning {
   width: 100%;
   background-color: var(--color-background-hover);
   padding: 0.25rem 0.5rem;
diff --git a/packages/react-devtools-shared/src/devtools/views/root.css b/packages/react-devtools-shared/src/devtools/views/root.css
index b49213ccdffee..1fc6d461a4da1 100644
--- a/packages/react-devtools-shared/src/devtools/views/root.css
+++ b/packages/react-devtools-shared/src/devtools/views/root.css
@@ -44,6 +44,16 @@
   --light-color-component-badge-background-inverted: rgba(255, 255, 255, 0.25);
   --light-color-component-badge-count: #777d88;
   --light-color-component-badge-count-inverted: rgba(255, 255, 255, 0.7);
+  --light-color-console-error-badge-text: #ffffff;
+  --light-color-console-error-background: #fff0f0;
+  --light-color-console-error-border: #ffd6d6;
+  --light-color-console-error-icon: #eb3941;
+  --light-color-console-error-text: #fe2e31;
+  --light-color-console-warning-badge-text: #000000;
+  --light-color-console-warning-background: #fffbe5;
+  --light-color-console-warning-border: #fff5c1;
+  --light-color-console-warning-icon: #f4bd00;
+  --light-color-console-warning-text: #64460c;
   --light-color-context-background: rgba(0,0,0,.9);
   --light-color-context-background-hover: rgba(255, 255, 255, 0.1);
   --light-color-context-background-selected: #178fb9;
@@ -121,6 +131,16 @@
   --dark-color-component-badge-background-inverted: rgba(0, 0, 0, 0.25);
   --dark-color-component-badge-count: #8f949d;
   --dark-color-component-badge-count-inverted: rgba(255, 255, 255, 0.7);
+  --dark-color-console-error-badge-text: #000000;
+  --dark-color-console-error-background: #290000;
+  --dark-color-console-error-border: #5c0000;
+  --dark-color-console-error-icon: #eb3941;
+  --dark-color-console-error-text: #fc7f7f;
+  --dark-color-console-warning-badge-text: #000000;
+  --dark-color-console-warning-background: #332b00;
+  --dark-color-console-warning-border: #665500;
+  --dark-color-console-warning-icon: #f4bd00;
+  --dark-color-console-warning-text: #f5f2ed;
   --dark-color-context-background: rgba(255,255,255,.9);
   --dark-color-context-background-hover: rgba(0, 136, 250, 0.1);
   --dark-color-context-background-selected: #0088fa;
diff --git a/packages/react-devtools-shared/src/hook.js b/packages/react-devtools-shared/src/hook.js
index 62234fdc46425..9bec6f40a1537 100644
--- a/packages/react-devtools-shared/src/hook.js
+++ b/packages/react-devtools-shared/src/hook.js
@@ -182,6 +182,8 @@ export function installHook(target: any): DevToolsHook | null {
           window.__REACT_DEVTOOLS_APPEND_COMPONENT_STACK__ !== false;
         const breakOnConsoleErrors =
           window.__REACT_DEVTOOLS_BREAK_ON_CONSOLE_ERRORS__ === true;
+        const showInlineWarningsAndErrors =
+          window.__REACT_DEVTOOLS_SHOW_INLINE_WARNINGS_AND_ERRORS__ === true;
 
         // The installHook() function is injected by being stringified in the browser,
         // so imports outside of this function do not get included.
@@ -195,6 +197,7 @@ export function installHook(target: any): DevToolsHook | null {
           patchConsole({
             appendComponentStack,
             breakOnConsoleErrors,
+            showInlineWarningsAndErrors,
           });
         }
       } catch (error) {}
diff --git a/packages/react-devtools-shared/src/utils.js b/packages/react-devtools-shared/src/utils.js
index 2c796c2228b8d..d84d5e67e98b6 100644
--- a/packages/react-devtools-shared/src/utils.js
+++ b/packages/react-devtools-shared/src/utils.js
@@ -26,7 +26,9 @@ import {REACT_SUSPENSE_LIST_TYPE as SuspenseList} from 'shared/ReactSymbols';
 import {
   TREE_OPERATION_ADD,
   TREE_OPERATION_REMOVE,
+  TREE_OPERATION_REMOVE_ROOT,
   TREE_OPERATION_REORDER_CHILDREN,
+  TREE_OPERATION_UPDATE_ERRORS_OR_WARNINGS,
   TREE_OPERATION_UPDATE_TREE_BASE_DURATION,
 } from './constants';
 import {ElementTypeRoot} from 'react-devtools-shared/src/types';
@@ -34,6 +36,7 @@ import {
   LOCAL_STORAGE_FILTER_PREFERENCES_KEY,
   LOCAL_STORAGE_SHOULD_BREAK_ON_CONSOLE_ERRORS,
   LOCAL_STORAGE_SHOULD_PATCH_CONSOLE_KEY,
+  LOCAL_STORAGE_SHOW_INLINE_WARNINGS_AND_ERRORS_KEY,
 } from './constants';
 import {ComponentFilterElementType, ElementTypeHostComponent} from './types';
 import {
@@ -204,6 +207,12 @@ export function printOperationsArray(operations: Array<number>) {
         }
         break;
       }
+      case TREE_OPERATION_REMOVE_ROOT: {
+        i += 1;
+
+        logs.push(`Remove root ${rootID}`);
+        break;
+      }
       case TREE_OPERATION_REORDER_CHILDREN: {
         const id = ((operations[i + 1]: any): number);
         const numChildren = ((operations[i + 2]: any): number);
@@ -220,6 +229,17 @@ export function printOperationsArray(operations: Array<number>) {
         // The profiler UI uses them lazily in order to generate the tree.
         i += 3;
         break;
+      case TREE_OPERATION_UPDATE_ERRORS_OR_WARNINGS:
+        const id = operations[i + 1];
+        const numErrors = operations[i + 2];
+        const numWarnings = operations[i + 3];
+
+        i += 4;
+
+        logs.push(
+          `Node ${id} has ${numErrors} errors and ${numWarnings} warnings`,
+        );
+        break;
       default:
         throw Error(`Unsupported Bridge operation ${operation}`);
     }
@@ -293,6 +313,25 @@ export function setBreakOnConsoleErrors(value: boolean): void {
   );
 }
 
+export function getShowInlineWarningsAndErrors(): boolean {
+  try {
+    const raw = localStorageGetItem(
+      LOCAL_STORAGE_SHOW_INLINE_WARNINGS_AND_ERRORS_KEY,
+    );
+    if (raw != null) {
+      return JSON.parse(raw);
+    }
+  } catch (error) {}
+  return true;
+}
+
+export function setShowInlineWarningsAndErrors(value: boolean): void {
+  localStorageSetItem(
+    LOCAL_STORAGE_SHOW_INLINE_WARNINGS_AND_ERRORS_KEY,
+    JSON.stringify(value),
+  );
+}
+
 export function separateDisplayNameAndHOCs(
   displayName: string | null,
   type: ElementType,
diff --git a/packages/react-devtools-shell/src/app/InlineWarnings/index.js b/packages/react-devtools-shell/src/app/InlineWarnings/index.js
new file mode 100644
index 0000000000000..9aa681b29381d
--- /dev/null
+++ b/packages/react-devtools-shell/src/app/InlineWarnings/index.js
@@ -0,0 +1,181 @@
+/** @flow */
+
+import * as React from 'react';
+import {Fragment, useEffect, useRef, useState} from 'react';
+
+function WarnDuringRender({children = null}) {
+  console.warn('This warning fires during every render');
+  return children;
+}
+
+function WarnOnMount({children = null}) {
+  useEffect(() => {
+    console.warn('This warning fires on initial mount only');
+  }, []);
+  return children;
+}
+
+function WarnOnUpdate({children = null}) {
+  const didMountRef = useRef(false);
+  useEffect(() => {
+    if (didMountRef.current) {
+      console.warn('This warning fires on every update');
+    } else {
+      didMountRef.current = true;
+    }
+  });
+  return children;
+}
+
+function WarnOnUnmount({children = null}) {
+  useEffect(() => {
+    return () => {
+      console.warn('This warning fires on unmount');
+    };
+  }, []);
+  return children;
+}
+
+function ErrorDuringRender({children = null}) {
+  console.error('This error fires during every render');
+  return children;
+}
+
+function ErrorOnMount({children = null}) {
+  useEffect(() => {
+    console.error('This error fires on initial mount only');
+  }, []);
+  return children;
+}
+
+function ErrorOnUpdate({children = null}) {
+  const didMountRef = useRef(false);
+  useEffect(() => {
+    if (didMountRef.current) {
+      console.error('This error fires on every update');
+    } else {
+      didMountRef.current = true;
+    }
+  });
+  return children;
+}
+
+function ErrorOnUnmount({children = null}) {
+  useEffect(() => {
+    return () => {
+      console.error('This error fires on unmount');
+    };
+  }, []);
+  return children;
+}
+
+function ErrorAndWarningDuringRender({children = null}) {
+  console.warn('This warning fires during every render');
+  console.error('This error fires during every render');
+  return children;
+}
+
+function ErrorAndWarningOnMount({children = null}) {
+  useEffect(() => {
+    console.warn('This warning fires on initial mount only');
+    console.error('This error fires on initial mount only');
+  }, []);
+  return children;
+}
+
+function ErrorAndWarningOnUpdate({children = null}) {
+  const didMountRef = useRef(false);
+  useEffect(() => {
+    if (didMountRef.current) {
+      console.warn('This warning fires on every update');
+      console.error('This error fires on every update');
+    } else {
+      didMountRef.current = true;
+    }
+  });
+  return children;
+}
+
+function ErrorAndWarningOnUnmount({children = null}) {
+  useEffect(() => {
+    return () => {
+      console.warn('This warning fires on unmount');
+      console.error('This error fires on unmount');
+    };
+  }, []);
+  return children;
+}
+
+function ReallyLongErrorMessageThatWillCauseTextToBeTruncated({
+  children = null,
+}) {
+  console.error(
+    'This error is a really long error message that should cause the text to be truncated in DevTools',
+  );
+  return children;
+}
+
+function ErrorWithMultipleArgs({children = null}) {
+  console.error('This error', 'passes console', 4, 'arguments');
+  return children;
+}
+
+function ErrorWithStringSubstitutions({children = null}) {
+  console.error('This error uses "%s" substitutions', 'string');
+  return children;
+}
+
+function ReactErrorOnHostComponent({children = null}) {
+  return <div data-camelCasedAttribute="should-lower-case">{children}</div>;
+}
+
+function DuplicateWarningsAndErrors({children = null}) {
+  console.warn('this warning is logged twice per render');
+  console.warn('this warning is logged twice per render');
+  console.error('this error is logged twice per render');
+  console.error('this error is logged twice per render');
+  return <div data-camelCasedAttribute="should-lower-case">{children}</div>;
+}
+
+function MultipleWarningsAndErrors({children = null}) {
+  console.warn('this is the first warning logged');
+  console.warn('this is the second warning logged');
+  console.error('this is the first error logged');
+  console.error('this is the second error logged');
+  return <div data-camelCasedAttribute="should-lower-case">{children}</div>;
+}
+
+function ComponentWithMissingKey({children}) {
+  return [<div />];
+}
+
+export default function ErrorsAndWarnings() {
+  const [count, setCount] = useState(0);
+  const handleClick = () => setCount(count + 1);
+  return (
+    <Fragment>
+      <h1>Inline warnings</h1>
+      <button onClick={handleClick}>Update {count > 0 ? count : ''}</button>
+      <ComponentWithMissingKey />
+      <WarnDuringRender />
+      <WarnOnMount />
+      <WarnOnUpdate />
+      {count === 0 ? <WarnOnUnmount /> : null}
+      {count === 0 ? <WarnOnMount /> : null}
+      <ErrorDuringRender />
+      <ErrorOnMount />
+      <ErrorOnUpdate />
+      {count === 0 ? <ErrorOnUnmount /> : null}
+      <ErrorAndWarningDuringRender />
+      <ErrorAndWarningOnMount />
+      <ErrorAndWarningOnUpdate />
+      {count === 0 ? <ErrorAndWarningOnUnmount /> : null}
+      <ErrorWithMultipleArgs />
+      <ErrorWithStringSubstitutions />
+      <ReactErrorOnHostComponent />
+      <ReallyLongErrorMessageThatWillCauseTextToBeTruncated />
+      <DuplicateWarningsAndErrors />
+      <MultipleWarningsAndErrors />
+    </Fragment>
+  );
+}
diff --git a/packages/react-devtools-shell/src/app/index.js b/packages/react-devtools-shell/src/app/index.js
index 8483be6b050a9..b3c7b9be228c1 100644
--- a/packages/react-devtools-shell/src/app/index.js
+++ b/packages/react-devtools-shell/src/app/index.js
@@ -12,6 +12,7 @@ import Iframe from './Iframe';
 import EditableProps from './EditableProps';
 import ElementTypes from './ElementTypes';
 import Hydration from './Hydration';
+import InlineWarnings from './InlineWarnings';
 import InspectableElements from './InspectableElements';
 import InteractionTracing from './InteractionTracing';
 import PriorityLevels from './PriorityLevels';
@@ -53,6 +54,7 @@ function mountTestApp() {
   mountHelper(Hydration);
   mountHelper(ElementTypes);
   mountHelper(EditableProps);
+  mountHelper(InlineWarnings);
   mountHelper(PriorityLevels);
   mountHelper(ReactNativeWeb);
   mountHelper(Toggle);
diff --git a/packages/react-devtools/OVERVIEW.md b/packages/react-devtools/OVERVIEW.md
index a59a9fcc3bfdc..ee73da31c64ab 100644
--- a/packages/react-devtools/OVERVIEW.md
+++ b/packages/react-devtools/OVERVIEW.md
@@ -141,12 +141,41 @@ While profiling is in progress, we send an extra operation any time a fiber is a
 For example, updating the base duration for a fiber with an id of 1:
 ```js
 [
+  4,  // update tree base duration operation
   4,  // tree base duration operation
   1,  // fiber id
   32, // new tree base duration value
 ]
 ```
 
+#### Updating errors and warnings on a Fiber
+
+We record calls to `console.warn` and `console.error` in the backend.
+Periodically we notify the frontend that the number of recorded calls got updated.
+We only send the serialized messages as part of the `inspectElement` event.
+
+
+```js
+[
+  5, // update error/warning counts operation
+  4, // fiber id
+  0, // number of calls to console.error from that fiber
+  3, // number of calls to console.warn from that fiber
+]
+```
+
+#### Removing a root
+
+Special case of unmounting an entire root (include its decsendants). This specialized message replaces what would otherwise be a series of remove-node operations. It is currently only used in one case: updating component filters. The primary motivation for this is actually to preserve fiber ids for components that are re-added to the tree after the updated filters have been applied. This preserves mappings between the Fiber (id) and things like error and warning logs.
+
+```js
+[
+  6, // remove root operation
+]
+```
+
+This operation has no additional payload because renderer and root ids are already sent at the beginning of every operations payload.
+
 ## Reconstructing the tree
 
 The frontend stores its information about the tree in a map of id to objects with the following keys:
@@ -268,4 +297,4 @@ Once profiling is finished, the frontend requests profiling data from the backen
 
 ### Importing/exporting data
 
-Because all of the data is merged in the frontend after a profiling session is completed, it can be exported and imported (as a single JSON object), enabling profiling sessions to be shared between users.
\ No newline at end of file
+Because all of the data is merged in the frontend after a profiling session is completed, it can be exported and imported (as a single JSON object), enabling profiling sessions to be shared between users.
diff --git a/scripts/jest/config.build-devtools.js b/scripts/jest/config.build-devtools.js
index 7f1cd8855929b..39985e7416e18 100644
--- a/scripts/jest/config.build-devtools.js
+++ b/scripts/jest/config.build-devtools.js
@@ -59,6 +59,9 @@ module.exports = Object.assign({}, baseConfig, {
     require.resolve(
       '../../packages/react-devtools-shared/src/__tests__/storeSerializer.js'
     ),
+    require.resolve(
+      '../../packages/react-devtools-shared/src/__tests__/treeContextStateSerializer.js'
+    ),
   ],
   setupFiles: [
     ...baseConfig.setupFiles,