Backed out changeset eee8f9b6791d (bug 1336383) for failing browser_net_har_post_data.js. r=backout
authorSebastian Hengst <archaeopteryx@coole-files.de>
Fri, 10 Feb 2017 17:52:46 +0100
changeset 342258 55916d685e88b8915fdda658c8dc8f0fadb0bfb4
parent 342257 20a02827856fbddd59b1cca5b471433f3f8fee62
child 342259 6a97271a798eaba95033a783f41e8f16938025dd
push id31346
push userkwierso@gmail.com
push dateFri, 10 Feb 2017 22:33:24 +0000
treeherdermozilla-central@7b9d9e4a82a6 [default view] [failures only]
perfherder[talos] [build metrics] [platform microbench] (compared to previous push)
reviewersbackout
bugs1336383
milestone54.0a1
backs outeee8f9b6791deff3db5fb7a3423ed04fdc4e6725
first release with
nightly linux32
nightly linux64
nightly mac
nightly win32
nightly win64
last release without
nightly linux32
nightly linux64
nightly mac
nightly win32
nightly win64
Backed out changeset eee8f9b6791d (bug 1336383) for failing browser_net_har_post_data.js. r=backout
devtools/client/framework/test/browser_ignore_toolbox_network_requests.js
devtools/client/netmonitor/actions/selection.js
devtools/client/netmonitor/actions/ui.js
devtools/client/netmonitor/components/moz.build
devtools/client/netmonitor/components/request-list-content.js
devtools/client/netmonitor/components/request-list-empty.js
devtools/client/netmonitor/components/request-list-item.js
devtools/client/netmonitor/components/request-list-tooltip.js
devtools/client/netmonitor/components/request-list.js
devtools/client/netmonitor/components/toolbar.js
devtools/client/netmonitor/har/har-builder.js
devtools/client/netmonitor/har/har-exporter.js
devtools/client/netmonitor/har/test/browser_net_har_copy_all_as_har.js
devtools/client/netmonitor/moz.build
devtools/client/netmonitor/netmonitor-controller.js
devtools/client/netmonitor/netmonitor-view.js
devtools/client/netmonitor/netmonitor.xul
devtools/client/netmonitor/request-list-context-menu.js
devtools/client/netmonitor/request-list-tooltip.js
devtools/client/netmonitor/requests-menu-view.js
devtools/client/netmonitor/test/browser.ini
devtools/client/netmonitor/test/browser_net_accessibility-01.js
devtools/client/netmonitor/test/browser_net_accessibility-02.js
devtools/client/netmonitor/test/browser_net_api-calls.js
devtools/client/netmonitor/test/browser_net_autoscroll.js
devtools/client/netmonitor/test/browser_net_brotli.js
devtools/client/netmonitor/test/browser_net_cached-status.js
devtools/client/netmonitor/test/browser_net_cause.js
devtools/client/netmonitor/test/browser_net_cause_redirect.js
devtools/client/netmonitor/test/browser_net_clear.js
devtools/client/netmonitor/test/browser_net_complex-params.js
devtools/client/netmonitor/test/browser_net_content-type.js
devtools/client/netmonitor/test/browser_net_copy_as_curl.js
devtools/client/netmonitor/test/browser_net_copy_headers.js
devtools/client/netmonitor/test/browser_net_copy_image_as_data_uri.js
devtools/client/netmonitor/test/browser_net_copy_params.js
devtools/client/netmonitor/test/browser_net_copy_response.js
devtools/client/netmonitor/test/browser_net_copy_svg_image_as_data_uri.js
devtools/client/netmonitor/test/browser_net_copy_url.js
devtools/client/netmonitor/test/browser_net_cors_requests.js
devtools/client/netmonitor/test/browser_net_curl-utils.js
devtools/client/netmonitor/test/browser_net_cyrillic-01.js
devtools/client/netmonitor/test/browser_net_cyrillic-02.js
devtools/client/netmonitor/test/browser_net_filter-01.js
devtools/client/netmonitor/test/browser_net_filter-02.js
devtools/client/netmonitor/test/browser_net_filter-03.js
devtools/client/netmonitor/test/browser_net_filter-04.js
devtools/client/netmonitor/test/browser_net_footer-summary.js
devtools/client/netmonitor/test/browser_net_frame.js
devtools/client/netmonitor/test/browser_net_header-docs.js
devtools/client/netmonitor/test/browser_net_html-preview.js
devtools/client/netmonitor/test/browser_net_icon-preview.js
devtools/client/netmonitor/test/browser_net_image-tooltip.js
devtools/client/netmonitor/test/browser_net_json-b64.js
devtools/client/netmonitor/test/browser_net_json-long.js
devtools/client/netmonitor/test/browser_net_json-malformed.js
devtools/client/netmonitor/test/browser_net_json-null.js
devtools/client/netmonitor/test/browser_net_json_custom_mime.js
devtools/client/netmonitor/test/browser_net_json_text_mime.js
devtools/client/netmonitor/test/browser_net_jsonp.js
devtools/client/netmonitor/test/browser_net_large-response.js
devtools/client/netmonitor/test/browser_net_open_request_in_tab.js
devtools/client/netmonitor/test/browser_net_page-nav.js
devtools/client/netmonitor/test/browser_net_pane-collapse.js
devtools/client/netmonitor/test/browser_net_pane-toggle.js
devtools/client/netmonitor/test/browser_net_persistent_logs.js
devtools/client/netmonitor/test/browser_net_post-data-01.js
devtools/client/netmonitor/test/browser_net_post-data-02.js
devtools/client/netmonitor/test/browser_net_post-data-03.js
devtools/client/netmonitor/test/browser_net_post-data-04.js
devtools/client/netmonitor/test/browser_net_prefs-and-l10n.js
devtools/client/netmonitor/test/browser_net_raw_headers.js
devtools/client/netmonitor/test/browser_net_reload-button.js
devtools/client/netmonitor/test/browser_net_reload-markers.js
devtools/client/netmonitor/test/browser_net_req-resp-bodies.js
devtools/client/netmonitor/test/browser_net_resend.js
devtools/client/netmonitor/test/browser_net_resend_cors.js
devtools/client/netmonitor/test/browser_net_resend_headers.js
devtools/client/netmonitor/test/browser_net_security-details.js
devtools/client/netmonitor/test/browser_net_security-error.js
devtools/client/netmonitor/test/browser_net_security-icon-click.js
devtools/client/netmonitor/test/browser_net_security-redirect.js
devtools/client/netmonitor/test/browser_net_security-state.js
devtools/client/netmonitor/test/browser_net_security-tab-deselect.js
devtools/client/netmonitor/test/browser_net_security-tab-visibility.js
devtools/client/netmonitor/test/browser_net_security-warnings.js
devtools/client/netmonitor/test/browser_net_send-beacon-other-tab.js
devtools/client/netmonitor/test/browser_net_send-beacon.js
devtools/client/netmonitor/test/browser_net_service-worker-status.js
devtools/client/netmonitor/test/browser_net_simple-request-data.js
devtools/client/netmonitor/test/browser_net_simple-request-details.js
devtools/client/netmonitor/test/browser_net_simple-request.js
devtools/client/netmonitor/test/browser_net_sort-01.js
devtools/client/netmonitor/test/browser_net_sort-02.js
devtools/client/netmonitor/test/browser_net_sort-03.js
devtools/client/netmonitor/test/browser_net_status-codes.js
devtools/client/netmonitor/test/browser_net_streaming-response.js
devtools/client/netmonitor/test/browser_net_throttle.js
devtools/client/netmonitor/test/browser_net_timing-division.js
devtools/client/netmonitor/test/browser_net_truncate.js
devtools/client/netmonitor/test/head.js
devtools/client/styleeditor/test/browser_styleeditor_fetch-from-cache.js
devtools/client/themes/netmonitor.css
devtools/client/webconsole/test/browser_netmonitor_shows_reqs_in_webconsole.js
devtools/client/webconsole/test/browser_webconsole_netlogging_panel.js
devtools/client/webconsole/test/browser_webconsole_netlogging_reset_filter.js
devtools/client/webconsole/test/browser_webconsole_shows_reqs_in_netmonitor.js
--- a/devtools/client/framework/test/browser_ignore_toolbox_network_requests.js
+++ b/devtools/client/framework/test/browser_ignore_toolbox_network_requests.js
@@ -18,17 +18,16 @@ add_task(function* () {
   let tab = yield addTab(URL_ROOT + "doc_viewsource.html");
   let target = TargetFactory.forTab(tab);
   let toolbox = yield gDevTools.showToolbox(target, "styleeditor");
   let panel = toolbox.getPanel("styleeditor");
 
   is(panel.UI.editors.length, 1, "correct number of editors opened");
 
   let monitor = yield toolbox.selectTool("netmonitor");
-  let { gStore } = monitor.panelWin;
-
-  is(gStore.getState().requests.requests.size, 0, "No network requests appear in the network panel");
+  let { RequestsMenu } = monitor.panelWin.NetMonitorView;
+  is(RequestsMenu.itemCount, 0, "No network requests appear in the network panel");
 
   yield gDevTools.closeToolbox(target);
   tab = target = toolbox = panel = null;
   gBrowser.removeCurrentTab();
   flags.testing = isTesting;
 });
--- a/devtools/client/netmonitor/actions/selection.js
+++ b/devtools/client/netmonitor/actions/selection.js
@@ -1,45 +1,28 @@
 /* This Source Code Form is subject to the terms of the Mozilla Public
  * License, v. 2.0. If a copy of the MPL was not distributed with this
  * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
 
 "use strict";
 
+const { getDisplayedRequests } = require("../selectors/index");
 const { SELECT_REQUEST } = require("../constants");
-const {
-  getDisplayedRequests,
-  getSortedRequests,
-} = require("../selectors/index");
-
-const PAGE_SIZE_ITEM_COUNT_RATIO = 5;
 
 /**
  * Select request with a given id.
  */
 function selectRequest(id) {
   return {
     type: SELECT_REQUEST,
-    id,
+    id
   };
 }
 
-/**
- * Select request with a given index (sorted order)
- */
-function selectRequestByIndex(index) {
-  return (dispatch, getState) => {
-    const requests = getSortedRequests(getState());
-    let itemId;
-    if (index >= 0 && index < requests.size) {
-      itemId = requests.get(index).id;
-    }
-    dispatch(selectRequest(itemId));
-  };
-}
+const PAGE_SIZE_ITEM_COUNT_RATIO = 5;
 
 /**
  * Move the selection up to down according to the "delta" parameter. Possible values:
  * - Number: positive or negative, move up or down by specified distance
  * - "PAGE_UP" | "PAGE_DOWN" (String): page up or page down
  * - +Infinity | -Infinity: move to the start or end of the list
  */
 function selectDelta(delta) {
@@ -62,11 +45,10 @@ function selectDelta(delta) {
     const newIndex = Math.min(Math.max(0, selIndex + delta), requests.size - 1);
     const newItem = requests.get(newIndex);
     dispatch(selectRequest(newItem.id));
   };
 }
 
 module.exports = {
   selectRequest,
-  selectRequestByIndex,
   selectDelta,
 };
--- a/devtools/client/netmonitor/actions/ui.js
+++ b/devtools/client/netmonitor/actions/ui.js
@@ -19,19 +19,19 @@ const {
 function openNetworkDetails(open) {
   return {
     type: OPEN_NETWORK_DETAILS,
     open,
   };
 }
 
 /**
- * Change performance statistics panel open state.
+ * Change performance statistics view open state.
  *
- * @param {boolean} visible - expected performance statistics panel open state
+ * @param {boolean} visible - expected performance statistics open state
  */
 function openStatistics(open) {
   return {
     type: OPEN_STATISTICS,
     open,
   };
 }
 
@@ -61,21 +61,20 @@ function selectDetailsPanelTab(id) {
  * Toggle network details panel.
  */
 function toggleNetworkDetails() {
   return (dispatch, getState) =>
     dispatch(openNetworkDetails(!getState().ui.networkDetailsOpen));
 }
 
 /**
- * Toggle performance statistics panel.
+ * Toggle to show/hide performance statistics view.
  */
 function toggleStatistics() {
-  return (dispatch, getState) =>
-    dispatch(openStatistics(!getState().ui.statisticsOpen));
+  return (dispatch, getState) => dispatch(openStatistics(!getState().ui.statisticsOpen));
 }
 
 module.exports = {
   openNetworkDetails,
   openStatistics,
   resizeWaterfall,
   selectDetailsPanelTab,
   toggleNetworkDetails,
--- a/devtools/client/netmonitor/components/moz.build
+++ b/devtools/client/netmonitor/components/moz.build
@@ -2,12 +2,13 @@
 # License, v. 2.0. If a copy of the MPL was not distributed with this
 # file, You can obtain one at http://mozilla.org/MPL/2.0/.
 
 DevToolsModules(
     'request-list-content.js',
     'request-list-empty.js',
     'request-list-header.js',
     'request-list-item.js',
+    'request-list-tooltip.js',
     'request-list.js',
     'statistics-panel.js',
     'toolbar.js',
 )
--- a/devtools/client/netmonitor/components/request-list-content.js
+++ b/devtools/client/netmonitor/components/request-list-content.js
@@ -1,83 +1,58 @@
 /* This Source Code Form is subject to the terms of the Mozilla Public
  * License, v. 2.0. If a copy of the MPL was not distributed with this
  * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
-
-/* globals NetMonitorController */
+/* globals NetMonitorView */
 
 "use strict";
 
-const { KeyCodes } = require("devtools/client/shared/keycodes");
-const {
-  createClass,
-  createFactory,
-  DOM,
-  PropTypes,
-} = require("devtools/client/shared/vendor/react");
-const { connect } = require("devtools/client/shared/vendor/react-redux");
-const { HTMLTooltip } = require("devtools/client/shared/widgets/tooltip/HTMLTooltip");
 const { Task } = require("devtools/shared/task");
+const { createClass, createFactory, DOM, PropTypes } = require("devtools/client/shared/vendor/react");
+const { div } = DOM;
 const Actions = require("../actions/index");
-const {
-  setTooltipImageContent,
-  setTooltipStackTraceContent,
-} = require("../request-list-tooltip");
-const {
-  getDisplayedRequests,
-  getWaterfallScale,
-} = require("../selectors/index");
-
-// Components
 const RequestListItem = createFactory(require("./request-list-item"));
-const RequestListContextMenu = require("../request-list-context-menu");
-
-const { div } = DOM;
+const { connect } = require("devtools/client/shared/vendor/react-redux");
+const { setTooltipImageContent,
+        setTooltipStackTraceContent } = require("./request-list-tooltip");
+const { getDisplayedRequests,
+        getWaterfallScale } = require("../selectors/index");
+const { KeyCodes } = require("devtools/client/shared/keycodes");
 
 // tooltip show/hide delay in ms
 const REQUESTS_TOOLTIP_TOGGLE_DELAY = 500;
 
 /**
  * Renders the actual contents of the request list.
  */
 const RequestListContent = createClass({
   displayName: "RequestListContent",
 
   propTypes: {
-    contextMenu: PropTypes.object.isRequired,
-    dispatch: PropTypes.func.isRequired,
     displayedRequests: PropTypes.object.isRequired,
     firstRequestStartedMillis: PropTypes.number.isRequired,
+    onItemContextMenu: PropTypes.func.isRequired,
     onItemMouseDown: PropTypes.func.isRequired,
     onSecurityIconClick: PropTypes.func.isRequired,
     onSelectDelta: PropTypes.func.isRequired,
     scale: PropTypes.number,
     selectedRequestId: PropTypes.string,
     tooltip: PropTypes.shape({
       hide: PropTypes.func.isRequired,
       startTogglingOnHover: PropTypes.func.isRequired,
       stopTogglingOnHover: PropTypes.func.isRequired,
     }).isRequired
   },
 
-  componentWillMount() {
-    const { dispatch } = this.props;
-    this.contextMenu = new RequestListContextMenu({
-      cloneSelectedRequest: () => dispatch(Actions.cloneSelectedRequest()),
-      openStatistics: (open) => dispatch(Actions.openStatistics(open)),
-    });
-    this.tooltip = new HTMLTooltip(NetMonitorController._toolbox.doc, { type: "arrow" });
-  },
-
   componentDidMount() {
     // Set the CSS variables for waterfall scaling
     this.setScalingStyles();
 
     // Install event handler for displaying a tooltip
-    this.tooltip.startTogglingOnHover(this.refs.contentEl, this.onHover, {
+    this.props.tooltip.startTogglingOnHover(this.refs.contentEl, this.onHover, {
       toggleDelay: REQUESTS_TOOLTIP_TOGGLE_DELAY,
       interactive: true
     });
 
     // Install event handler to hide the tooltip on scroll
     this.refs.contentEl.addEventListener("scroll", this.onScroll, true);
   },
 
@@ -98,17 +73,17 @@ const RequestListContent = createClass({
       node.scrollTop = node.scrollHeight;
     }
   },
 
   componentWillUnmount() {
     this.refs.contentEl.removeEventListener("scroll", this.onScroll, true);
 
     // Uninstall the tooltip event handler
-    this.tooltip.stopTogglingOnHover();
+    this.props.tooltip.stopTogglingOnHover();
   },
 
   /**
    * Set the CSS variables for waterfall scaling. If React supported setting CSS
    * variables as part of the "style" property of a DOM element, we would use that.
    *
    * However, React doesn't support this, so we need to use a hack and update the
    * DOM element directly: https://github.com/facebook/react/issues/6411
@@ -172,17 +147,17 @@ const RequestListContent = createClass({
 
     return false;
   }),
 
   /**
    * Scroll listener for the requests menu view.
    */
   onScroll() {
-    this.tooltip.hide();
+    this.props.tooltip.hide();
   },
 
   /**
    * Handler for keyboard events. For arrow up/down, page up/down, home/end,
    * move the selection up or down.
    */
   onKeyDown(e) {
     let delta;
@@ -213,21 +188,16 @@ const RequestListContent = createClass({
     if (delta) {
       // Prevent scrolling when pressing navigation keys.
       e.preventDefault();
       e.stopPropagation();
       this.props.onSelectDelta(delta);
     }
   },
 
-  onContextMenu(evt) {
-    evt.preventDefault();
-    this.contextMenu.open(evt);
-  },
-
   /**
    * If selection has just changed (by keyboard navigation), don't keep the list
    * scrolled to bottom, but allow scrolling up with the selection.
    */
   onFocusedNodeChange() {
     this.shouldScrollBottom = false;
   },
 
@@ -236,62 +206,66 @@ const RequestListContent = createClass({
    */
   onFocusedNodeUnmount() {
     if (this.refs.contentEl) {
       this.refs.contentEl.focus();
     }
   },
 
   render() {
-    const {
-      displayedRequests,
-      firstRequestStartedMillis,
-      selectedRequestId,
-      onItemMouseDown,
-      onSecurityIconClick,
-    } = this.props;
+    const { selectedRequestId,
+            displayedRequests,
+            firstRequestStartedMillis,
+            onItemMouseDown,
+            onItemContextMenu,
+            onSecurityIconClick } = this.props;
 
-    return (
-      div({
+    return div(
+      {
         ref: "contentEl",
         className: "requests-menu-contents",
         tabIndex: 0,
         onKeyDown: this.onKeyDown,
       },
-        displayedRequests.map((item, index) => RequestListItem({
-          key: item.id,
-          item,
-          index,
-          isSelected: item.id === selectedRequestId,
-          firstRequestStartedMillis,
-          onMouseDown: () => onItemMouseDown(item.id),
-          onContextMenu: this.onContextMenu,
-          onFocusedNodeChange: this.onFocusedNodeChange,
-          onFocusedNodeUnmount: this.onFocusedNodeUnmount,
-          onSecurityIconClick: () => onSecurityIconClick(item.securityState),
-        }))
-      )
+      displayedRequests.map((item, index) => RequestListItem({
+        key: item.id,
+        item,
+        index,
+        isSelected: item.id === selectedRequestId,
+        firstRequestStartedMillis,
+        onMouseDown: e => onItemMouseDown(e, item.id),
+        onContextMenu: e => onItemContextMenu(e, item.id),
+        onSecurityIconClick: e => onSecurityIconClick(e, item),
+        onFocusedNodeChange: this.onFocusedNodeChange,
+        onFocusedNodeUnmount: this.onFocusedNodeUnmount,
+      }))
     );
   },
 });
 
 module.exports = connect(
-  (state) => ({
+  state => ({
     displayedRequests: getDisplayedRequests(state),
-    firstRequestStartedMillis: state.requests.firstStartedMillis,
     selectedRequestId: state.requests.selectedId,
     scale: getWaterfallScale(state),
+    firstRequestStartedMillis: state.requests.firstStartedMillis,
+    tooltip: NetMonitorView.RequestsMenu.tooltip,
   }),
-  (dispatch) => ({
-    dispatch,
-    onItemMouseDown: (id) => dispatch(Actions.selectRequest(id)),
+  dispatch => ({
+    onItemMouseDown: (e, item) => dispatch(Actions.selectRequest(item)),
+    onItemContextMenu: (e, item) => {
+      e.preventDefault();
+      NetMonitorView.RequestsMenu.contextMenu.open(e);
+    },
+    onSelectDelta: (delta) => dispatch(Actions.selectDelta(delta)),
     /**
      * A handler that opens the security tab in the details view if secure or
      * broken security indicator is clicked.
      */
-    onSecurityIconClick: (securityState) => {
+    onSecurityIconClick: (e, item) => {
+      const { securityState } = item;
+      // Choose the security tab.
       if (securityState && securityState !== "insecure") {
         dispatch(Actions.selectDetailsPanelTab("security"));
       }
     },
-    onSelectDelta: (delta) => dispatch(Actions.selectDelta(delta)),
-  }),
+  })
 )(RequestListContent);
--- a/devtools/client/netmonitor/components/request-list-empty.js
+++ b/devtools/client/netmonitor/components/request-list-empty.js
@@ -62,13 +62,13 @@ const RequestListEmptyNotice = createCla
       )
     );
   }
 });
 
 module.exports = connect(
   undefined,
   dispatch => ({
-    onPerfClick: () => dispatch(Actions.openStatistics(true)),
-    onReloadClick: () =>
+    onPerfClick: e => dispatch(Actions.openStatistics(true)),
+    onReloadClick: e =>
       NetMonitorController.triggerActivity(ACTIVITY_TYPE.RELOAD.WITH_CACHE_DEFAULT),
   })
 )(RequestListEmptyNotice);
--- a/devtools/client/netmonitor/components/request-list-item.js
+++ b/devtools/client/netmonitor/components/request-list-item.js
@@ -1,27 +1,21 @@
 /* This Source Code Form is subject to the terms of the Mozilla Public
  * License, v. 2.0. If a copy of the MPL was not distributed with this
  * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
 
 /* eslint-disable react/prop-types */
 
 "use strict";
 
-const {
-  createClass,
-  createFactory,
-  DOM,
-  PropTypes,
-} = require("devtools/client/shared/vendor/react");
+const { createClass, createFactory, PropTypes, DOM } = require("devtools/client/shared/vendor/react");
+const { div, span, img } = DOM;
 const { L10N } = require("../l10n");
+const { getFormattedSize } = require("../utils/format-utils");
 const { getAbbreviatedMimeType } = require("../request-utils");
-const { getFormattedSize } = require("../utils/format-utils");
-
-const { div, img, span } = DOM;
 
 /**
  * Compare two objects on a subset of their properties
  */
 function propertiesEqual(props, item1, item2) {
   return item1 === item2 || props.every(p => item1[p] === item2[p]);
 }
 
@@ -48,17 +42,17 @@ const UPDATED_REQ_ITEM_PROPS = [
   "transferredSize",
   "startedMillis",
   "totalTime",
 ];
 
 const UPDATED_REQ_PROPS = [
   "index",
   "isSelected",
-  "firstRequestStartedMillis",
+  "firstRequestStartedMillis"
 ];
 
 /**
  * Render one row in the request list.
  */
 const RequestListItem = createClass({
   displayName: "RequestListItem",
 
@@ -77,17 +71,17 @@ const RequestListItem = createClass({
   componentDidMount() {
     if (this.props.isSelected) {
       this.refs.el.focus();
     }
   },
 
   shouldComponentUpdate(nextProps) {
     return !propertiesEqual(UPDATED_REQ_ITEM_PROPS, this.props.item, nextProps.item) ||
-      !propertiesEqual(UPDATED_REQ_PROPS, this.props, nextProps);
+           !propertiesEqual(UPDATED_REQ_PROPS, this.props, nextProps);
   },
 
   componentDidUpdate(prevProps) {
     if (!prevProps.isSelected && this.props.isSelected) {
       this.refs.el.focus();
       if (this.props.onFocusedNodeChange) {
         this.props.onFocusedNodeChange();
       }
@@ -120,49 +114,46 @@ const RequestListItem = createClass({
     } = this.props;
 
     let classList = [ "request-list-item" ];
     if (isSelected) {
       classList.push("selected");
     }
     classList.push(index % 2 ? "odd" : "even");
 
-    return (
-      div({
+    return div(
+      {
         ref: "el",
         className: classList.join(" "),
         "data-id": item.id,
         tabIndex: 0,
         onContextMenu,
         onMouseDown,
       },
-        StatusColumn({ item }),
-        MethodColumn({ item }),
-        FileColumn({ item }),
-        DomainColumn({ item, onSecurityIconClick }),
-        CauseColumn({ item }),
-        TypeColumn({ item }),
-        TransferredSizeColumn({ item }),
-        ContentSizeColumn({ item }),
-        WaterfallColumn({ item, firstRequestStartedMillis }),
-      )
+      StatusColumn({ item }),
+      MethodColumn({ item }),
+      FileColumn({ item }),
+      DomainColumn({ item, onSecurityIconClick }),
+      CauseColumn({ item }),
+      TypeColumn({ item }),
+      TransferredSizeColumn({ item }),
+      ContentSizeColumn({ item }),
+      WaterfallColumn({ item, firstRequestStartedMillis })
     );
   }
 });
 
 const UPDATED_STATUS_PROPS = [
   "status",
   "statusText",
   "fromCache",
   "fromServiceWorker",
 ];
 
 const StatusColumn = createFactory(createClass({
-  displayName: "StatusColumn",
-
   shouldComponentUpdate(nextProps) {
     return !propertiesEqual(UPDATED_STATUS_PROPS, this.props.item, nextProps.item);
   },
 
   render() {
     const { status, statusText, fromCache, fromServiceWorker } = this.props.item;
 
     let code, title;
@@ -182,85 +173,74 @@ const StatusColumn = createFactory(creat
           title += " (cached)";
         }
         if (fromServiceWorker) {
           title += " (service worker)";
         }
       }
     }
 
-    return (
-      div({ className: "requests-menu-subitem requests-menu-status", title },
-        div({ className: "requests-menu-status-icon", "data-code": code }),
-        span({ className: "subitem-label requests-menu-status-code" }, status),
-      )
+    return div({ className: "requests-menu-subitem requests-menu-status", title },
+      div({ className: "requests-menu-status-icon", "data-code": code }),
+      span({ className: "subitem-label requests-menu-status-code" }, status)
     );
   }
 }));
 
 const MethodColumn = createFactory(createClass({
-  displayName: "MethodColumn",
-
   shouldComponentUpdate(nextProps) {
     return this.props.item.method !== nextProps.item.method;
   },
 
   render() {
     const { method } = this.props.item;
-    return (
-      div({ className: "requests-menu-subitem requests-menu-method-box" },
-        span({ className: "subitem-label requests-menu-method" }, method)
-      )
+    return div({ className: "requests-menu-subitem requests-menu-method-box" },
+      span({ className: "subitem-label requests-menu-method" }, method)
     );
   }
 }));
 
 const UPDATED_FILE_PROPS = [
   "urlDetails",
   "responseContentDataUri",
 ];
 
 const FileColumn = createFactory(createClass({
-  displayName: "FileColumn",
-
   shouldComponentUpdate(nextProps) {
     return !propertiesEqual(UPDATED_FILE_PROPS, this.props.item, nextProps.item);
   },
 
   render() {
     const { urlDetails, responseContentDataUri } = this.props.item;
 
-    return (
-      div({ className: "requests-menu-subitem requests-menu-icon-and-file" },
-        img({
-          className: "requests-menu-icon",
-          src: responseContentDataUri,
-          hidden: !responseContentDataUri,
-          "data-type": responseContentDataUri ? "thumbnail" : undefined,
-        }),
-        div({
+    return div({ className: "requests-menu-subitem requests-menu-icon-and-file" },
+      img({
+        className: "requests-menu-icon",
+        src: responseContentDataUri,
+        hidden: !responseContentDataUri,
+        "data-type": responseContentDataUri ? "thumbnail" : undefined
+      }),
+      div(
+        {
           className: "subitem-label requests-menu-file",
-          title: urlDetails.unicodeUrl,
+          title: urlDetails.unicodeUrl
         },
-          urlDetails.baseNameWithQuery,
-        ),
+        urlDetails.baseNameWithQuery
       )
     );
   }
 }));
 
 const UPDATED_DOMAIN_PROPS = [
   "urlDetails",
   "remoteAddress",
   "securityState",
 ];
 
 const DomainColumn = createFactory(createClass({
-  displayName: "DomainColumn",
-
   shouldComponentUpdate(nextProps) {
     return !propertiesEqual(UPDATED_DOMAIN_PROPS, this.props.item, nextProps.item);
   },
 
   render() {
     const { item, onSecurityIconClick } = this.props;
     const { urlDetails, remoteAddress, securityState } = item;
 
@@ -271,32 +251,29 @@ const DomainColumn = createFactory(creat
       iconTitle = L10N.getStr("netmonitor.security.state.secure");
     } else if (securityState) {
       iconClassList.push(`security-state-${securityState}`);
       iconTitle = L10N.getStr(`netmonitor.security.state.${securityState}`);
     }
 
     let title = urlDetails.host + (remoteAddress ? ` (${remoteAddress})` : "");
 
-    return (
-      div({ className: "requests-menu-subitem requests-menu-security-and-domain" },
-        div({
-          className: iconClassList.join(" "),
-          title: iconTitle,
-          onClick: onSecurityIconClick,
-        }),
-        span({ className: "subitem-label requests-menu-domain", title }, urlDetails.host),
-      )
+    return div(
+      { className: "requests-menu-subitem requests-menu-security-and-domain" },
+      div({
+        className: iconClassList.join(" "),
+        title: iconTitle,
+        onClick: onSecurityIconClick,
+      }),
+      span({ className: "subitem-label requests-menu-domain", title }, urlDetails.host)
     );
   }
 }));
 
 const CauseColumn = createFactory(createClass({
-  displayName: "CauseColumn",
-
   shouldComponentUpdate(nextProps) {
     return this.props.item.cause !== nextProps.item.cause;
   },
 
   render() {
     const { cause } = this.props.item;
 
     let causeType = "";
@@ -305,72 +282,57 @@ const CauseColumn = createFactory(create
 
     if (cause) {
       // Legacy server might send a numeric value. Display it as "unknown"
       causeType = typeof cause.type === "string" ? cause.type : "unknown";
       causeUri = cause.loadingDocumentUri;
       causeHasStack = cause.stacktrace && cause.stacktrace.length > 0;
     }
 
-    return (
-      div({
-        className: "requests-menu-subitem requests-menu-cause",
-        title: causeUri,
-      },
-        span({
-          className: "requests-menu-cause-stack",
-          hidden: !causeHasStack,
-        }, "JS"),
-        span({ className: "subitem-label" }, causeType),
-      )
+    return div(
+      { className: "requests-menu-subitem requests-menu-cause", title: causeUri },
+      span({ className: "requests-menu-cause-stack", hidden: !causeHasStack }, "JS"),
+      span({ className: "subitem-label" }, causeType)
     );
   }
 }));
 
 const CONTENT_MIME_TYPE_ABBREVIATIONS = {
   "ecmascript": "js",
   "javascript": "js",
   "x-javascript": "js"
 };
 
 const TypeColumn = createFactory(createClass({
-  displayName: "TypeColumn",
-
   shouldComponentUpdate(nextProps) {
     return this.props.item.mimeType !== nextProps.item.mimeType;
   },
 
   render() {
     const { mimeType } = this.props.item;
     let abbrevType;
     if (mimeType) {
       abbrevType = getAbbreviatedMimeType(mimeType);
       abbrevType = CONTENT_MIME_TYPE_ABBREVIATIONS[abbrevType] || abbrevType;
     }
 
-    return (
-      div({
-        className: "requests-menu-subitem requests-menu-type",
-        title: mimeType,
-      },
-        span({ className: "subitem-label" }, abbrevType),
-      )
+    return div(
+      { className: "requests-menu-subitem requests-menu-type", title: mimeType },
+      span({ className: "subitem-label" }, abbrevType)
     );
   }
 }));
 
 const UPDATED_TRANSFERRED_PROPS = [
   "transferredSize",
   "fromCache",
   "fromServiceWorker",
 ];
 
 const TransferredSizeColumn = createFactory(createClass({
-  displayName: "TransferredSizeColumn",
-
   shouldComponentUpdate(nextProps) {
     return !propertiesEqual(UPDATED_TRANSFERRED_PROPS, this.props.item, nextProps.item);
   },
 
   render() {
     const { transferredSize, fromCache, fromServiceWorker } = this.props.item;
 
     let text;
@@ -382,81 +344,68 @@ const TransferredSizeColumn = createFact
       text = L10N.getStr("networkMenu.sizeServiceWorker");
       className += " theme-comment";
     } else if (typeof transferredSize == "number") {
       text = getFormattedSize(transferredSize);
     } else if (transferredSize === null) {
       text = L10N.getStr("networkMenu.sizeUnavailable");
     }
 
-    return (
-      div({
-        className: "requests-menu-subitem requests-menu-transferred",
-        title: text,
-      },
-        span({ className }, text),
-      )
+    return div(
+      { className: "requests-menu-subitem requests-menu-transferred", title: text },
+      span({ className }, text)
     );
   }
 }));
 
 const ContentSizeColumn = createFactory(createClass({
-  displayName: "ContentSizeColumn",
-
   shouldComponentUpdate(nextProps) {
     return this.props.item.contentSize !== nextProps.item.contentSize;
   },
 
   render() {
     const { contentSize } = this.props.item;
 
     let text;
     if (typeof contentSize == "number") {
       text = getFormattedSize(contentSize);
     }
 
-    return (
-      div({
+    return div(
+      {
         className: "requests-menu-subitem subitem-label requests-menu-size",
-        title: text,
+        title: text
       },
-        span({ className: "subitem-label" }, text),
-      )
+      span({ className: "subitem-label" }, text)
     );
   }
 }));
 
 const UPDATED_WATERFALL_PROPS = [
   "eventTimings",
   "totalTime",
   "fromCache",
   "fromServiceWorker",
 ];
 
 const WaterfallColumn = createFactory(createClass({
-  displayName: "WaterfallColumn",
-
   shouldComponentUpdate(nextProps) {
     return this.props.firstRequestStartedMillis !== nextProps.firstRequestStartedMillis ||
-      !propertiesEqual(UPDATED_WATERFALL_PROPS, this.props.item, nextProps.item);
+           !propertiesEqual(UPDATED_WATERFALL_PROPS, this.props.item, nextProps.item);
   },
 
   render() {
     const { item, firstRequestStartedMillis } = this.props;
+    const startedDeltaMillis = item.startedMillis - firstRequestStartedMillis;
+    const paddingInlineStart = `${startedDeltaMillis}px`;
 
-    return (
-      div({ className: "requests-menu-subitem requests-menu-waterfall" },
-        div({
-          className: "requests-menu-timings",
-          style: {
-            paddingInlineStart: `${item.startedMillis - firstRequestStartedMillis}px`,
-          },
-        },
-          timingBoxes(item),
-        )
+    return div({ className: "requests-menu-subitem requests-menu-waterfall" },
+      div(
+        { className: "requests-menu-timings", style: { paddingInlineStart } },
+        timingBoxes(item)
       )
     );
   }
 }));
 
 // List of properties of the timing info we want to create boxes for
 const TIMING_KEYS = ["blocked", "dns", "connect", "send", "wait", "receive"];
 
new file mode 100644
--- /dev/null
+++ b/devtools/client/netmonitor/components/request-list-tooltip.js
@@ -0,0 +1,107 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+/* globals gNetwork, NetMonitorController */
+
+"use strict";
+
+const { Task } = require("devtools/shared/task");
+const { formDataURI } = require("../request-utils");
+const { WEBCONSOLE_L10N } = require("../l10n");
+const { setImageTooltip,
+        getImageDimensions } = require("devtools/client/shared/widgets/tooltip/ImageTooltipHelper");
+
+// px
+const REQUESTS_TOOLTIP_IMAGE_MAX_DIM = 400;
+// px
+const REQUESTS_TOOLTIP_STACK_TRACE_WIDTH = 600;
+
+const HTML_NS = "http://www.w3.org/1999/xhtml";
+
+const setTooltipImageContent = Task.async(function* (tooltip, itemEl, requestItem) {
+  let { mimeType, text, encoding } = requestItem.responseContent.content;
+
+  if (!mimeType || !mimeType.includes("image/")) {
+    return false;
+  }
+
+  let string = yield gNetwork.getString(text);
+  let src = formDataURI(mimeType, encoding, string);
+  let maxDim = REQUESTS_TOOLTIP_IMAGE_MAX_DIM;
+  let { naturalWidth, naturalHeight } = yield getImageDimensions(tooltip.doc, src);
+  let options = { maxDim, naturalWidth, naturalHeight };
+  setImageTooltip(tooltip, tooltip.doc, src, options);
+
+  return itemEl.querySelector(".requests-menu-icon");
+});
+
+const setTooltipStackTraceContent = Task.async(function* (tooltip, requestItem) {
+  let {stacktrace} = requestItem.cause;
+
+  if (!stacktrace || stacktrace.length == 0) {
+    return false;
+  }
+
+  let doc = tooltip.doc;
+  let el = doc.createElementNS(HTML_NS, "div");
+  el.className = "stack-trace-tooltip devtools-monospace";
+
+  for (let f of stacktrace) {
+    let { functionName, filename, lineNumber, columnNumber, asyncCause } = f;
+
+    if (asyncCause) {
+      // if there is asyncCause, append a "divider" row into the trace
+      let asyncFrameEl = doc.createElementNS(HTML_NS, "div");
+      asyncFrameEl.className = "stack-frame stack-frame-async";
+      asyncFrameEl.textContent =
+        WEBCONSOLE_L10N.getFormatStr("stacktrace.asyncStack", asyncCause);
+      el.appendChild(asyncFrameEl);
+    }
+
+    // Parse a source name in format "url -> url"
+    let sourceUrl = filename.split(" -> ").pop();
+
+    let frameEl = doc.createElementNS(HTML_NS, "div");
+    frameEl.className = "stack-frame stack-frame-call";
+
+    let funcEl = doc.createElementNS(HTML_NS, "span");
+    funcEl.className = "stack-frame-function-name";
+    funcEl.textContent =
+      functionName || WEBCONSOLE_L10N.getStr("stacktrace.anonymousFunction");
+    frameEl.appendChild(funcEl);
+
+    let sourceEl = doc.createElementNS(HTML_NS, "span");
+    sourceEl.className = "stack-frame-source-name";
+    frameEl.appendChild(sourceEl);
+
+    let sourceInnerEl = doc.createElementNS(HTML_NS, "span");
+    sourceInnerEl.className = "stack-frame-source-name-inner";
+    sourceEl.appendChild(sourceInnerEl);
+
+    sourceInnerEl.textContent = sourceUrl;
+    sourceInnerEl.title = sourceUrl;
+
+    let lineEl = doc.createElementNS(HTML_NS, "span");
+    lineEl.className = "stack-frame-line";
+    lineEl.textContent = `:${lineNumber}:${columnNumber}`;
+    sourceInnerEl.appendChild(lineEl);
+
+    frameEl.addEventListener("click", () => {
+      // hide the tooltip immediately, not after delay
+      tooltip.hide();
+      NetMonitorController.viewSourceInDebugger(filename, lineNumber);
+    });
+
+    el.appendChild(frameEl);
+  }
+
+  tooltip.setContent(el, {width: REQUESTS_TOOLTIP_STACK_TRACE_WIDTH});
+
+  return true;
+});
+
+module.exports = {
+  setTooltipImageContent,
+  setTooltipStackTraceContent,
+};
--- a/devtools/client/netmonitor/components/request-list.js
+++ b/devtools/client/netmonitor/components/request-list.js
@@ -1,127 +1,34 @@
 /* This Source Code Form is subject to the terms of the Mozilla Public
  * License, v. 2.0. If a copy of the MPL was not distributed with this
  * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
 
-/* eslint-env browser */
-/* globals gNetwork */
-
 "use strict";
 
-const {
-  createClass,
-  createFactory,
-  DOM,
-  PropTypes,
-} = require("devtools/client/shared/vendor/react");
+const { createFactory, PropTypes, DOM } = require("devtools/client/shared/vendor/react");
+const { div } = DOM;
 const { connect } = require("devtools/client/shared/vendor/react-redux");
-const { setNamedTimeout } = require("devtools/client/shared/widgets/view-helpers");
-const Actions = require("../actions/index");
-const { Prefs } = require("../prefs");
-const { getFormDataSections } = require("../request-utils");
-const {
-  getActiveFilters,
-  getSelectedRequest,
-} = require("../selectors/index");
-
-// Components
+const RequestListHeader = createFactory(require("./request-list-header"));
+const RequestListEmptyNotice = createFactory(require("./request-list-empty"));
 const RequestListContent = createFactory(require("./request-list-content"));
-const RequestListEmptyNotice = createFactory(require("./request-list-empty"));
-const RequestListHeader = createFactory(require("./request-list-header"));
-
-const { div } = DOM;
 
 /**
- * Request panel component
+ * Renders the request list - header, empty text, the actual content with rows
  */
-const RequestList = createClass({
-  displayName: "RequestList",
-
-  propTypes: {
-    activeFilters: PropTypes.array,
-    dispatch: PropTypes.func,
-    isEmpty: PropTypes.bool.isRequired,
-    request: PropTypes.object,
-    networkDetailsOpen: PropTypes.bool,
-  },
-
-  componentDidMount() {
-    const { dispatch } = this.props;
-
-    Prefs.filters.forEach((type) => dispatch(Actions.toggleRequestFilterType(type)));
-    this.splitter = document.querySelector("#network-inspector-view-splitter");
-    this.splitter.addEventListener("mouseup", this.resize);
-    window.addEventListener("resize", this.resize);
-  },
-
-  componentWillReceiveProps(nextProps) {
-    const { dispatch, request = {}, networkDetailsOpen } = this.props;
-
-    if (nextProps.request && nextProps.request !== request) {
-      dispatch(Actions.openNetworkDetails(true));
-    }
-
-    if (nextProps.networkDetailsOpen !== networkDetailsOpen) {
-      this.resize();
-    }
-
-    const {
-      formDataSections,
-      requestHeaders,
-      requestHeadersFromUploadStream,
-      requestPostData,
-    } = nextProps.request || {};
+const RequestList = function ({ isEmpty }) {
+  return div({ className: "request-list-container" },
+    RequestListHeader(),
+    isEmpty ? RequestListEmptyNotice() : RequestListContent()
+  );
+};
 
-    if (!formDataSections && requestHeaders &&
-        requestHeadersFromUploadStream && requestPostData) {
-      getFormDataSections(
-        requestHeaders,
-        requestHeadersFromUploadStream,
-        requestPostData,
-        gNetwork.getString.bind(gNetwork),
-      ).then((newFormDataSections) => {
-        dispatch(Actions.updateRequest(
-          request.id,
-          { formDataSections: newFormDataSections },
-          true,
-        ));
-      });
-    }
-  },
-
-  componentWillUnmount() {
-    Prefs.filters = this.props.activeFilters;
-    this.splitter.removeEventListener("mouseup", this.resize);
-    window.removeEventListener("resize", this.resize);
-  },
+RequestList.displayName = "RequestList";
 
-  resize() {
-    const { dispatch } = this.props;
-    // Allow requests to settle down first.
-    setNamedTimeout("resize-events", 50, () => {
-      const waterfallHeaderEl =
-        document.querySelector("#requests-menu-waterfall-header-box");
-      if (waterfallHeaderEl) {
-        const { width } = waterfallHeaderEl.getBoundingClientRect();
-        dispatch(Actions.resizeWaterfall(width));
-      }
-    });
-  },
-
-  render() {
-    return (
-      div({ className: "request-list-container" },
-        RequestListHeader(),
-        this.props.isEmpty ? RequestListEmptyNotice() : RequestListContent(),
-      )
-    );
-  }
-});
+RequestList.propTypes = {
+  isEmpty: PropTypes.bool.isRequired,
+};
 
 module.exports = connect(
-  (state) => ({
-    activeFilters: getActiveFilters(state),
-    isEmpty: state.requests.requests.isEmpty(),
-    request: getSelectedRequest(state),
-    networkDetailsOpen: state.ui.networkDetailsOpen,
+  state => ({
+    isEmpty: state.requests.requests.isEmpty()
   })
 )(RequestList);
--- a/devtools/client/netmonitor/components/toolbar.js
+++ b/devtools/client/netmonitor/components/toolbar.js
@@ -111,17 +111,17 @@ function Toolbar({
           type: "filter",
           onChange: setRequestFilterText,
         }),
         button({
           className: toggleButtonClassName.join(" "),
           title: networkDetailsOpen ? COLLPASE_DETAILS_PANE : EXPAND_DETAILS_PANE,
           disabled: networkDetailsToggleDisabled,
           tabIndex: "0",
-          onClick: toggleNetworkDetails,
+          onMouseDown: toggleNetworkDetails,
         }),
       )
     )
   );
 }
 
 Toolbar.displayName = "Toolbar";
 
--- a/devtools/client/netmonitor/har/har-builder.js
+++ b/devtools/client/netmonitor/har/har-builder.js
@@ -25,17 +25,18 @@ const HAR_VERSION = "1.1";
  * This object is responsible for building HAR file. See HAR spec:
  * https://dvcs.w3.org/hg/webperf/raw-file/tip/specs/HAR/Overview.html
  * http://www.softwareishard.com/blog/har-12-spec/
  *
  * @param {Object} options configuration object
  *
  * The following options are supported:
  *
- * - items {Array}: List of Network requests to be exported.
+ * - items {Array}: List of Network requests to be exported. It is possible
+ *   to use directly: NetMonitorView.RequestsMenu.items
  *
  * - id {String}: ID of the exported page.
  *
  * - title {String}: Title of the exported page.
  *
  * - includeResponseBodies {Boolean}: Set to true to include HTTP response
  *   bodies in the result data structure.
  */
--- a/devtools/client/netmonitor/har/har-exporter.js
+++ b/devtools/client/netmonitor/har/har-exporter.js
@@ -41,17 +41,18 @@ const HarExporter = {
    *        Configuration object
    *
    * The following options are supported:
    *
    * - includeResponseBodies {Boolean}: If set to true, HTTP response bodies
    *   are also included in the HAR file (can produce significantly bigger
    *   amount of data).
    *
-   * - items {Array}: List of Network requests to be exported.
+   * - items {Array}: List of Network requests to be exported. It is possible
+   *   to use directly: NetMonitorView.RequestsMenu.items
    *
    * - jsonp {Boolean}: If set to true the export format is HARP (support
    *   for JSONP syntax).
    *
    * - jsonpCallback {String}: Default name of JSONP callback (used for
    *   HARP format).
    *
    * - compress {Boolean}: If set to true the final HAR file is zipped.
--- a/devtools/client/netmonitor/har/test/browser_net_har_copy_all_as_har.js
+++ b/devtools/client/netmonitor/har/test/browser_net_har_copy_all_as_har.js
@@ -6,29 +6,26 @@
 /**
  * Basic tests for exporting Network panel content into HAR format.
  */
 add_task(function* () {
   let { tab, monitor } = yield initNetMonitor(SIMPLE_URL);
 
   info("Starting test... ");
 
-  let { document, gStore, windowRequire } = monitor.panelWin;
-  let Actions = windowRequire("devtools/client/netmonitor/actions/index");
-  let RequestListContextMenu = windowRequire(
-    "devtools/client/netmonitor/request-list-context-menu");
+  let { NetMonitorView } = monitor.panelWin;
+  let { RequestsMenu } = NetMonitorView;
 
-  gStore.dispatch(Actions.batchEnable(false));
+  RequestsMenu.lazyUpdate = false;
 
   let wait = waitForNetworkEvents(monitor, 1);
   tab.linkedBrowser.reload();
   yield wait;
 
-  let contextMenu = new RequestListContextMenu({});
-  yield contextMenu.copyAllAsHar();
+  yield RequestsMenu.contextMenu.copyAllAsHar();
 
   let jsonString = SpecialPowers.getClipboardData("text/unicode");
   let har = JSON.parse(jsonString);
 
   // Check out HAR log
   isnot(har.log, null, "The HAR log must exist");
   is(har.log.creator.name, "Firefox", "The creator field must be set");
   is(har.log.browser.name, "Firefox", "The browser field must be set");
--- a/devtools/client/netmonitor/moz.build
+++ b/devtools/client/netmonitor/moz.build
@@ -18,18 +18,18 @@ DevToolsModules(
     'events.js',
     'filter-predicates.js',
     'l10n.js',
     'netmonitor-controller.js',
     'netmonitor-view.js',
     'panel.js',
     'prefs.js',
     'request-list-context-menu.js',
-    'request-list-tooltip.js',
     'request-utils.js',
+    'requests-menu-view.js',
     'sort-predicates.js',
     'store.js',
     'waterfall-background.js',
 )
 
 BROWSER_CHROME_MANIFESTS += ['test/browser.ini']
 
 with Files('**'):
--- a/devtools/client/netmonitor/netmonitor-controller.js
+++ b/devtools/client/netmonitor/netmonitor-controller.js
@@ -1,35 +1,35 @@
 /* This Source Code Form is subject to the terms of the Mozilla Public
  * License, v. 2.0. If a copy of the MPL was not distributed with this
  * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
 
 /* eslint-disable mozilla/reject-some-requires */
-/* globals window, NetMonitorView, gStore, gNetwork, dumpn */
+/* globals window, NetMonitorView, gStore, dumpn */
 
 "use strict";
 
 const promise = require("promise");
 const Services = require("Services");
+const {XPCOMUtils} = require("resource://gre/modules/XPCOMUtils.jsm");
 const EventEmitter = require("devtools/shared/event-emitter");
-const { TimelineFront } = require("devtools/shared/fronts/timeline");
-const { CurlUtils } = require("devtools/client/shared/curl");
-const { Task } = require("devtools/shared/task");
+const Editor = require("devtools/client/sourceeditor/editor");
+const {TimelineFront} = require("devtools/shared/fronts/timeline");
+const {Task} = require("devtools/shared/task");
 const { ACTIVITY_TYPE } = require("./constants");
 const { EVENTS } = require("./events");
 const { configureStore } = require("./store");
 const Actions = require("./actions/index");
-const {
-  fetchHeaders,
-  formDataURI,
-} = require("./request-utils");
-const {
-  getRequestById,
-  getDisplayedRequestById,
-} = require("./selectors/index");
+const { getDisplayedRequestById } = require("./selectors/index");
+const { Prefs } = require("./prefs");
+
+XPCOMUtils.defineConstant(window, "EVENTS", EVENTS);
+XPCOMUtils.defineConstant(window, "ACTIVITY_TYPE", ACTIVITY_TYPE);
+XPCOMUtils.defineConstant(window, "Editor", Editor);
+XPCOMUtils.defineConstant(window, "Prefs", Prefs);
 
 // Initialize the global Redux store
 window.gStore = configureStore();
 
 /**
  * Object defining the network monitor controller components.
  */
 var NetMonitorController = {
@@ -413,18 +413,17 @@ TargetEventsHandler.prototype = {
    * @param object packet
    *        Packet received from the server.
    */
   _onTabNavigated: function (type, packet) {
     switch (type) {
       case "will-navigate": {
         // Reset UI.
         if (!Services.prefs.getBoolPref("devtools.webconsole.persistlog")) {
-          gStore.dispatch(Actions.batchReset());
-          gStore.dispatch(Actions.clearRequests());
+          NetMonitorView.RequestsMenu.reset();
         } else {
           // If the log is persistent, just clear all accumulated timing markers.
           gStore.dispatch(Actions.clearTimingMarkers());
         }
 
         window.emit(EVENTS.TARGET_WILL_NAVIGATE);
         break;
       }
@@ -442,28 +441,25 @@ TargetEventsHandler.prototype = {
     NetMonitorController.shutdownNetMonitor();
   }
 };
 
 /**
  * Functions handling target network events.
  */
 function NetworkEventsHandler() {
-  this.addRequest = this.addRequest.bind(this);
-  this.updateRequest = this.updateRequest.bind(this);
   this._onNetworkEvent = this._onNetworkEvent.bind(this);
   this._onNetworkEventUpdate = this._onNetworkEventUpdate.bind(this);
   this._onDocLoadingMarker = this._onDocLoadingMarker.bind(this);
   this._onRequestHeaders = this._onRequestHeaders.bind(this);
   this._onRequestCookies = this._onRequestCookies.bind(this);
   this._onRequestPostData = this._onRequestPostData.bind(this);
   this._onResponseHeaders = this._onResponseHeaders.bind(this);
   this._onResponseCookies = this._onResponseCookies.bind(this);
   this._onResponseContent = this._onResponseContent.bind(this);
-  this._onSecurityInfo = this._onSecurityInfo.bind(this);
   this._onEventTimings = this._onEventTimings.bind(this);
 }
 
 NetworkEventsHandler.prototype = {
   get client() {
     return NetMonitorController._target.client;
   },
 
@@ -547,343 +543,203 @@ NetworkEventsHandler.prototype = {
       startedDateTime,
       request: { method, url },
       isXHR,
       cause,
       fromCache,
       fromServiceWorker
     } = networkInfo;
 
-    this.addRequest(
+    NetMonitorView.RequestsMenu.addRequest(
       actor, {startedDateTime, method, url, isXHR, cause, fromCache, fromServiceWorker}
     );
     window.emit(EVENTS.NETWORK_EVENT, actor);
   },
 
-  addRequest(id, data) {
-    let { method, url, isXHR, cause, startedDateTime, fromCache,
-          fromServiceWorker } = data;
-
-    gStore.dispatch(Actions.addRequest(
-      id,
-      {
-        // Convert the received date/time string to a unix timestamp.
-        startedMillis: Date.parse(startedDateTime),
-        method,
-        url,
-        isXHR,
-        cause,
-        fromCache,
-        fromServiceWorker,
-      },
-      true
-    ))
-    .then(() => window.emit(EVENTS.REQUEST_ADDED, id));
-  },
-
-  updateRequest: Task.async(function* (id, data) {
-    const action = Actions.updateRequest(id, data, true);
-    yield gStore.dispatch(action);
-    let {
-      responseContent,
-      responseCookies,
-      responseHeaders,
-      requestCookies,
-      requestHeaders,
-      requestPostData,
-    } = action.data;
-    let request = getRequestById(gStore.getState(), action.id);
-
-    if (requestHeaders && requestHeaders.headers && requestHeaders.headers.length) {
-      let headers = yield fetchHeaders(
-        requestHeaders, gNetwork.getString.bind(gNetwork));
-      if (headers) {
-        yield gStore.dispatch(Actions.updateRequest(
-          action.id,
-          { requestHeaders: headers },
-          true,
-        ));
-      }
-    }
-
-    if (responseHeaders && responseHeaders.headers && responseHeaders.headers.length) {
-      let headers = yield fetchHeaders(
-        responseHeaders, gNetwork.getString.bind(gNetwork));
-      if (headers) {
-        yield gStore.dispatch(Actions.updateRequest(
-          action.id,
-          { responseHeaders: headers },
-          true,
-        ));
-      }
-    }
-
-    if (request && responseContent && responseContent.content) {
-      let { mimeType } = request;
-      let { text, encoding } = responseContent.content;
-      let response = yield gNetwork.getString(text);
-      let payload = {};
-
-      if (mimeType.includes("image/")) {
-        payload.responseContentDataUri = formDataURI(mimeType, encoding, response);
-      }
-
-      responseContent.content.text = response;
-      payload.responseContent = responseContent;
-
-      yield gStore.dispatch(Actions.updateRequest(action.id, payload, true));
-
-      if (mimeType.includes("image/")) {
-        window.emit(EVENTS.RESPONSE_IMAGE_THUMBNAIL_DISPLAYED);
-      }
-    }
-
-    // Search the POST data upload stream for request headers and add
-    // them as a separate property, different from the classic headers.
-    if (requestPostData && requestPostData.postData) {
-      let { text } = requestPostData.postData;
-      let postData = yield gNetwork.getString(text);
-      const headers = CurlUtils.getHeadersFromMultipartText(postData);
-      const headersSize = headers.reduce((acc, { name, value }) => {
-        return acc + name.length + value.length + 2;
-      }, 0);
-      let payload = {};
-      requestPostData.postData.text = postData;
-      payload.requestPostData = Object.assign({}, requestPostData);
-      payload.requestHeadersFromUploadStream = { headers, headersSize };
-
-      yield gStore.dispatch(Actions.updateRequest(action.id, payload, true));
-    }
-
-    // Fetch request and response cookies long value.
-    // Actor does not provide full sized cookie value when the value is too long
-    // To display values correctly, we need fetch them in each request.
-    if (requestCookies) {
-      let reqCookies = [];
-      // request store cookies in requestCookies or requestCookies.cookies
-      let cookies = requestCookies.cookies ?
-        requestCookies.cookies : requestCookies;
-      // make sure cookies is iterable
-      if (typeof cookies[Symbol.iterator] === "function") {
-        for (let cookie of cookies) {
-          reqCookies.push(Object.assign({}, cookie, {
-            value: yield gNetwork.getString(cookie.value),
-          }));
-        }
-        if (reqCookies.length) {
-          yield gStore.dispatch(Actions.updateRequest(
-            action.id,
-            { requestCookies: reqCookies },
-            true));
-        }
-      }
-    }
-
-    if (responseCookies) {
-      let resCookies = [];
-      // response store cookies in responseCookies or responseCookies.cookies
-      let cookies = responseCookies.cookies ?
-        responseCookies.cookies : responseCookies;
-      // make sure cookies is iterable
-      if (typeof cookies[Symbol.iterator] === "function") {
-        for (let cookie of cookies) {
-          resCookies.push(Object.assign({}, cookie, {
-            value: yield gNetwork.getString(cookie.value),
-          }));
-        }
-        if (resCookies.length) {
-          yield gStore.dispatch(Actions.updateRequest(
-            action.id,
-            { responseCookies: resCookies },
-            true));
-        }
-      }
-    }
-  }),
-
   /**
    * The "networkEventUpdate" message type handler.
    *
    * @param string type
    *        Message type.
    * @param object packet
    *        The message received from the server.
    * @param object networkInfo
    *        The network request information.
    */
   _onNetworkEventUpdate: function (type, { packet, networkInfo }) {
     let { actor } = networkInfo;
+
     switch (packet.updateType) {
       case "requestHeaders":
         this.webConsoleClient.getRequestHeaders(actor, this._onRequestHeaders);
         window.emit(EVENTS.UPDATING_REQUEST_HEADERS, actor);
         break;
       case "requestCookies":
         this.webConsoleClient.getRequestCookies(actor, this._onRequestCookies);
         window.emit(EVENTS.UPDATING_REQUEST_COOKIES, actor);
         break;
       case "requestPostData":
         this.webConsoleClient.getRequestPostData(actor,
           this._onRequestPostData);
         window.emit(EVENTS.UPDATING_REQUEST_POST_DATA, actor);
         break;
       case "securityInfo":
-        this.updateRequest(actor, {
+        NetMonitorView.RequestsMenu.updateRequest(actor, {
           securityState: networkInfo.securityInfo,
         });
         this.webConsoleClient.getSecurityInfo(actor, this._onSecurityInfo);
         window.emit(EVENTS.UPDATING_SECURITY_INFO, actor);
         break;
       case "responseHeaders":
         this.webConsoleClient.getResponseHeaders(actor,
           this._onResponseHeaders);
         window.emit(EVENTS.UPDATING_RESPONSE_HEADERS, actor);
         break;
       case "responseCookies":
         this.webConsoleClient.getResponseCookies(actor,
           this._onResponseCookies);
         window.emit(EVENTS.UPDATING_RESPONSE_COOKIES, actor);
         break;
       case "responseStart":
-        this.updateRequest(actor, {
+        NetMonitorView.RequestsMenu.updateRequest(actor, {
           httpVersion: networkInfo.response.httpVersion,
           remoteAddress: networkInfo.response.remoteAddress,
           remotePort: networkInfo.response.remotePort,
           status: networkInfo.response.status,
           statusText: networkInfo.response.statusText,
           headersSize: networkInfo.response.headersSize
         });
         window.emit(EVENTS.STARTED_RECEIVING_RESPONSE, actor);
         break;
       case "responseContent":
-        this.updateRequest(actor, {
+        NetMonitorView.RequestsMenu.updateRequest(actor, {
           contentSize: networkInfo.response.bodySize,
           transferredSize: networkInfo.response.transferredSize,
           mimeType: networkInfo.response.content.mimeType
         });
         this.webConsoleClient.getResponseContent(actor,
           this._onResponseContent);
         window.emit(EVENTS.UPDATING_RESPONSE_CONTENT, actor);
         break;
       case "eventTimings":
-        this.updateRequest(actor, {
+        NetMonitorView.RequestsMenu.updateRequest(actor, {
           totalTime: networkInfo.totalTime
         });
         this.webConsoleClient.getEventTimings(actor, this._onEventTimings);
         window.emit(EVENTS.UPDATING_EVENT_TIMINGS, actor);
         break;
     }
   },
 
   /**
    * Handles additional information received for a "requestHeaders" packet.
    *
    * @param object response
    *        The message received from the server.
    */
   _onRequestHeaders: function (response) {
-    this.updateRequest(response.from, {
+    NetMonitorView.RequestsMenu.updateRequest(response.from, {
       requestHeaders: response
     }).then(() => {
       window.emit(EVENTS.RECEIVED_REQUEST_HEADERS, response.from);
     });
   },
 
   /**
    * Handles additional information received for a "requestCookies" packet.
    *
    * @param object response
    *        The message received from the server.
    */
   _onRequestCookies: function (response) {
-    this.updateRequest(response.from, {
+    NetMonitorView.RequestsMenu.updateRequest(response.from, {
       requestCookies: response
     }).then(() => {
       window.emit(EVENTS.RECEIVED_REQUEST_COOKIES, response.from);
     });
   },
 
   /**
    * Handles additional information received for a "requestPostData" packet.
    *
    * @param object response
    *        The message received from the server.
    */
   _onRequestPostData: function (response) {
-    this.updateRequest(response.from, {
+    NetMonitorView.RequestsMenu.updateRequest(response.from, {
       requestPostData: response
     }).then(() => {
       window.emit(EVENTS.RECEIVED_REQUEST_POST_DATA, response.from);
     });
   },
 
   /**
    * Handles additional information received for a "securityInfo" packet.
    *
    * @param object response
    *        The message received from the server.
    */
   _onSecurityInfo: function (response) {
-    this.updateRequest(response.from, {
+    NetMonitorView.RequestsMenu.updateRequest(response.from, {
       securityInfo: response.securityInfo
     }).then(() => {
       window.emit(EVENTS.RECEIVED_SECURITY_INFO, response.from);
     });
   },
 
   /**
    * Handles additional information received for a "responseHeaders" packet.
    *
    * @param object response
    *        The message received from the server.
    */
   _onResponseHeaders: function (response) {
-    this.updateRequest(response.from, {
+    NetMonitorView.RequestsMenu.updateRequest(response.from, {
       responseHeaders: response
     }).then(() => {
       window.emit(EVENTS.RECEIVED_RESPONSE_HEADERS, response.from);
     });
   },
 
   /**
    * Handles additional information received for a "responseCookies" packet.
    *
    * @param object response
    *        The message received from the server.
    */
   _onResponseCookies: function (response) {
-    this.updateRequest(response.from, {
+    NetMonitorView.RequestsMenu.updateRequest(response.from, {
       responseCookies: response
     }).then(() => {
       window.emit(EVENTS.RECEIVED_RESPONSE_COOKIES, response.from);
     });
   },
 
   /**
    * Handles additional information received for a "responseContent" packet.
    *
    * @param object response
    *        The message received from the server.
    */
   _onResponseContent: function (response) {
-    this.updateRequest(response.from, {
+    NetMonitorView.RequestsMenu.updateRequest(response.from, {
       responseContent: response
     }).then(() => {
       window.emit(EVENTS.RECEIVED_RESPONSE_CONTENT, response.from);
     });
   },
 
   /**
    * Handles additional information received for a "eventTimings" packet.
    *
    * @param object response
    *        The message received from the server.
    */
   _onEventTimings: function (response) {
-    this.updateRequest(response.from, {
+    NetMonitorView.RequestsMenu.updateRequest(response.from, {
       eventTimings: response
     }).then(() => {
       window.emit(EVENTS.RECEIVED_EVENT_TIMINGS, response.from);
     });
   },
 
   /**
    * Fetches the full text of a LongString.
--- a/devtools/client/netmonitor/netmonitor-view.js
+++ b/devtools/client/netmonitor/netmonitor-view.js
@@ -1,97 +1,102 @@
 /* This Source Code Form is subject to the terms of the Mozilla Public
  * License, v. 2.0. If a copy of the MPL was not distributed with this
  * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
 
-/* eslint-env browser */
-/* globals gStore, NetMonitorController */
+/* eslint-disable mozilla/reject-some-requires */
+/* globals $, gStore, NetMonitorController */
 
 "use strict";
 
+const { RequestsMenuView } = require("./requests-menu-view");
 const { ACTIVITY_TYPE } = require("./constants");
 const { createFactory } = require("devtools/client/shared/vendor/react");
 const ReactDOM = require("devtools/client/shared/vendor/react-dom");
 const Provider = createFactory(require("devtools/client/shared/vendor/react-redux").Provider);
 
 // Components
 const NetworkDetailsPanel = createFactory(require("./shared/components/network-details-panel"));
-const RequestList = createFactory(require("./components/request-list"));
 const StatisticsPanel = createFactory(require("./components/statistics-panel"));
 const Toolbar = createFactory(require("./components/toolbar"));
 
 /**
  * Object defining the network monitor view components.
  */
-exports.NetMonitorView = {
+var NetMonitorView = {
   /**
    * Initializes the network monitor view.
    */
   initialize: function () {
-    this._body = document.querySelector("#body");
+    this._body = $("#body");
 
-    this.networkDetailsPanel = document.querySelector(
-      "#react-network-details-panel-hook");
+    this.networkDetailsPanel = $("#react-network-details-panel-hook");
+
     ReactDOM.render(Provider(
       { store: gStore },
       NetworkDetailsPanel({ toolbox: NetMonitorController._toolbox }),
     ), this.networkDetailsPanel);
 
-    this.requestList = document.querySelector("#react-request-list-hook");
-    ReactDOM.render(Provider(
-      { store: gStore },
-      RequestList({ toolbox: NetMonitorController._toolbox })
-    ), this.requestList);
+    this.statisticsPanel = $("#react-statistics-panel-hook");
 
-    this.statisticsPanel = document.querySelector("#react-statistics-panel-hook");
     ReactDOM.render(Provider(
       { store: gStore },
       StatisticsPanel(),
     ), this.statisticsPanel);
 
-    this.toolbar = document.querySelector("#react-toolbar-hook");
+    this.toolbar = $("#react-toolbar-hook");
+
     ReactDOM.render(Provider(
       { store: gStore },
       Toolbar(),
     ), this.toolbar);
 
+    this.RequestsMenu.initialize(gStore);
+
     // Store watcher here is for observing the statisticsOpen state change.
     // It should be removed once we migrate to react and apply react/redex binding.
     this.unsubscribeStore = gStore.subscribe(storeWatcher(
       false,
       () => gStore.getState().ui.statisticsOpen,
       this.toggleFrontendMode.bind(this)
     ));
   },
 
   /**
    * Destroys the network monitor view.
    */
   destroy: function () {
+    this.RequestsMenu.destroy();
     ReactDOM.unmountComponentAtNode(this.networkDetailsPanel);
-    ReactDOM.unmountComponentAtNode(this.requestList);
     ReactDOM.unmountComponentAtNode(this.statisticsPanel);
     ReactDOM.unmountComponentAtNode(this.toolbar);
     this.unsubscribeStore();
   },
 
   toggleFrontendMode: function () {
     if (gStore.getState().ui.statisticsOpen) {
-      this._body.selectedPanel = document.querySelector("#react-statistics-panel-hook");
+      this._body.selectedPanel = $("#react-statistics-panel-hook");
       NetMonitorController.triggerActivity(ACTIVITY_TYPE.RELOAD.WITH_CACHE_ENABLED);
     } else {
-      this._body.selectedPanel = document.querySelector("#inspector-panel");
+      this._body.selectedPanel = $("#inspector-panel");
     }
   },
 };
 
 // A smart store watcher to notify store changes as necessary
 function storeWatcher(initialValue, reduceValue, onChange) {
   let currentValue = initialValue;
 
   return () => {
     const newValue = reduceValue();
     if (newValue !== currentValue) {
       onChange();
       currentValue = newValue;
     }
   };
 }
+
+/**
+ * Preliminary setup for the NetMonitorView object.
+ */
+NetMonitorView.RequestsMenu = new RequestsMenuView();
+
+exports.NetMonitorView = NetMonitorView;
--- a/devtools/client/netmonitor/netmonitor.xul
+++ b/devtools/client/netmonitor/netmonitor.xul
@@ -21,17 +21,17 @@
 
     <vbox id="inspector-panel" flex="1">
       <html:div xmlns="http://www.w3.org/1999/xhtml"
                 id="react-toolbar-hook"/>
       <hbox id="network-table-and-sidebar"
             class="devtools-responsive-container"
             flex="1">
         <html:div xmlns="http://www.w3.org/1999/xhtml"
-                  id="react-request-list-hook"
+                  id="network-table"
                   class="devtools-main-content">
         </html:div>
 
         <splitter id="network-inspector-view-splitter"
                   class="devtools-side-splitter"/>
 
         <box id="splitter-adjustable-box"
              hidden="true">
--- a/devtools/client/netmonitor/request-list-context-menu.js
+++ b/devtools/client/netmonitor/request-list-context-menu.js
@@ -1,232 +1,230 @@
 /* This Source Code Form is subject to the terms of the Mozilla Public
  * License, v. 2.0. If a copy of the MPL was not distributed with this
  * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
 
-/* globals NetMonitorController, gNetwork, gStore */
+/* globals NetMonitorController, NetMonitorView, gNetwork */
 
 "use strict";
 
 const Services = require("Services");
 const { Task } = require("devtools/shared/task");
 const { Curl } = require("devtools/client/shared/curl");
 const { gDevTools } = require("devtools/client/framework/devtools");
 const Menu = require("devtools/client/framework/menu");
 const MenuItem = require("devtools/client/framework/menu-item");
 const { L10N } = require("./l10n");
 const {
   formDataURI,
   getFormDataSections,
   getUrlQuery,
   parseQueryString,
 } = require("./request-utils");
-const {
-  getSelectedRequest,
-  getSortedRequests,
-} = require("./selectors/index");
+const Actions = require("./actions/index");
 
 loader.lazyRequireGetter(this, "HarExporter",
   "devtools/client/netmonitor/har/har-exporter", true);
 
 loader.lazyServiceGetter(this, "clipboardHelper",
   "@mozilla.org/widget/clipboardhelper;1", "nsIClipboardHelper");
 
-function RequestListContextMenu({
-  cloneSelectedRequest,
-  openStatistics,
-}) {
-  this.cloneSelectedRequest = cloneSelectedRequest;
-  this.openStatistics = openStatistics;
-}
+function RequestListContextMenu() {}
 
 RequestListContextMenu.prototype = {
-  get selectedRequest() {
-    return getSelectedRequest(gStore.getState());
+  get selectedItem() {
+    return NetMonitorView.RequestsMenu.selectedItem;
   },
 
-  get sortedRequests() {
-    return getSortedRequests(gStore.getState());
+  get items() {
+    return NetMonitorView.RequestsMenu.items;
+  },
+
+  /**
+   * Initialization function, called when the RequestsMenu is initialized.
+   */
+  initialize: function (store) {
+    this.store = store;
   },
 
   /**
    * Handle the context menu opening. Hide items if no request is selected.
    * Since visible attribute only accept boolean value but the method call may
    * return undefined, we use !! to force convert any object to boolean
    */
   open({ screenX = 0, screenY = 0 } = {}) {
-    let selectedRequest = this.selectedRequest;
+    let selectedItem = this.selectedItem;
 
     let menu = new Menu();
     menu.append(new MenuItem({
       id: "request-menu-context-copy-url",
       label: L10N.getStr("netmonitor.context.copyUrl"),
       accesskey: L10N.getStr("netmonitor.context.copyUrl.accesskey"),
-      visible: !!selectedRequest,
+      visible: !!selectedItem,
       click: () => this.copyUrl(),
     }));
 
     menu.append(new MenuItem({
       id: "request-menu-context-copy-url-params",
       label: L10N.getStr("netmonitor.context.copyUrlParams"),
       accesskey: L10N.getStr("netmonitor.context.copyUrlParams.accesskey"),
-      visible: !!(selectedRequest && getUrlQuery(selectedRequest.url)),
+      visible: !!(selectedItem && getUrlQuery(selectedItem.url)),
       click: () => this.copyUrlParams(),
     }));
 
     menu.append(new MenuItem({
       id: "request-menu-context-copy-post-data",
       label: L10N.getStr("netmonitor.context.copyPostData"),
       accesskey: L10N.getStr("netmonitor.context.copyPostData.accesskey"),
-      visible: !!(selectedRequest && selectedRequest.requestPostData),
+      visible: !!(selectedItem && selectedItem.requestPostData),
       click: () => this.copyPostData(),
     }));
 
     menu.append(new MenuItem({
       id: "request-menu-context-copy-as-curl",
       label: L10N.getStr("netmonitor.context.copyAsCurl"),
       accesskey: L10N.getStr("netmonitor.context.copyAsCurl.accesskey"),
-      visible: !!selectedRequest,
+      visible: !!selectedItem,
       click: () => this.copyAsCurl(),
     }));
 
     menu.append(new MenuItem({
       type: "separator",
-      visible: !!selectedRequest,
+      visible: !!selectedItem,
     }));
 
     menu.append(new MenuItem({
       id: "request-menu-context-copy-request-headers",
       label: L10N.getStr("netmonitor.context.copyRequestHeaders"),
       accesskey: L10N.getStr("netmonitor.context.copyRequestHeaders.accesskey"),
-      visible: !!(selectedRequest && selectedRequest.requestHeaders),
+      visible: !!(selectedItem && selectedItem.requestHeaders),
       click: () => this.copyRequestHeaders(),
     }));
 
     menu.append(new MenuItem({
       id: "response-menu-context-copy-response-headers",
       label: L10N.getStr("netmonitor.context.copyResponseHeaders"),
       accesskey: L10N.getStr("netmonitor.context.copyResponseHeaders.accesskey"),
-      visible: !!(selectedRequest && selectedRequest.responseHeaders),
+      visible: !!(selectedItem && selectedItem.responseHeaders),
       click: () => this.copyResponseHeaders(),
     }));
 
     menu.append(new MenuItem({
       id: "request-menu-context-copy-response",
       label: L10N.getStr("netmonitor.context.copyResponse"),
       accesskey: L10N.getStr("netmonitor.context.copyResponse.accesskey"),
-      visible: !!(selectedRequest &&
-               selectedRequest.responseContent &&
-               selectedRequest.responseContent.content.text &&
-               selectedRequest.responseContent.content.text.length !== 0),
+      visible: !!(selectedItem &&
+               selectedItem.responseContent &&
+               selectedItem.responseContent.content.text &&
+               selectedItem.responseContent.content.text.length !== 0),
       click: () => this.copyResponse(),
     }));
 
     menu.append(new MenuItem({
       id: "request-menu-context-copy-image-as-data-uri",
       label: L10N.getStr("netmonitor.context.copyImageAsDataUri"),
       accesskey: L10N.getStr("netmonitor.context.copyImageAsDataUri.accesskey"),
-      visible: !!(selectedRequest &&
-               selectedRequest.responseContent &&
-               selectedRequest.responseContent.content.mimeType.includes("image/")),
+      visible: !!(selectedItem &&
+               selectedItem.responseContent &&
+               selectedItem.responseContent.content.mimeType.includes("image/")),
       click: () => this.copyImageAsDataUri(),
     }));
 
     menu.append(new MenuItem({
       type: "separator",
-      visible: !!selectedRequest,
+      visible: !!selectedItem,
     }));
 
     menu.append(new MenuItem({
       id: "request-menu-context-copy-all-as-har",
       label: L10N.getStr("netmonitor.context.copyAllAsHar"),
       accesskey: L10N.getStr("netmonitor.context.copyAllAsHar.accesskey"),
-      visible: this.sortedRequests.size > 0,
+      visible: this.items.size > 0,
       click: () => this.copyAllAsHar(),
     }));
 
     menu.append(new MenuItem({
       id: "request-menu-context-save-all-as-har",
       label: L10N.getStr("netmonitor.context.saveAllAsHar"),
       accesskey: L10N.getStr("netmonitor.context.saveAllAsHar.accesskey"),
-      visible: this.sortedRequests.size > 0,
+      visible: this.items.size > 0,
       click: () => this.saveAllAsHar(),
     }));
 
     menu.append(new MenuItem({
       type: "separator",
-      visible: !!selectedRequest,
+      visible: !!selectedItem,
     }));
 
     menu.append(new MenuItem({
       id: "request-menu-context-resend",
       label: L10N.getStr("netmonitor.context.editAndResend"),
       accesskey: L10N.getStr("netmonitor.context.editAndResend.accesskey"),
       visible: !!(NetMonitorController.supportsCustomRequest &&
-               selectedRequest && !selectedRequest.isCustom),
-      click: this.cloneSelectedRequest,
+               selectedItem && !selectedItem.isCustom),
+      click: () => NetMonitorView.RequestsMenu.cloneSelectedRequest(),
     }));
 
     menu.append(new MenuItem({
       type: "separator",
-      visible: !!selectedRequest,
+      visible: !!selectedItem,
     }));
 
     menu.append(new MenuItem({
       id: "request-menu-context-newtab",
       label: L10N.getStr("netmonitor.context.newTab"),
       accesskey: L10N.getStr("netmonitor.context.newTab.accesskey"),
-      visible: !!selectedRequest,
+      visible: !!selectedItem,
       click: () => this.openRequestInTab()
     }));
 
     menu.append(new MenuItem({
       id: "request-menu-context-perf",
       label: L10N.getStr("netmonitor.context.perfTools"),
       accesskey: L10N.getStr("netmonitor.context.perfTools.accesskey"),
       visible: !!NetMonitorController.supportsPerfStats,
-      click: () => this.openStatistics(true)
+      click: () => this.store.dispatch(Actions.openStatistics(true))
     }));
 
     menu.popup(screenX, screenY, NetMonitorController._toolbox);
     return menu;
   },
 
   /**
    * Opens selected item in a new tab.
    */
   openRequestInTab() {
     let win = Services.wm.getMostRecentWindow(gDevTools.chromeWindowType);
-    win.openUILinkIn(this.selectedRequest.url, "tab", { relatedToCurrent: true });
+    win.openUILinkIn(this.selectedItem.url, "tab", { relatedToCurrent: true });
   },
 
   /**
    * Copy the request url from the currently selected item.
    */
   copyUrl() {
-    clipboardHelper.copyString(this.selectedRequest.url);
+    clipboardHelper.copyString(this.selectedItem.url);
   },
 
   /**
    * Copy the request url query string parameters from the currently
    * selected item.
    */
   copyUrlParams() {
-    let { url } = this.selectedRequest;
+    let { url } = this.selectedItem;
     let params = getUrlQuery(url).split("&");
     let string = params.join(Services.appinfo.OS === "WINNT" ? "\r\n" : "\n");
     clipboardHelper.copyString(string);
   },
 
   /**
    * Copy the request form data parameters (or raw payload) from
    * the currently selected item.
    */
   copyPostData: Task.async(function* () {
-    let selected = this.selectedRequest;
+    let selected = this.selectedItem;
 
     // Try to extract any form data parameters.
     let formDataSections = yield getFormDataSections(
       selected.requestHeaders,
       selected.requestHeadersFromUploadStream,
       selected.requestPostData,
       gNetwork.getString.bind(gNetwork));
 
@@ -253,17 +251,17 @@ RequestListContextMenu.prototype = {
 
     clipboardHelper.copyString(string);
   }),
 
   /**
    * Copy a cURL command from the currently selected item.
    */
   copyAsCurl: Task.async(function* () {
-    let selected = this.selectedRequest;
+    let selected = this.selectedItem;
 
     // Create a sanitized object for the Curl command generator.
     let data = {
       url: selected.url,
       method: selected.method,
       headers: [],
       httpVersion: selected.httpVersion,
       postDataText: null
@@ -283,51 +281,51 @@ RequestListContextMenu.prototype = {
 
     clipboardHelper.copyString(Curl.generateCommand(data));
   }),
 
   /**
    * Copy the raw request headers from the currently selected item.
    */
   copyRequestHeaders() {
-    let rawHeaders = this.selectedRequest.requestHeaders.rawHeaders.trim();
+    let rawHeaders = this.selectedItem.requestHeaders.rawHeaders.trim();
     if (Services.appinfo.OS !== "WINNT") {
       rawHeaders = rawHeaders.replace(/\r/g, "");
     }
     clipboardHelper.copyString(rawHeaders);
   },
 
   /**
    * Copy the raw response headers from the currently selected item.
    */
   copyResponseHeaders() {
-    let rawHeaders = this.selectedRequest.responseHeaders.rawHeaders.trim();
+    let rawHeaders = this.selectedItem.responseHeaders.rawHeaders.trim();
     if (Services.appinfo.OS !== "WINNT") {
       rawHeaders = rawHeaders.replace(/\r/g, "");
     }
     clipboardHelper.copyString(rawHeaders);
   },
 
   /**
    * Copy image as data uri.
    */
   copyImageAsDataUri() {
-    const { mimeType, text, encoding } = this.selectedRequest.responseContent.content;
+    const { mimeType, text, encoding } = this.selectedItem.responseContent.content;
 
     gNetwork.getString(text).then(string => {
       let data = formDataURI(mimeType, encoding, string);
       clipboardHelper.copyString(data);
     });
   },
 
   /**
    * Copy response data as a string.
    */
   copyResponse() {
-    const { text } = this.selectedRequest.responseContent.content;
+    const { text } = this.selectedItem.responseContent.content;
 
     gNetwork.getString(text).then(string => {
       clipboardHelper.copyString(string);
     });
   },
 
   /**
    * Copy HAR from the network panel content to the clipboard.
@@ -346,15 +344,15 @@ RequestListContextMenu.prototype = {
   },
 
   getDefaultHarOptions() {
     let form = NetMonitorController._target.form;
     let title = form.title || form.url;
 
     return {
       getString: gNetwork.getString.bind(gNetwork),
-      items: this.sortedRequests,
+      items: this.items,
       title: title
     };
   }
 };
 
 module.exports = RequestListContextMenu;
deleted file mode 100644
--- a/devtools/client/netmonitor/request-list-tooltip.js
+++ /dev/null
@@ -1,109 +0,0 @@
-/* This Source Code Form is subject to the terms of the Mozilla Public
- * License, v. 2.0. If a copy of the MPL was not distributed with this
- * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
-
-/* globals gNetwork, NetMonitorController */
-
-"use strict";
-
-const { Task } = require("devtools/shared/task");
-const {
-  setImageTooltip,
-  getImageDimensions,
-} = require("devtools/client/shared/widgets/tooltip/ImageTooltipHelper");
-const { WEBCONSOLE_L10N } = require("./l10n");
-const { formDataURI } = require("./request-utils");
-
-// px
-const REQUESTS_TOOLTIP_IMAGE_MAX_DIM = 400;
-// px
-const REQUESTS_TOOLTIP_STACK_TRACE_WIDTH = 600;
-
-const HTML_NS = "http://www.w3.org/1999/xhtml";
-
-const setTooltipImageContent = Task.async(function* (tooltip, itemEl, requestItem) {
-  let { mimeType, text, encoding } = requestItem.responseContent.content;
-
-  if (!mimeType || !mimeType.includes("image/")) {
-    return false;
-  }
-
-  let string = yield gNetwork.getString(text);
-  let src = formDataURI(mimeType, encoding, string);
-  let maxDim = REQUESTS_TOOLTIP_IMAGE_MAX_DIM;
-  let { naturalWidth, naturalHeight } = yield getImageDimensions(tooltip.doc, src);
-  let options = { maxDim, naturalWidth, naturalHeight };
-  setImageTooltip(tooltip, tooltip.doc, src, options);
-
-  return itemEl.querySelector(".requests-menu-icon");
-});
-
-const setTooltipStackTraceContent = Task.async(function* (tooltip, requestItem) {
-  let {stacktrace} = requestItem.cause;
-
-  if (!stacktrace || stacktrace.length == 0) {
-    return false;
-  }
-
-  let doc = tooltip.doc;
-  let el = doc.createElementNS(HTML_NS, "div");
-  el.className = "stack-trace-tooltip devtools-monospace";
-
-  for (let f of stacktrace) {
-    let { functionName, filename, lineNumber, columnNumber, asyncCause } = f;
-
-    if (asyncCause) {
-      // if there is asyncCause, append a "divider" row into the trace
-      let asyncFrameEl = doc.createElementNS(HTML_NS, "div");
-      asyncFrameEl.className = "stack-frame stack-frame-async";
-      asyncFrameEl.textContent =
-        WEBCONSOLE_L10N.getFormatStr("stacktrace.asyncStack", asyncCause);
-      el.appendChild(asyncFrameEl);
-    }
-
-    // Parse a source name in format "url -> url"
-    let sourceUrl = filename.split(" -> ").pop();
-
-    let frameEl = doc.createElementNS(HTML_NS, "div");
-    frameEl.className = "stack-frame stack-frame-call";
-
-    let funcEl = doc.createElementNS(HTML_NS, "span");
-    funcEl.className = "stack-frame-function-name";
-    funcEl.textContent =
-      functionName || WEBCONSOLE_L10N.getStr("stacktrace.anonymousFunction");
-    frameEl.appendChild(funcEl);
-
-    let sourceEl = doc.createElementNS(HTML_NS, "span");
-    sourceEl.className = "stack-frame-source-name";
-    frameEl.appendChild(sourceEl);
-
-    let sourceInnerEl = doc.createElementNS(HTML_NS, "span");
-    sourceInnerEl.className = "stack-frame-source-name-inner";
-    sourceEl.appendChild(sourceInnerEl);
-
-    sourceInnerEl.textContent = sourceUrl;
-    sourceInnerEl.title = sourceUrl;
-
-    let lineEl = doc.createElementNS(HTML_NS, "span");
-    lineEl.className = "stack-frame-line";
-    lineEl.textContent = `:${lineNumber}:${columnNumber}`;
-    sourceInnerEl.appendChild(lineEl);
-
-    frameEl.addEventListener("click", () => {
-      // hide the tooltip immediately, not after delay
-      tooltip.hide();
-      NetMonitorController.viewSourceInDebugger(filename, lineNumber);
-    });
-
-    el.appendChild(frameEl);
-  }
-
-  tooltip.setContent(el, {width: REQUESTS_TOOLTIP_STACK_TRACE_WIDTH});
-
-  return true;
-});
-
-module.exports = {
-  setTooltipImageContent,
-  setTooltipStackTraceContent,
-};
new file mode 100644
--- /dev/null
+++ b/devtools/client/netmonitor/requests-menu-view.js
@@ -0,0 +1,410 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+/* globals window, dumpn, $, gNetwork, NetMonitorController */
+
+"use strict";
+
+const { Task } = require("devtools/shared/task");
+const { HTMLTooltip } = require("devtools/client/shared/widgets/tooltip/HTMLTooltip");
+const { setNamedTimeout } = require("devtools/client/shared/widgets/view-helpers");
+const { CurlUtils } = require("devtools/client/shared/curl");
+const { L10N } = require("./l10n");
+const { EVENTS } = require("./events");
+const { createElement, createFactory } = require("devtools/client/shared/vendor/react");
+const ReactDOM = require("devtools/client/shared/vendor/react-dom");
+const { Provider } = require("devtools/client/shared/vendor/react-redux");
+const RequestList = createFactory(require("./components/request-list"));
+const RequestListContextMenu = require("./request-list-context-menu");
+const Actions = require("./actions/index");
+const { Prefs } = require("./prefs");
+
+const {
+  fetchHeaders,
+  formDataURI,
+  getFormDataSections,
+} = require("./request-utils");
+
+const {
+  getActiveFilters,
+  getDisplayedRequests,
+  getRequestById,
+  getSelectedRequest,
+  getSortedRequests,
+} = require("./selectors/index");
+
+// ms
+const RESIZE_REFRESH_RATE = 50;
+
+// A smart store watcher to notify store changes as necessary
+function storeWatcher(initialValue, reduceValue, onChange) {
+  let currentValue = initialValue;
+
+  return () => {
+    const oldValue = currentValue;
+    const newValue = reduceValue(currentValue);
+    if (newValue !== oldValue) {
+      currentValue = newValue;
+      onChange(newValue, oldValue);
+    }
+  };
+}
+
+/**
+ * Functions handling the requests menu (containing details about each request,
+ * like status, method, file, domain, as well as a waterfall representing
+ * timing information).
+ */
+function RequestsMenuView() {
+  dumpn("RequestsMenuView was instantiated");
+}
+
+RequestsMenuView.prototype = {
+  /**
+   * Initialization function, called when the network monitor is started.
+   */
+  initialize: function (store) {
+    dumpn("Initializing the RequestsMenuView");
+
+    this.store = store;
+
+    this.contextMenu = new RequestListContextMenu();
+    this.contextMenu.initialize(store);
+
+    Prefs.filters.forEach(type => store.dispatch(Actions.toggleRequestFilterType(type)));
+
+    // Watch selection changes
+    this.store.subscribe(storeWatcher(
+      null,
+      () => getSelectedRequest(this.store.getState()),
+      (newSelected, oldSelected) => this.onSelectionUpdate(newSelected, oldSelected)
+    ));
+
+    // Watch the network details panel toggle and resize the waterfall column on change
+    this.store.subscribe(storeWatcher(
+      false,
+      () => this.store.getState().ui.networkDetailsOpen,
+      () => this.onResize()
+    ));
+
+    // Watch the requestHeaders, requestHeadersFromUploadStream and requestPostData
+    // in order to update formDataSections for composing form data
+    this.store.subscribe(storeWatcher(
+      false,
+      (currentRequest) => {
+        const request = getSelectedRequest(this.store.getState());
+        if (!request) {
+          return {};
+        }
+
+        const isChanged = request.requestHeaders !== currentRequest.requestHeaders ||
+        request.requestHeadersFromUploadStream !==
+        currentRequest.requestHeadersFromUploadStream ||
+        request.requestPostData !== currentRequest.requestPostData;
+
+        if (isChanged) {
+          return {
+            id: request.id,
+            requestHeaders: request.requestHeaders,
+            requestHeadersFromUploadStream: request.requestHeadersFromUploadStream,
+            requestPostData: request.requestPostData,
+          };
+        }
+
+        return currentRequest;
+      },
+      (newRequest) => {
+        const {
+          id,
+          requestHeaders,
+          requestHeadersFromUploadStream,
+          requestPostData,
+        } = newRequest;
+
+        if (requestHeaders && requestHeadersFromUploadStream && requestPostData) {
+          getFormDataSections(
+            requestHeaders,
+            requestHeadersFromUploadStream,
+            requestPostData,
+            gNetwork.getString.bind(gNetwork),
+          ).then((formDataSections) => {
+            this.store.dispatch(Actions.updateRequest(
+              id,
+              { formDataSections },
+              true,
+            ));
+          });
+        }
+      },
+    ));
+
+    this._summary = $("#requests-menu-network-summary-button");
+    this._summary.setAttribute("label", L10N.getStr("networkMenu.empty"));
+
+    this.onResize = this.onResize.bind(this);
+    this._splitter = $("#network-inspector-view-splitter");
+    this._splitter.addEventListener("mouseup", this.onResize);
+    window.addEventListener("resize", this.onResize);
+
+    this.tooltip = new HTMLTooltip(NetMonitorController._toolbox.doc, { type: "arrow" });
+
+    this.mountPoint = $("#network-table");
+    ReactDOM.render(createElement(Provider,
+      { store: this.store },
+      RequestList()
+    ), this.mountPoint);
+  },
+
+  /**
+   * Destruction function, called when the network monitor is closed.
+   */
+  destroy() {
+    dumpn("Destroying the RequestsMenuView");
+
+    Prefs.filters = getActiveFilters(this.store.getState());
+
+    this._splitter.removeEventListener("mouseup", this.onResize);
+    window.removeEventListener("resize", this.onResize);
+
+    this.tooltip.destroy();
+
+    ReactDOM.unmountComponentAtNode(this.mountPoint);
+  },
+
+  /**
+   * Resets this container (removes all the networking information).
+   */
+  reset() {
+    this.store.dispatch(Actions.batchReset());
+    this.store.dispatch(Actions.clearRequests());
+  },
+
+  /**
+   * Removes all network requests and closes the network details panel if open.
+   */
+  clear() {
+    this.store.dispatch(Actions.clearRequests());
+  },
+
+  addRequest(id, data) {
+    let { method, url, isXHR, cause, startedDateTime, fromCache,
+          fromServiceWorker } = data;
+
+    // Convert the received date/time string to a unix timestamp.
+    let startedMillis = Date.parse(startedDateTime);
+
+    const action = Actions.addRequest(
+      id,
+      {
+        startedMillis,
+        method,
+        url,
+        isXHR,
+        cause,
+        fromCache,
+        fromServiceWorker,
+      },
+      true
+    );
+
+    this.store.dispatch(action).then(() => window.emit(EVENTS.REQUEST_ADDED, action.id));
+  },
+
+  updateRequest: Task.async(function* (id, data) {
+    const action = Actions.updateRequest(id, data, true);
+    yield this.store.dispatch(action);
+    let {
+      responseContent,
+      responseCookies,
+      responseHeaders,
+      requestCookies,
+      requestHeaders,
+      requestPostData,
+    } = action.data;
+    let request = getRequestById(this.store.getState(), action.id);
+
+    if (requestHeaders && requestHeaders.headers && requestHeaders.headers.length) {
+      let headers = yield fetchHeaders(
+        requestHeaders, gNetwork.getString.bind(gNetwork));
+      if (headers) {
+        yield this.store.dispatch(Actions.updateRequest(
+          action.id,
+          { requestHeaders: headers },
+          true,
+        ));
+      }
+    }
+
+    if (responseHeaders && responseHeaders.headers && responseHeaders.headers.length) {
+      let headers = yield fetchHeaders(
+        responseHeaders, gNetwork.getString.bind(gNetwork));
+      if (headers) {
+        yield this.store.dispatch(Actions.updateRequest(
+          action.id,
+          { responseHeaders: headers },
+          true,
+        ));
+      }
+    }
+
+    if (request && responseContent && responseContent.content) {
+      let { mimeType } = request;
+      let { text, encoding } = responseContent.content;
+      let response = yield gNetwork.getString(text);
+      let payload = {};
+
+      if (mimeType.includes("image/")) {
+        payload.responseContentDataUri = formDataURI(mimeType, encoding, response);
+      }
+
+      responseContent.content.text = response;
+      payload.responseContent = responseContent;
+
+      yield this.store.dispatch(Actions.updateRequest(action.id, payload, true));
+
+      if (mimeType.includes("image/")) {
+        window.emit(EVENTS.RESPONSE_IMAGE_THUMBNAIL_DISPLAYED);
+      }
+    }
+
+    // Search the POST data upload stream for request headers and add
+    // them as a separate property, different from the classic headers.
+    if (requestPostData && requestPostData.postData) {
+      let { text } = requestPostData.postData;
+      let postData = yield gNetwork.getString(text);
+      const headers = CurlUtils.getHeadersFromMultipartText(postData);
+      const headersSize = headers.reduce((acc, { name, value }) => {
+        return acc + name.length + value.length + 2;
+      }, 0);
+      let payload = {};
+      requestPostData.postData.text = postData;
+      payload.requestPostData = Object.assign({}, requestPostData);
+      payload.requestHeadersFromUploadStream = { headers, headersSize };
+
+      yield this.store.dispatch(Actions.updateRequest(action.id, payload, true));
+    }
+
+    // Fetch request and response cookies long value.
+    // Actor does not provide full sized cookie value when the value is too long
+    // To display values correctly, we need fetch them in each request.
+    if (requestCookies) {
+      let reqCookies = [];
+      // request store cookies in requestCookies or requestCookies.cookies
+      let cookies = requestCookies.cookies ?
+        requestCookies.cookies : requestCookies;
+      // make sure cookies is iterable
+      if (typeof cookies[Symbol.iterator] === "function") {
+        for (let cookie of cookies) {
+          reqCookies.push(Object.assign({}, cookie, {
+            value: yield gNetwork.getString(cookie.value),
+          }));
+        }
+        if (reqCookies.length) {
+          yield this.store.dispatch(Actions.updateRequest(
+            action.id,
+            { requestCookies: reqCookies },
+            true));
+        }
+      }
+    }
+
+    if (responseCookies) {
+      let resCookies = [];
+      // response store cookies in responseCookies or responseCookies.cookies
+      let cookies = responseCookies.cookies ?
+        responseCookies.cookies : responseCookies;
+      // make sure cookies is iterable
+      if (typeof cookies[Symbol.iterator] === "function") {
+        for (let cookie of cookies) {
+          resCookies.push(Object.assign({}, cookie, {
+            value: yield gNetwork.getString(cookie.value),
+          }));
+        }
+        if (resCookies.length) {
+          yield this.store.dispatch(Actions.updateRequest(
+            action.id,
+            { responseCookies: resCookies },
+            true));
+        }
+      }
+    }
+  }),
+
+  /**
+   * Disable batched updates. Used by tests.
+   */
+  set lazyUpdate(value) {
+    this.store.dispatch(Actions.batchEnable(value));
+  },
+
+  get items() {
+    return getSortedRequests(this.store.getState());
+  },
+
+  get visibleItems() {
+    return getDisplayedRequests(this.store.getState());
+  },
+
+  get itemCount() {
+    return this.store.getState().requests.requests.size;
+  },
+
+  getItemAtIndex(index) {
+    return getSortedRequests(this.store.getState()).get(index);
+  },
+
+  get selectedIndex() {
+    const state = this.store.getState();
+    if (!state.requests.selectedId) {
+      return -1;
+    }
+    return getSortedRequests(state).findIndex(r => r.id === state.requests.selectedId);
+  },
+
+  set selectedIndex(index) {
+    const requests = getSortedRequests(this.store.getState());
+    let itemId = null;
+    if (index >= 0 && index < requests.size) {
+      itemId = requests.get(index).id;
+    }
+    this.store.dispatch(Actions.selectRequest(itemId));
+  },
+
+  get selectedItem() {
+    return getSelectedRequest(this.store.getState());
+  },
+
+  set selectedItem(item) {
+    this.store.dispatch(Actions.selectRequest(item ? item.id : null));
+  },
+
+  /**
+   * Updates the network details panel state when something about the selection changes
+   */
+  onSelectionUpdate(newSelected, oldSelected) {
+    if (newSelected) {
+      // Another item just got selected
+      this.store.dispatch(Actions.openNetworkDetails(true));
+    } else {
+      // Selection just got empty
+      this.store.dispatch(Actions.openNetworkDetails(false));
+    }
+  },
+
+  /**
+   * The resize listener for this container's window.
+   */
+  onResize() {
+    // Allow requests to settle down first.
+    setNamedTimeout("resize-events", RESIZE_REFRESH_RATE, () => {
+      const waterfallHeaderEl = $("#requests-menu-waterfall-header-box");
+      if (waterfallHeaderEl) {
+        const { width } = waterfallHeaderEl.getBoundingClientRect();
+        this.store.dispatch(Actions.resizeWaterfall(width));
+      }
+    });
+  }
+};
+
+exports.RequestsMenuView = RequestsMenuView;
--- a/devtools/client/netmonitor/test/browser.ini
+++ b/devtools/client/netmonitor/test/browser.ini
@@ -143,21 +143,24 @@ skip-if = true # bug 1309183, it should 
 [browser_net_security-state.js]
 [browser_net_security-tab-deselect.js]
 [browser_net_security-tab-visibility.js]
 [browser_net_security-warnings.js]
 [browser_net_send-beacon.js]
 [browser_net_send-beacon-other-tab.js]
 [browser_net_simple-init.js]
 [browser_net_simple-request-data.js]
+skip-if = true # Bug 1258809
 [browser_net_simple-request-details.js]
 skip-if = true # Bug 1258809
 [browser_net_simple-request.js]
 [browser_net_sort-01.js]
+skip-if = true # Redundant for React/Redux version
 [browser_net_sort-02.js]
+[browser_net_sort-03.js]
 [browser_net_statistics-01.js]
 [browser_net_statistics-02.js]
 [browser_net_status-codes.js]
 [browser_net_streaming-response.js]
 [browser_net_throttle.js]
 [browser_net_timeline_ticks.js]
 skip-if = true # TODO: fix the test
 [browser_net_timing-division.js]
--- a/devtools/client/netmonitor/test/browser_net_accessibility-01.js
+++ b/devtools/client/netmonitor/test/browser_net_accessibility-01.js
@@ -9,27 +9,28 @@
 
 add_task(function* () {
   let { tab, monitor } = yield initNetMonitor(CUSTOM_GET_URL);
   info("Starting test... ");
 
   // It seems that this test may be slow on Ubuntu builds running on ec2.
   requestLongerTimeout(2);
 
-  let { document, gStore, windowRequire } = monitor.panelWin;
+  let { document, NetMonitorView, gStore, windowRequire } = monitor.panelWin;
+  let { RequestsMenu } = NetMonitorView;
+
+  RequestsMenu.lazyUpdate = false;
+
   let Actions = windowRequire("devtools/client/netmonitor/actions/index");
 
-  gStore.dispatch(Actions.batchEnable(false));
-
   let count = 0;
   function check(selectedIndex, panelVisibility) {
     info("Performing check " + (count++) + ".");
 
-    let requestItems = Array.from(document.querySelectorAll(".request-list-item"));
-    is(requestItems.findIndex((item) => item.matches(".selected")), selectedIndex,
+    is(RequestsMenu.selectedIndex, selectedIndex,
       "The selected item in the requests menu was incorrect.");
     is(!!document.querySelector(".network-details-panel"), panelVisibility,
       "The network details panel should render correctly.");
   }
 
   let wait = waitForNetworkEvents(monitor, 2);
   yield ContentTask.spawn(tab.linkedBrowser, {}, function* () {
     content.wrappedJSObject.performRequests(2);
--- a/devtools/client/netmonitor/test/browser_net_accessibility-02.js
+++ b/devtools/client/netmonitor/test/browser_net_accessibility-02.js
@@ -9,27 +9,26 @@
 
 add_task(function* () {
   let { tab, monitor } = yield initNetMonitor(CUSTOM_GET_URL);
   info("Starting test... ");
 
   // It seems that this test may be slow on Ubuntu builds running on ec2.
   requestLongerTimeout(2);
 
-  let { window, document, gStore, windowRequire } = monitor.panelWin;
-  let Actions = windowRequire("devtools/client/netmonitor/actions/index");
+  let { window, document, NetMonitorView } = monitor.panelWin;
+  let { RequestsMenu } = NetMonitorView;
 
-  gStore.dispatch(Actions.batchEnable(false));
+  RequestsMenu.lazyUpdate = false;
 
   let count = 0;
   function check(selectedIndex, panelVisibility) {
     info("Performing check " + (count++) + ".");
 
-    let requestItems = Array.from(document.querySelectorAll(".request-list-item"));
-    is(requestItems.findIndex((item) => item.matches(".selected")), selectedIndex,
+    is(RequestsMenu.selectedIndex, selectedIndex,
       "The selected item in the requests menu was incorrect.");
     is(!!document.querySelector(".network-details-panel"), panelVisibility,
       "The network details panel should render correctly.");
   }
 
   let wait = waitForNetworkEvents(monitor, 2);
   yield ContentTask.spawn(tab.linkedBrowser, {}, function* () {
     content.wrappedJSObject.performRequests(2);
--- a/devtools/client/netmonitor/test/browser_net_api-calls.js
+++ b/devtools/client/netmonitor/test/browser_net_api-calls.js
@@ -7,43 +7,33 @@
  * Tests whether API call URLs (without a filename) are correctly displayed
  * (including Unicode)
  */
 
 add_task(function* () {
   let { tab, monitor } = yield initNetMonitor(API_CALLS_URL);
   info("Starting test... ");
 
-  let { document, gStore, windowRequire } = monitor.panelWin;
-  let Actions = windowRequire("devtools/client/netmonitor/actions/index");
-  let {
-    getDisplayedRequests,
-    getSortedRequests,
-  } = windowRequire("devtools/client/netmonitor/selectors/index");
+  let { NetMonitorView } = monitor.panelWin;
+  let { RequestsMenu } = NetMonitorView;
 
-  gStore.dispatch(Actions.batchEnable(false));
+  RequestsMenu.lazyUpdate = false;
 
   const REQUEST_URIS = [
     "http://example.com/api/fileName.xml",
     "http://example.com/api/file%E2%98%A2.xml",
     "http://example.com/api/ascii/get/",
     "http://example.com/api/unicode/%E2%98%A2/",
     "http://example.com/api/search/?q=search%E2%98%A2"
   ];
 
   let wait = waitForNetworkEvents(monitor, 5);
   yield ContentTask.spawn(tab.linkedBrowser, {}, function* () {
     content.wrappedJSObject.performRequests();
   });
   yield wait;
 
   REQUEST_URIS.forEach(function (uri, index) {
-    verifyRequestItemTarget(
-      document,
-      getDisplayedRequests(gStore.getState()),
-      getSortedRequests(gStore.getState()).get(index),
-      "GET",
-      uri
-     );
+    verifyRequestItemTarget(RequestsMenu, RequestsMenu.getItemAtIndex(index), "GET", uri);
   });
 
   yield teardown(monitor);
 });
--- a/devtools/client/netmonitor/test/browser_net_autoscroll.js
+++ b/devtools/client/netmonitor/test/browser_net_autoscroll.js
@@ -5,23 +5,22 @@
 
 /**
  * Bug 863102 - Automatically scroll down upon new network requests.
  */
 add_task(function* () {
   requestLongerTimeout(2);
 
   let { monitor } = yield initNetMonitor(INFINITE_GET_URL);
-  let { document, gStore, windowRequire } = monitor.panelWin;
-  let Actions = windowRequire("devtools/client/netmonitor/actions/index");
+  let { $ } = monitor.panelWin;
 
   // Wait until the first request makes the empty notice disappear
   yield waitForRequestListToAppear();
 
-  let requestsContainer = document.querySelector(".requests-menu-contents");
+  let requestsContainer = $(".requests-menu-contents");
   ok(requestsContainer, "Container element exists as expected.");
 
   // (1) Check that the scroll position is maintained at the bottom
   // when the requests overflow the vertical size of the container.
   yield waitForRequestsToOverflowContainer();
   yield waitForScroll();
   ok(true, "Scrolled to bottom on overflow.");
 
@@ -42,36 +41,34 @@ add_task(function* () {
   requestsContainer.scrollTop = requestsContainer.scrollHeight;
   ok(scrolledToBottom(requestsContainer), "Set scroll position to bottom.");
   yield waitForNetworkEvents(monitor, 8);
   yield waitForScroll();
   ok(true, "Still scrolled to bottom.");
 
   // (4) Now select an item in the list and check that additional requests
   // do not change the scroll position.
-  gStore.dispatch(Actions.selectRequestByIndex(0));
+  monitor.panelWin.NetMonitorView.RequestsMenu.selectedIndex = 0;
   yield waitForNetworkEvents(monitor, 8);
   yield waitSomeTime();
   is(requestsContainer.scrollTop, 0, "Did not scroll.");
 
   // Done: clean up.
   return teardown(monitor);
 
   function waitForRequestListToAppear() {
     info("Waiting until the empty notice disappears and is replaced with the list");
-    return waitUntil(() => !!document.querySelector(".requests-menu-contents"));
+    return waitUntil(() => !!$(".requests-menu-contents"));
   }
 
   function* waitForRequestsToOverflowContainer() {
     info("Waiting for enough requests to overflow the container");
     while (true) {
       info("Waiting for one network request");
       yield waitForNetworkEvents(monitor, 1);
-      console.log(requestsContainer.scrollHeight);
-      console.log(requestsContainer.clientHeight)
       if (requestsContainer.scrollHeight > requestsContainer.clientHeight) {
         info("The list is long enough, returning");
         return;
       }
     }
   }
 
   function scrolledToBottom(element) {
--- a/devtools/client/netmonitor/test/browser_net_brotli.js
+++ b/devtools/client/netmonitor/test/browser_net_brotli.js
@@ -11,50 +11,42 @@ const BROTLI_REQUESTS = 1;
  */
 
 add_task(function* () {
   let { L10N } = require("devtools/client/netmonitor/l10n");
 
   let { tab, monitor } = yield initNetMonitor(BROTLI_URL);
   info("Starting test... ");
 
-  let { document, gStore, windowRequire } = monitor.panelWin;
-  let Actions = windowRequire("devtools/client/netmonitor/actions/index");
-  let {
-    getDisplayedRequests,
-    getSortedRequests,
-  } = windowRequire("devtools/client/netmonitor/selectors/index");
+  let { document, NetMonitorView } = monitor.panelWin;
+  let { RequestsMenu } = NetMonitorView;
 
-  gStore.dispatch(Actions.batchEnable(false));
+  RequestsMenu.lazyUpdate = false;
 
   let wait = waitForNetworkEvents(monitor, BROTLI_REQUESTS);
   yield ContentTask.spawn(tab.linkedBrowser, {}, function* () {
     content.wrappedJSObject.performRequests();
   });
   yield wait;
 
-  verifyRequestItemTarget(
-    document,
-    getDisplayedRequests(gStore.getState()),
-    getSortedRequests(gStore.getState()).get(0),
+  verifyRequestItemTarget(RequestsMenu, RequestsMenu.getItemAtIndex(0),
     "GET", HTTPS_CONTENT_TYPE_SJS + "?fmt=br", {
       status: 200,
       statusText: "Connected",
       type: "plain",
       fullMimeType: "text/plain",
       transferred: L10N.getFormatStrWithNumbers("networkMenu.sizeB", 10),
       size: L10N.getFormatStrWithNumbers("networkMenu.sizeB", 64),
       time: true
     });
 
   wait = waitForDOM(document, "#response-panel .editor-mount iframe");
-  EventUtils.sendMouseEvent({ type: "click" },
+  EventUtils.sendMouseEvent({ type: "mousedown" },
     document.querySelector(".network-details-panel-toggle"));
-  EventUtils.sendMouseEvent({ type: "click" },
-    document.querySelector("#response-tab"));
+  document.querySelector("#response-tab").click();
   let [editorFrame] = yield wait;
 
   yield once(editorFrame, "DOMContentLoaded");
   yield waitForDOM(editorFrame.contentDocument, ".CodeMirror-code");
   yield testResponse("br");
 
   yield teardown(monitor);
 
--- a/devtools/client/netmonitor/test/browser_net_cached-status.js
+++ b/devtools/client/netmonitor/test/browser_net_cached-status.js
@@ -6,24 +6,20 @@
 /**
  * Tests if cached requests have the correct status code
  */
 
 add_task(function* () {
   let { tab, monitor } = yield initNetMonitor(STATUS_CODES_URL, null, true);
   info("Starting test... ");
 
-  let { document, gStore, windowRequire } = monitor.panelWin;
-  let Actions = windowRequire("devtools/client/netmonitor/actions/index");
-  let {
-    getDisplayedRequests,
-    getSortedRequests,
-  } = windowRequire("devtools/client/netmonitor/selectors/index");
+  let { NetMonitorView } = monitor.panelWin;
+  let { RequestsMenu, NetworkDetails } = NetMonitorView;
 
-  gStore.dispatch(Actions.batchEnable(false));
+  RequestsMenu.lazyUpdate = false;
 
   const REQUEST_DATA = [
     {
       method: "GET",
       uri: STATUS_CODES_SJS + "?sts=ok&cached",
       details: {
         status: 200,
         statusText: "OK",
@@ -88,25 +84,21 @@ add_task(function* () {
   info("Performing requests #1...");
   yield performRequestsAndWait();
 
   info("Performing requests #2...");
   yield performRequestsAndWait();
 
   let index = 0;
   for (let request of REQUEST_DATA) {
+    let item = RequestsMenu.getItemAtIndex(index);
+
     info("Verifying request #" + index);
-    yield verifyRequestItemTarget(
-      document,
-      getDisplayedRequests(gStore.getState()),
-      getSortedRequests(gStore.getState()).get(index),
-      request.method,
-      request.uri,
-      request.details
-    );
+    yield verifyRequestItemTarget(RequestsMenu, item,
+      request.method, request.uri, request.details);
 
     index++;
   }
 
   yield teardown(monitor);
 
   function* performRequestsAndWait() {
     let wait = waitForNetworkEvents(monitor, 3);
--- a/devtools/client/netmonitor/test/browser_net_cause.js
+++ b/devtools/client/netmonitor/test/browser_net_cause.js
@@ -83,43 +83,33 @@ add_task(function* () {
 
   // the initNetMonitor function clears the network request list after the
   // page is loaded. That's why we first load a bogus page from SIMPLE_URL,
   // and only then load the real thing from CAUSE_URL - we want to catch
   // all the requests the page is making, not only the XHRs.
   // We can't use about:blank here, because initNetMonitor checks that the
   // page has actually made at least one request.
   let { tab, monitor } = yield initNetMonitor(SIMPLE_URL);
+  let { $, NetMonitorView } = monitor.panelWin;
+  let { RequestsMenu } = NetMonitorView;
+  RequestsMenu.lazyUpdate = false;
 
-  let { document, gStore, windowRequire } = monitor.panelWin;
-  let Actions = windowRequire("devtools/client/netmonitor/actions/index");
-  let {
-    getDisplayedRequests,
-    getSortedRequests,
-  } = windowRequire("devtools/client/netmonitor/selectors/index");
-
-  gStore.dispatch(Actions.batchEnable(false));
   let wait = waitForNetworkEvents(monitor, EXPECTED_REQUESTS.length);
   tab.linkedBrowser.loadURI(CAUSE_URL);
   yield wait;
 
-  is(gStore.getState().requests.requests.size, EXPECTED_REQUESTS.length,
+  is(RequestsMenu.itemCount, EXPECTED_REQUESTS.length,
     "All the page events should be recorded.");
 
   EXPECTED_REQUESTS.forEach((spec, i) => {
     let { method, url, causeType, causeUri, stack } = spec;
 
-    let requestItem = getSortedRequests(gStore.getState()).get(i);
-    verifyRequestItemTarget(
-      document,
-      getDisplayedRequests(gStore.getState()),
-      requestItem,
-      method,
-      url,
-      { cause: { type: causeType, loadingDocumentUri: causeUri } }
+    let requestItem = RequestsMenu.getItemAtIndex(i);
+    verifyRequestItemTarget(RequestsMenu, requestItem,
+      method, url, { cause: { type: causeType, loadingDocumentUri: causeUri } }
     );
 
     let { stacktrace } = requestItem.cause;
     let stackLen = stacktrace ? stacktrace.length : 0;
 
     if (stack) {
       ok(stacktrace, `Request #${i} has a stacktrace`);
       ok(stackLen > 0,
@@ -139,18 +129,17 @@ add_task(function* () {
         });
       }
     } else {
       is(stackLen, 0, `Request #${i} (${causeType}) has an empty stacktrace`);
     }
   });
 
   // Sort the requests by cause and check the order
-  EventUtils.sendMouseEvent({ type: "click" },
-    document.querySelector("#requests-menu-cause-button"));
+  EventUtils.sendMouseEvent({ type: "click" }, $("#requests-menu-cause-button"));
   let expectedOrder = EXPECTED_REQUESTS.map(r => r.causeType).sort();
   expectedOrder.forEach((expectedCause, i) => {
-    const cause = getSortedRequests(gStore.getState()).get(i).cause.type;
+    const cause = RequestsMenu.getItemAtIndex(i).cause.type;
     is(cause, expectedCause, `The request #${i} has the expected cause after sorting`);
   });
 
   yield teardown(monitor);
 });
--- a/devtools/client/netmonitor/test/browser_net_cause_redirect.js
+++ b/devtools/client/netmonitor/test/browser_net_cause_redirect.js
@@ -14,28 +14,25 @@ add_task(function* () {
     { status: 302, hasStack: true },
     // Serves HTTPS, sets the Strict-Transport-Security header, no stack
     { status: 200, hasStack: false },
     // Second request to HTTP redirects to HTTPS internally
     { status: 200, hasStack: true },
   ];
 
   let { tab, monitor } = yield initNetMonitor(CUSTOM_GET_URL);
-  let { gStore, windowRequire } = monitor.panelWin;
-  let Actions = windowRequire("devtools/client/netmonitor/actions/index");
-  let { getSortedRequests } = windowRequire("devtools/client/netmonitor/selectors/index");
-
-  gStore.dispatch(Actions.batchEnable(false));
+  let { RequestsMenu } = monitor.panelWin.NetMonitorView;
+  RequestsMenu.lazyUpdate = false;
 
   let wait = waitForNetworkEvents(monitor, EXPECTED_REQUESTS.length);
   yield performRequests(2, HSTS_SJS);
   yield wait;
 
   EXPECTED_REQUESTS.forEach(({status, hasStack}, i) => {
-    let item = getSortedRequests(gStore.getState()).get(i);
+    let item = RequestsMenu.getItemAtIndex(i);
 
     is(item.status, status, `Request #${i} has the expected status`);
 
     let { stacktrace } = item.cause;
     let stackLen = stacktrace ? stacktrace.length : 0;
 
     if (hasStack) {
       ok(stacktrace, `Request #${i} has a stacktrace`);
--- a/devtools/client/netmonitor/test/browser_net_clear.js
+++ b/devtools/client/netmonitor/test/browser_net_clear.js
@@ -6,74 +6,72 @@
 /**
  * Tests if the clear button empties the request menu.
  */
 
 add_task(function* () {
   let { tab, monitor } = yield initNetMonitor(SIMPLE_URL);
   info("Starting test... ");
 
-  let { document, gStore, windowRequire } = monitor.panelWin;
-  let Actions = windowRequire("devtools/client/netmonitor/actions/index");
-  let { EVENTS } = windowRequire("devtools/client/netmonitor/events");
-  let detailsPane = document.querySelector("#details-pane");
+  let { document, NetMonitorView } = monitor.panelWin;
+  let { RequestsMenu } = NetMonitorView;
   let detailsPanelToggleButton = document.querySelector(".network-details-panel-toggle");
   let clearButton = document.querySelector("#requests-menu-clear-button");
 
-  gStore.dispatch(Actions.batchEnable(false));
+  RequestsMenu.lazyUpdate = false;
 
   // Make sure we start in a sane state
-  assertNoRequestState();
+  assertNoRequestState(RequestsMenu, detailsPanelToggleButton);
 
   // Load one request and assert it shows up in the list
-  let networkEvent = monitor.panelWin.once(EVENTS.NETWORK_EVENT);
+  let networkEvent = monitor.panelWin.once(monitor.panelWin.EVENTS.NETWORK_EVENT);
   tab.linkedBrowser.reload();
   yield networkEvent;
 
   assertSingleRequestState();
 
   // Click clear and make sure the requests are gone
   EventUtils.sendMouseEvent({ type: "click" }, clearButton);
   assertNoRequestState();
 
   // Load a second request and make sure they still show up
-  networkEvent = monitor.panelWin.once(EVENTS.NETWORK_EVENT);
+  networkEvent = monitor.panelWin.once(monitor.panelWin.EVENTS.NETWORK_EVENT);
   tab.linkedBrowser.reload();
   yield networkEvent;
 
   assertSingleRequestState();
 
   // Make sure we can now open the network details panel
-  EventUtils.sendMouseEvent({ type: "click" }, detailsPanelToggleButton);
+  EventUtils.sendMouseEvent({ type: "mousedown" }, detailsPanelToggleButton);
 
   ok(document.querySelector(".network-details-panel") &&
     !detailsPanelToggleButton.classList.contains("pane-collapsed"),
     "The details pane should be visible after clicking the toggle button.");
 
   // Click clear and make sure the details pane closes
   EventUtils.sendMouseEvent({ type: "click" }, clearButton);
-
   assertNoRequestState();
-  ok(!document.querySelector(".network-details-panel"),
+  ok(!document.querySelector(".network-details-panel") &&
+    detailsPanelToggleButton.classList.contains("pane-collapsed"),
     "The details pane should not be visible clicking 'clear'.");
 
   return teardown(monitor);
 
   /**
    * Asserts the state of the network monitor when one request has loaded
    */
   function assertSingleRequestState() {
-    is(gStore.getState().requests.requests.size, 1,
+    is(RequestsMenu.itemCount, 1,
       "The request menu should have one item at this point.");
     is(detailsPanelToggleButton.hasAttribute("disabled"), false,
       "The pane toggle button should be enabled after a request is made.");
   }
 
   /**
    * Asserts the state of the network monitor when no requests have loaded
    */
   function assertNoRequestState() {
-    is(gStore.getState().requests.requests.size, 0,
+    is(RequestsMenu.itemCount, 0,
       "The request menu should be empty at this point.");
     is(detailsPanelToggleButton.hasAttribute("disabled"), true,
       "The pane toggle button should be disabled when the request menu is cleared.");
   }
 });
--- a/devtools/client/netmonitor/test/browser_net_complex-params.js
+++ b/devtools/client/netmonitor/test/browser_net_complex-params.js
@@ -4,68 +4,69 @@
 "use strict";
 
 /**
  * Tests whether complex request params and payload sent via POST are
  * displayed correctly.
  */
 
 add_task(function* () {
+  let { L10N } = require("devtools/client/netmonitor/l10n");
+
   let { tab, monitor } = yield initNetMonitor(PARAMS_URL);
   info("Starting test... ");
 
-  let { document, gStore, windowRequire } = monitor.panelWin;
-  let Actions = windowRequire("devtools/client/netmonitor/actions/index");
-  let { L10N } = windowRequire("devtools/client/netmonitor/l10n");
+  let { document, NetMonitorView } = monitor.panelWin;
+  let { RequestsMenu } = NetMonitorView;
 
-  gStore.dispatch(Actions.batchEnable(false));
+  RequestsMenu.lazyUpdate = false;
 
   let wait = waitForNetworkEvents(monitor, 1, 6);
   yield ContentTask.spawn(tab.linkedBrowser, {}, function* () {
     content.wrappedJSObject.performRequests();
   });
   yield wait;
 
   wait = waitForDOM(document, "#params-panel .tree-section", 2);
-  gStore.dispatch(Actions.selectRequestByIndex(0));
-  EventUtils.sendMouseEvent({ type: "click" },
-    document.querySelector("#params-tab"));
+  EventUtils.sendMouseEvent({ type: "mousedown" },
+    document.querySelector(".network-details-panel-toggle"));
+  document.querySelector("#params-tab").click();
   yield wait;
   testParamsTab1("a", '""', '{ "foo": "bar" }', '""');
 
   wait = waitForDOM(document, "#params-panel .tree-section", 2);
-  gStore.dispatch(Actions.selectRequestByIndex(1));
+  RequestsMenu.selectedIndex = 1;
   yield wait;
   testParamsTab1("a", '"b"', '{ "foo": "bar" }', '""');
 
   wait = waitForDOM(document, "#params-panel .tree-section", 2);
-  gStore.dispatch(Actions.selectRequestByIndex(2));
+  RequestsMenu.selectedIndex = 2;
   yield wait;
   testParamsTab1("a", '"b"', "foo", '"bar"');
 
   wait = waitForDOM(document, "#params-panel tr:not(.tree-section).treeRow", 2);
-  gStore.dispatch(Actions.selectRequestByIndex(3));
+  RequestsMenu.selectedIndex = 3;
   yield wait;
   testParamsTab2("a", '""', '{ "foo": "bar" }', "js");
 
   wait = waitForDOM(document, "#params-panel tr:not(.tree-section).treeRow", 2);
-  gStore.dispatch(Actions.selectRequestByIndex(4));
+  RequestsMenu.selectedIndex = 4;
   yield wait;
   testParamsTab2("a", '"b"', '{ "foo": "bar" }', "js");
 
   // Wait for all tree sections and editor updated by react
   let waitSections = waitForDOM(document, "#params-panel .tree-section", 2);
   let waitEditor = waitForDOM(document, "#params-panel .editor-mount iframe");
-  gStore.dispatch(Actions.selectRequestByIndex(5));
+  RequestsMenu.selectedIndex = 5;
   let [, editorFrames] = yield Promise.all([waitSections, waitEditor]);
   yield once(editorFrames[0], "DOMContentLoaded");
   yield waitForDOM(editorFrames[0].contentDocument, ".CodeMirror-code");
   testParamsTab2("a", '"b"', "?foo=bar", "text");
 
-  gStore.dispatch(Actions.selectRequestByIndex(6));
+  RequestsMenu.selectedIndex = 6;
   testParamsTab3();
 
   yield teardown(monitor);
 
   function testParamsTab1(queryStringParamName, queryStringParamValue,
                           formDataParamName, formDataParamValue) {
     let tabpanel = document.querySelector("#params-panel");
 
--- a/devtools/client/netmonitor/test/browser_net_content-type.js
+++ b/devtools/client/netmonitor/test/browser_net_content-type.js
@@ -8,120 +8,84 @@
  */
 
 add_task(function* () {
   let { L10N } = require("devtools/client/netmonitor/l10n");
 
   let { tab, monitor } = yield initNetMonitor(CONTENT_TYPE_WITHOUT_CACHE_URL);
   info("Starting test... ");
 
-  let { document, gStore, windowRequire } = monitor.panelWin;
-  let Actions = windowRequire("devtools/client/netmonitor/actions/index");
-  let {
-    getDisplayedRequests,
-    getSortedRequests,
-  } = windowRequire("devtools/client/netmonitor/selectors/index");
+  let { document, NetMonitorView } = monitor.panelWin;
+  let { RequestsMenu } = NetMonitorView;
 
-  gStore.dispatch(Actions.batchEnable(false));
+  RequestsMenu.lazyUpdate = false;
 
   let wait = waitForNetworkEvents(monitor, CONTENT_TYPE_WITHOUT_CACHE_REQUESTS);
   yield ContentTask.spawn(tab.linkedBrowser, {}, function* () {
     content.wrappedJSObject.performRequests();
   });
   yield wait;
 
-  verifyRequestItemTarget(
-    document,
-    getDisplayedRequests(gStore.getState()),
-    getSortedRequests(gStore.getState()).get(0),
-    "GET",
-    CONTENT_TYPE_SJS + "?fmt=xml",
-    {
+  verifyRequestItemTarget(RequestsMenu, RequestsMenu.getItemAtIndex(0),
+    "GET", CONTENT_TYPE_SJS + "?fmt=xml", {
       status: 200,
       statusText: "OK",
       type: "xml",
       fullMimeType: "text/xml; charset=utf-8",
       size: L10N.getFormatStrWithNumbers("networkMenu.sizeB", 42),
       time: true
     });
-   verifyRequestItemTarget(
-    document,
-    getDisplayedRequests(gStore.getState()),
-    getSortedRequests(gStore.getState()).get(1),
-    "GET",
-    CONTENT_TYPE_SJS + "?fmt=css",
-    {
+  verifyRequestItemTarget(RequestsMenu, RequestsMenu.getItemAtIndex(1),
+    "GET", CONTENT_TYPE_SJS + "?fmt=css", {
       status: 200,
       statusText: "OK",
       type: "css",
       fullMimeType: "text/css; charset=utf-8",
       size: L10N.getFormatStrWithNumbers("networkMenu.sizeB", 34),
       time: true
     });
-   verifyRequestItemTarget(
-    document,
-    getDisplayedRequests(gStore.getState()),
-    getSortedRequests(gStore.getState()).get(2),
-    "GET",
-    CONTENT_TYPE_SJS + "?fmt=js",
-    {
+  verifyRequestItemTarget(RequestsMenu, RequestsMenu.getItemAtIndex(2),
+    "GET", CONTENT_TYPE_SJS + "?fmt=js", {
       status: 200,
       statusText: "OK",
       type: "js",
       fullMimeType: "application/javascript; charset=utf-8",
       size: L10N.getFormatStrWithNumbers("networkMenu.sizeB", 34),
       time: true
     });
-   verifyRequestItemTarget(
-    document,
-    getDisplayedRequests(gStore.getState()),
-    getSortedRequests(gStore.getState()).get(3),
-    "GET",
-    CONTENT_TYPE_SJS + "?fmt=json",
-    {
+  verifyRequestItemTarget(RequestsMenu, RequestsMenu.getItemAtIndex(3),
+    "GET", CONTENT_TYPE_SJS + "?fmt=json", {
       status: 200,
       statusText: "OK",
       type: "json",
       fullMimeType: "application/json; charset=utf-8",
       size: L10N.getFormatStrWithNumbers("networkMenu.sizeB", 29),
       time: true
     });
-   verifyRequestItemTarget(
-    document,
-    getDisplayedRequests(gStore.getState()),
-    getSortedRequests(gStore.getState()).get(4),
-    "GET",
-    CONTENT_TYPE_SJS + "?fmt=bogus", {
+  verifyRequestItemTarget(RequestsMenu, RequestsMenu.getItemAtIndex(4),
+    "GET", CONTENT_TYPE_SJS + "?fmt=bogus", {
       status: 404,
       statusText: "Not Found",
       type: "html",
       fullMimeType: "text/html; charset=utf-8",
       size: L10N.getFormatStrWithNumbers("networkMenu.sizeB", 24),
       time: true
     });
-   verifyRequestItemTarget(
-    document,
-    getDisplayedRequests(gStore.getState()),
-    getSortedRequests(gStore.getState()).get(5),
-    "GET",
-    TEST_IMAGE, {
+  verifyRequestItemTarget(RequestsMenu, RequestsMenu.getItemAtIndex(5),
+    "GET", TEST_IMAGE, {
       fuzzyUrl: true,
       status: 200,
       statusText: "OK",
       type: "png",
       fullMimeType: "image/png",
       size: L10N.getFormatStrWithNumbers("networkMenu.sizeB", 580),
       time: true
     });
-   verifyRequestItemTarget(
-    document,
-    getDisplayedRequests(gStore.getState()),
-    getSortedRequests(gStore.getState()).get(6),
-    "GET",
-    CONTENT_TYPE_SJS + "?fmt=gzip", {
+  verifyRequestItemTarget(RequestsMenu, RequestsMenu.getItemAtIndex(6),
+    "GET", CONTENT_TYPE_SJS + "?fmt=gzip", {
       status: 200,
       statusText: "OK",
       type: "plain",
       fullMimeType: "text/plain",
       transferred: L10N.getFormatStrWithNumbers("networkMenu.sizeB", 73),
       size: L10N.getFormatStrWithNumbers("networkMenu.sizeKB", 10.73),
       time: true
     });
@@ -258,34 +222,34 @@ add_task(function* () {
       }
     }
   }
 
   function* selectIndexAndWaitForEditor(index) {
     let editor = document.querySelector("#response-panel .editor-mount iframe");
     if (!editor) {
       let waitDOM = waitForDOM(document, "#response-panel .editor-mount iframe");
-      gStore.dispatch(Actions.selectRequestByIndex(index));
+      RequestsMenu.selectedIndex = index;
       document.querySelector("#response-tab").click();
       [editor] = yield waitDOM;
       yield once(editor, "DOMContentLoaded");
     } else {
-      gStore.dispatch(Actions.selectRequestByIndex(index));
+      RequestsMenu.selectedIndex = index;
     }
 
     yield waitForDOM(editor.contentDocument, ".CodeMirror-code");
   }
 
   function* selectIndexAndWaitForJSONView(index) {
     let tabpanel = document.querySelector("#response-panel");
     let waitDOM = waitForDOM(tabpanel, ".treeTable");
-    gStore.dispatch(Actions.selectRequestByIndex(index));
+    RequestsMenu.selectedIndex = index;
     yield waitDOM;
   }
 
   function* selectIndexAndWaitForImageView(index) {
     let tabpanel = document.querySelector("#response-panel");
     let waitDOM = waitForDOM(tabpanel, ".response-image");
-    gStore.dispatch(Actions.selectRequestByIndex(index));
+    RequestsMenu.selectedIndex = index;
     let [imageNode] = yield waitDOM;
     yield once(imageNode, "load");
   }
 });
--- a/devtools/client/netmonitor/test/browser_net_copy_as_curl.js
+++ b/devtools/client/netmonitor/test/browser_net_copy_as_curl.js
@@ -36,35 +36,31 @@ add_task(function* () {
     header("X-Custom-Header-2: 8.8.8.8"),
     header("X-Custom-Header-3: Mon, 3 Mar 2014 11:11:11 GMT"),
     header("Referer: " + CURL_URL),
     header("Connection: keep-alive"),
     header("Pragma: no-cache"),
     header("Cache-Control: no-cache")
   ];
 
-  let { document, gStore, windowRequire } = monitor.panelWin;
-  let Actions = windowRequire("devtools/client/netmonitor/actions/index");
+  let { NetMonitorView } = monitor.panelWin;
+  let { RequestsMenu } = NetMonitorView;
+  RequestsMenu.lazyUpdate = false;
 
   let wait = waitForNetworkEvents(monitor, 1);
   yield ContentTask.spawn(tab.linkedBrowser, SIMPLE_SJS, function* (url) {
     content.wrappedJSObject.performRequest(url);
   });
   yield wait;
 
-  EventUtils.sendMouseEvent({ type: "mousedown" },
-    document.querySelectorAll(".request-list-item")[0]);
-  EventUtils.sendMouseEvent({ type: "contextmenu" },
-    document.querySelectorAll(".request-list-item")[0]);
+  let requestItem = RequestsMenu.getItemAtIndex(0);
+  RequestsMenu.selectedItem = requestItem;
 
   yield waitForClipboardPromise(function setup() {
-    // Context menu is appending in XUL document, we must select it from
-    // _toolbox.doc
-    monitor._toolbox.doc
-      .querySelector("#request-menu-context-copy-as-curl").click();
+    RequestsMenu.contextMenu.copyAsCurl();
   }, function validate(result) {
     if (typeof result !== "string") {
       return false;
     }
 
     // Different setups may produce the same command, but with the
     // parameters in a different order in the commandline (which is fine).
     // Here we confirm that the commands are the same even in that case.
--- a/devtools/client/netmonitor/test/browser_net_copy_headers.js
+++ b/devtools/client/netmonitor/test/browser_net_copy_headers.js
@@ -6,50 +6,45 @@
 /**
  * Tests if copying a request's request/response headers works.
  */
 
 add_task(function* () {
   let { tab, monitor } = yield initNetMonitor(SIMPLE_URL);
   info("Starting test... ");
 
-  let { document, gStore, windowRequire } = monitor.panelWin;
-  let { getSortedRequests } = windowRequire("devtools/client/netmonitor/selectors/index");
+  let { NetMonitorView } = monitor.panelWin;
+  let { RequestsMenu } = NetMonitorView;
+
+  RequestsMenu.lazyUpdate = false;
 
   let wait = waitForNetworkEvents(monitor, 1);
   tab.linkedBrowser.reload();
   yield wait;
 
-  EventUtils.sendMouseEvent({ type: "contextmenu" },
-    document.querySelectorAll(".request-list-item")[0]);
+  let requestItem = RequestsMenu.getItemAtIndex(0);
+  RequestsMenu.selectedItem = requestItem;
 
-  let requestItem = getSortedRequests(gStore.getState()).get(0);
   let { method, httpVersion, status, statusText } = requestItem;
 
-  EventUtils.sendMouseEvent({ type: "contextmenu" },
-    document.querySelectorAll(".request-list-item")[0]);
-
   const EXPECTED_REQUEST_HEADERS = [
     `${method} ${SIMPLE_URL} ${httpVersion}`,
     "Host: example.com",
     "User-Agent: " + navigator.userAgent + "",
     "Accept: text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8",
     "Accept-Language: " + navigator.languages.join(",") + ";q=0.5",
     "Accept-Encoding: gzip, deflate",
     "Connection: keep-alive",
     "Upgrade-Insecure-Requests: 1",
     "Pragma: no-cache",
     "Cache-Control: no-cache"
   ].join("\n");
 
   yield waitForClipboardPromise(function setup() {
-    // Context menu is appending in XUL document, we must select it from
-    // _toolbox.doc
-    monitor._toolbox.doc
-      .querySelector("#request-menu-context-copy-request-headers").click();
+    RequestsMenu.contextMenu.copyRequestHeaders();
   }, function validate(result) {
     // Sometimes, a "Cookie" header is left over from other tests. Remove it:
     result = String(result).replace(/Cookie: [^\n]+\n/, "");
     return result === EXPECTED_REQUEST_HEADERS;
   });
   info("Clipboard contains the currently selected item's request headers.");
 
   const EXPECTED_RESPONSE_HEADERS = [
@@ -57,24 +52,18 @@ add_task(function* () {
     "Last-Modified: Sun, 3 May 2015 11:11:11 GMT",
     "Content-Type: text/html",
     "Content-Length: 465",
     "Connection: close",
     "Server: httpd.js",
     "Date: Sun, 3 May 2015 11:11:11 GMT"
   ].join("\n");
 
-  EventUtils.sendMouseEvent({ type: "contextmenu" },
-    document.querySelectorAll(".request-list-item")[0]);
-
   yield waitForClipboardPromise(function setup() {
-    // Context menu is appending in XUL document, we must select it from
-    // _toolbox.doc
-    monitor._toolbox.doc
-      .querySelector("#response-menu-context-copy-response-headers").click();
+    RequestsMenu.contextMenu.copyResponseHeaders();
   }, function validate(result) {
     // Fake the "Last-Modified" and "Date" headers because they will vary:
     result = String(result)
       .replace(/Last-Modified: [^\n]+ GMT/, "Last-Modified: Sun, 3 May 2015 11:11:11 GMT")
       .replace(/Date: [^\n]+ GMT/, "Date: Sun, 3 May 2015 11:11:11 GMT");
     return result === EXPECTED_RESPONSE_HEADERS;
   });
   info("Clipboard contains the currently selected item's response headers.");
--- a/devtools/client/netmonitor/test/browser_net_copy_image_as_data_uri.js
+++ b/devtools/client/netmonitor/test/browser_net_copy_image_as_data_uri.js
@@ -6,32 +6,30 @@
 /**
  * Tests if copying an image as data uri works.
  */
 
 add_task(function* () {
   let { tab, monitor } = yield initNetMonitor(CONTENT_TYPE_WITHOUT_CACHE_URL);
   info("Starting test... ");
 
-  let { document, gStore, windowRequire } = monitor.panelWin;
+  let { NetMonitorView } = monitor.panelWin;
+  let { RequestsMenu } = NetMonitorView;
+
+  RequestsMenu.lazyUpdate = false;
 
   let wait = waitForNetworkEvents(monitor, CONTENT_TYPE_WITHOUT_CACHE_REQUESTS);
   yield ContentTask.spawn(tab.linkedBrowser, {}, function* () {
     content.wrappedJSObject.performRequests();
   });
   yield wait;
 
-  EventUtils.sendMouseEvent({ type: "mousedown" },
-    document.querySelectorAll(".request-list-item")[5]);
-  EventUtils.sendMouseEvent({ type: "contextmenu" },
-    document.querySelectorAll(".request-list-item")[5]);
+  let requestItem = RequestsMenu.getItemAtIndex(5);
+  RequestsMenu.selectedItem = requestItem;
 
   yield waitForClipboardPromise(function setup() {
-    // Context menu is appending in XUL document, we must select it from
-    // _toolbox.doc
-    monitor._toolbox.doc
-      .querySelector("#request-menu-context-copy-image-as-data-uri").click();
+    RequestsMenu.contextMenu.copyImageAsDataUri();
   }, TEST_IMAGE_DATA_URI);
 
   ok(true, "Clipboard contains the currently selected image as data uri.");
 
   yield teardown(monitor);
 });
--- a/devtools/client/netmonitor/test/browser_net_copy_params.js
+++ b/devtools/client/netmonitor/test/browser_net_copy_params.js
@@ -6,102 +6,93 @@
 /**
  * Tests whether copying a request item's parameters works.
  */
 
 add_task(function* () {
   let { tab, monitor } = yield initNetMonitor(PARAMS_URL);
   info("Starting test... ");
 
-  let { document, gStore, windowRequire } = monitor.panelWin;
-  let Actions = windowRequire("devtools/client/netmonitor/actions/index");
+  let { NetMonitorView } = monitor.panelWin;
+  let { RequestsMenu } = NetMonitorView;
 
-  gStore.dispatch(Actions.batchEnable(false));
+  RequestsMenu.lazyUpdate = false;
 
   let wait = waitForNetworkEvents(monitor, 1, 6);
   yield ContentTask.spawn(tab.linkedBrowser, {}, function* () {
     content.wrappedJSObject.performRequests();
   });
   yield wait;
 
-  yield testCopyUrlParamsHidden(0, false);
-  yield testCopyUrlParams(0, "a");
-  yield testCopyPostDataHidden(0, false);
-  yield testCopyPostData(0, "{ \"foo\": \"bar\" }");
+  RequestsMenu.selectedItem = RequestsMenu.getItemAtIndex(0);
+  yield testCopyUrlParamsHidden(false);
+  yield testCopyUrlParams("a");
+  yield testCopyPostDataHidden(false);
+  yield testCopyPostData("{ \"foo\": \"bar\" }");
 
-  yield testCopyUrlParamsHidden(1, false);
-  yield testCopyUrlParams(1, "a=b");
-  yield testCopyPostDataHidden(1, false);
-  yield testCopyPostData(1, "{ \"foo\": \"bar\" }");
+  RequestsMenu.selectedItem = RequestsMenu.getItemAtIndex(1);
+  yield testCopyUrlParamsHidden(false);
+  yield testCopyUrlParams("a=b");
+  yield testCopyPostDataHidden(false);
+  yield testCopyPostData("{ \"foo\": \"bar\" }");
 
-  yield testCopyUrlParamsHidden(2, false);
-  yield testCopyUrlParams(2, "a=b");
-  yield testCopyPostDataHidden(2, false);
-  yield testCopyPostData(2, "foo=bar");
+  RequestsMenu.selectedItem = RequestsMenu.getItemAtIndex(2);
+  yield testCopyUrlParamsHidden(false);
+  yield testCopyUrlParams("a=b");
+  yield testCopyPostDataHidden(false);
+  yield testCopyPostData("foo=bar");
 
-  yield testCopyUrlParamsHidden(3, false);
-  yield testCopyUrlParams(3, "a");
-  yield testCopyPostDataHidden(3, false);
-  yield testCopyPostData(3, "{ \"foo\": \"bar\" }");
+  RequestsMenu.selectedItem = RequestsMenu.getItemAtIndex(3);
+  yield testCopyUrlParamsHidden(false);
+  yield testCopyUrlParams("a");
+  yield testCopyPostDataHidden(false);
+  yield testCopyPostData("{ \"foo\": \"bar\" }");
 
-  yield testCopyUrlParamsHidden(4, false);
-  yield testCopyUrlParams(4, "a=b");
-  yield testCopyPostDataHidden(4, false);
-  yield testCopyPostData(4, "{ \"foo\": \"bar\" }");
+  RequestsMenu.selectedItem = RequestsMenu.getItemAtIndex(4);
+  yield testCopyUrlParamsHidden(false);
+  yield testCopyUrlParams("a=b");
+  yield testCopyPostDataHidden(false);
+  yield testCopyPostData("{ \"foo\": \"bar\" }");
 
-  yield testCopyUrlParamsHidden(5, false);
-  yield testCopyUrlParams(5, "a=b");
-  yield testCopyPostDataHidden(5, false);
-  yield testCopyPostData(5, "?foo=bar");
+  RequestsMenu.selectedItem = RequestsMenu.getItemAtIndex(5);
+  yield testCopyUrlParamsHidden(false);
+  yield testCopyUrlParams("a=b");
+  yield testCopyPostDataHidden(false);
+  yield testCopyPostData("?foo=bar");
 
-  yield testCopyUrlParamsHidden(6, true);
-  yield testCopyPostDataHidden(6, true);
+  RequestsMenu.selectedItem = RequestsMenu.getItemAtIndex(6);
+  yield testCopyUrlParamsHidden(true);
+  yield testCopyPostDataHidden(true);
 
   return teardown(monitor);
 
-  function testCopyUrlParamsHidden(index, hidden) {
-    EventUtils.sendMouseEvent({ type: "mousedown" },
-      document.querySelectorAll(".request-list-item")[index]);
-    EventUtils.sendMouseEvent({ type: "contextmenu" },
-      document.querySelectorAll(".request-list-item")[index]);
-    let copyUrlParamsNode = monitor._toolbox.doc
-      .querySelector("#request-menu-context-copy-url-params");
-    is(!!copyUrlParamsNode, !hidden,
+  function testCopyUrlParamsHidden(hidden) {
+    let allMenuItems = openContextMenuAndGetAllItems(NetMonitorView);
+    let copyUrlParamsNode = allMenuItems.find(item =>
+      item.id === "request-menu-context-copy-url-params");
+    is(copyUrlParamsNode.visible, !hidden,
       "The \"Copy URL Parameters\" context menu item should" + (hidden ? " " : " not ") +
         "be hidden.");
   }
 
-  function* testCopyUrlParams(index, queryString) {
-    EventUtils.sendMouseEvent({ type: "mousedown" },
-      document.querySelectorAll(".request-list-item")[index]);
-    EventUtils.sendMouseEvent({ type: "contextmenu" },
-      document.querySelectorAll(".request-list-item")[index]);
+  function* testCopyUrlParams(queryString) {
     yield waitForClipboardPromise(function setup() {
-      monitor._toolbox.doc
-        .querySelector("#request-menu-context-copy-url-params").click();
+      RequestsMenu.contextMenu.copyUrlParams();
     }, queryString);
     ok(true, "The url query string copied from the selected item is correct.");
   }
 
-  function testCopyPostDataHidden(index, hidden) {
-    EventUtils.sendMouseEvent({ type: "mousedown" },
-      document.querySelectorAll(".request-list-item")[index]);
-    EventUtils.sendMouseEvent({ type: "contextmenu" },
-      document.querySelectorAll(".request-list-item")[index]);
-    let copyPostDataNode = monitor._toolbox.doc
-      .querySelector("#request-menu-context-copy-post-data");
-    is(!!copyPostDataNode, !hidden,
+  function testCopyPostDataHidden(hidden) {
+    let allMenuItems = openContextMenuAndGetAllItems(NetMonitorView);
+    let copyPostDataNode = allMenuItems.find(item =>
+      item.id === "request-menu-context-copy-post-data");
+    is(copyPostDataNode.visible, !hidden,
       "The \"Copy POST Data\" context menu item should" + (hidden ? " " : " not ") +
         "be hidden.");
   }
 
-  function* testCopyPostData(index, postData) {
-    EventUtils.sendMouseEvent({ type: "mousedown" },
-      document.querySelectorAll(".request-list-item")[index]);
-    EventUtils.sendMouseEvent({ type: "contextmenu" },
-      document.querySelectorAll(".request-list-item")[index]);
+  function* testCopyPostData(postData) {
     yield waitForClipboardPromise(function setup() {
-      monitor._toolbox.doc
-        .querySelector("#request-menu-context-copy-post-data").click();
+      RequestsMenu.contextMenu.copyPostData();
     }, postData);
     ok(true, "The post data string copied from the selected item is correct.");
   }
 });
--- a/devtools/client/netmonitor/test/browser_net_copy_response.js
+++ b/devtools/client/netmonitor/test/browser_net_copy_response.js
@@ -8,30 +8,28 @@
  */
 
 add_task(function* () {
   let { tab, monitor } = yield initNetMonitor(CONTENT_TYPE_WITHOUT_CACHE_URL);
   info("Starting test... ");
 
   const EXPECTED_RESULT = '{ "greeting": "Hello JSON!" }';
 
-  let { document } = monitor.panelWin;
+  let { NetMonitorView } = monitor.panelWin;
+  let { RequestsMenu } = NetMonitorView;
+
+  RequestsMenu.lazyUpdate = false;
 
   let wait = waitForNetworkEvents(monitor, CONTENT_TYPE_WITHOUT_CACHE_REQUESTS);
   yield ContentTask.spawn(tab.linkedBrowser, {}, function* () {
     content.wrappedJSObject.performRequests();
   });
   yield wait;
 
-  EventUtils.sendMouseEvent({ type: "mousedown" },
-    document.querySelectorAll(".request-list-item")[3]);
-  EventUtils.sendMouseEvent({ type: "contextmenu" },
-    document.querySelectorAll(".request-list-item")[3]);
+  let requestItem = RequestsMenu.getItemAtIndex(3);
+  RequestsMenu.selectedItem = requestItem;
 
   yield waitForClipboardPromise(function setup() {
-    // Context menu is appending in XUL document, we must select it from
-    // _toolbox.doc
-    monitor._toolbox.doc
-      .querySelector("#request-menu-context-copy-response").click();
+    RequestsMenu.contextMenu.copyResponse();
   }, EXPECTED_RESULT);
 
   yield teardown(monitor);
 });
--- a/devtools/client/netmonitor/test/browser_net_copy_svg_image_as_data_uri.js
+++ b/devtools/client/netmonitor/test/browser_net_copy_svg_image_as_data_uri.js
@@ -8,32 +8,30 @@
  */
 
 const SVG_URL = EXAMPLE_URL + "dropmarker.svg";
 
 add_task(function* () {
   let { tab, monitor } = yield initNetMonitor(CURL_URL);
   info("Starting test... ");
 
-  let { document } = monitor.panelWin;
+  let { NetMonitorView } = monitor.panelWin;
+  let { RequestsMenu } = NetMonitorView;
+
+  RequestsMenu.lazyUpdate = false;
 
   let wait = waitForNetworkEvents(monitor, 1);
   yield ContentTask.spawn(tab.linkedBrowser, SVG_URL, function* (url) {
     content.wrappedJSObject.performRequest(url);
   });
   yield wait;
 
-  EventUtils.sendMouseEvent({ type: "mousedown" },
-    document.querySelectorAll(".request-list-item")[0]);
-  EventUtils.sendMouseEvent({ type: "contextmenu" },
-    document.querySelectorAll(".request-list-item")[0]);
+  let requestItem = RequestsMenu.getItemAtIndex(0);
+  RequestsMenu.selectedItem = requestItem;
 
   yield waitForClipboardPromise(function setup() {
-    // Context menu is appending in XUL document, we must select it from
-    // _toolbox.doc
-    monitor._toolbox.doc
-      .querySelector("#request-menu-context-copy-image-as-data-uri").click();
+    RequestsMenu.contextMenu.copyImageAsDataUri();
   }, function check(text) {
     return text.startsWith("data:") && !/undefined/.test(text);
   });
 
   yield teardown(monitor);
 });
--- a/devtools/client/netmonitor/test/browser_net_copy_url.js
+++ b/devtools/client/netmonitor/test/browser_net_copy_url.js
@@ -6,33 +6,26 @@
 /**
  * Tests if copying a request's url works.
  */
 
 add_task(function* () {
   let { tab, monitor } = yield initNetMonitor(CUSTOM_GET_URL);
   info("Starting test... ");
 
-  let { document, gStore, windowRequire } = monitor.panelWin;
-  let { getSortedRequests } = windowRequire("devtools/client/netmonitor/selectors/index");
+  let { NetMonitorView } = monitor.panelWin;
+  let { RequestsMenu } = NetMonitorView;
 
   let wait = waitForNetworkEvents(monitor, 1);
   yield ContentTask.spawn(tab.linkedBrowser, {}, function* () {
     content.wrappedJSObject.performRequests(1);
   });
   yield wait;
 
-  EventUtils.sendMouseEvent({ type: "mousedown" },
-    document.querySelectorAll(".request-list-item")[0]);
-  EventUtils.sendMouseEvent({ type: "contextmenu" },
-    document.querySelectorAll(".request-list-item")[0]);
-
-  let requestItem = getSortedRequests(gStore.getState()).get(0);
+  let requestItem = RequestsMenu.getItemAtIndex(0);
+  RequestsMenu.selectedItem = requestItem;
 
   yield waitForClipboardPromise(function setup() {
-    // Context menu is appending in XUL document, we must select it from
-    // _toolbox.doc
-    monitor._toolbox.doc
-      .querySelector("#request-menu-context-copy-url").click();
+    RequestsMenu.contextMenu.copyUrl();
   }, requestItem.url);
 
   yield teardown(monitor);
 });
--- a/devtools/client/netmonitor/test/browser_net_cors_requests.js
+++ b/devtools/client/netmonitor/test/browser_net_cors_requests.js
@@ -4,42 +4,30 @@
 "use strict";
 
 /**
  * Test that CORS preflight requests are displayed by network monitor
  */
 
 add_task(function* () {
   let { tab, monitor } = yield initNetMonitor(CORS_URL);
-
-  let { document, gStore, windowRequire } = monitor.panelWin;
-  let Actions = windowRequire("devtools/client/netmonitor/actions/index");
-  let {
-    getDisplayedRequests,
-    getSortedRequests,
-  } = windowRequire("devtools/client/netmonitor/selectors/index");
-
-  gStore.dispatch(Actions.batchEnable(false));
+  let { RequestsMenu } = monitor.panelWin.NetMonitorView;
+  RequestsMenu.lazyUpdate = false;
 
   let wait = waitForNetworkEvents(monitor, 1, 1);
 
   info("Performing a CORS request");
   let requestUrl = "http://test1.example.com" + CORS_SJS_PATH;
   yield ContentTask.spawn(tab.linkedBrowser, requestUrl, function* (url) {
     content.wrappedJSObject.performRequests(url, "triggering/preflight", "post-data");
   });
 
   info("Waiting until the requests appear in netmonitor");
   yield wait;
 
   info("Checking the preflight and flight methods");
-  ["OPTIONS", "POST"].forEach((method, index) => {
-    verifyRequestItemTarget(
-      document,
-      getDisplayedRequests(gStore.getState()),
-      getSortedRequests(gStore.getState()).get(index),
-      method,
-      requestUrl
-    );
+  ["OPTIONS", "POST"].forEach((method, i) => {
+    verifyRequestItemTarget(RequestsMenu, RequestsMenu.getItemAtIndex(i),
+      method, requestUrl);
   });
 
   yield teardown(monitor);
 });
--- a/devtools/client/netmonitor/test/browser_net_curl-utils.js
+++ b/devtools/client/netmonitor/test/browser_net_curl-utils.js
@@ -8,33 +8,32 @@
  */
 
 const { CurlUtils } = require("devtools/client/shared/curl");
 
 add_task(function* () {
   let { tab, monitor } = yield initNetMonitor(CURL_UTILS_URL);
   info("Starting test... ");
 
-  let { document, gStore, windowRequire, gNetwork } = monitor.panelWin;
-  let Actions = windowRequire("devtools/client/netmonitor/actions/index");
-  let { getSortedRequests } = windowRequire("devtools/client/netmonitor/selectors/index");
+  let { NetMonitorView, gNetwork } = monitor.panelWin;
+  let { RequestsMenu } = NetMonitorView;
 
-  gStore.dispatch(Actions.batchEnable(false));
+  RequestsMenu.lazyUpdate = false;
 
   let wait = waitForNetworkEvents(monitor, 1, 3);
   yield ContentTask.spawn(tab.linkedBrowser, SIMPLE_SJS, function* (url) {
     content.wrappedJSObject.performRequests(url);
   });
   yield wait;
 
   let requests = {
-    get: getSortedRequests(gStore.getState()).get(0),
-    post: getSortedRequests(gStore.getState()).get(1),
-    multipart: getSortedRequests(gStore.getState()).get(2),
-    multipartForm: getSortedRequests(gStore.getState()).get(3),
+    get: RequestsMenu.getItemAtIndex(0),
+    post: RequestsMenu.getItemAtIndex(1),
+    multipart: RequestsMenu.getItemAtIndex(2),
+    multipartForm: RequestsMenu.getItemAtIndex(3)
   };
 
   let data = yield createCurlData(requests.get, gNetwork);
   testFindHeader(data);
 
   data = yield createCurlData(requests.post, gNetwork);
   testIsUrlEncodedRequest(data);
   testWritePostDataTextParams(data);
--- a/devtools/client/netmonitor/test/browser_net_cyrillic-01.js
+++ b/devtools/client/netmonitor/test/browser_net_cyrillic-01.js
@@ -6,50 +6,37 @@
 /**
  * Tests if cyrillic text is rendered correctly in the source editor.
  */
 
 add_task(function* () {
   let { tab, monitor } = yield initNetMonitor(CYRILLIC_URL);
   info("Starting test... ");
 
-  let { document, gStore, windowRequire } = monitor.panelWin;
-  let Actions = windowRequire("devtools/client/netmonitor/actions/index");
-  let {
-    getDisplayedRequests,
-    getSortedRequests,
-  } = windowRequire("devtools/client/netmonitor/selectors/index");
+  let { document, NetMonitorView } = monitor.panelWin;
+  let { RequestsMenu } = NetMonitorView;
 
-  gStore.dispatch(Actions.batchEnable(false));
+  RequestsMenu.lazyUpdate = false;
 
   let wait = waitForNetworkEvents(monitor, 1);
   yield ContentTask.spawn(tab.linkedBrowser, {}, function* () {
     content.wrappedJSObject.performRequests();
   });
   yield wait;
 
-  verifyRequestItemTarget(
-    document,
-    getDisplayedRequests(gStore.getState()),
-    getSortedRequests(gStore.getState()).get(0),
-    "GET",
-    CONTENT_TYPE_SJS + "?fmt=txt",
-    {
+  verifyRequestItemTarget(RequestsMenu, RequestsMenu.getItemAtIndex(0),
+    "GET", CONTENT_TYPE_SJS + "?fmt=txt", {
       status: 200,
       statusText: "DA DA DA"
-    }
-  );
+    });
 
-  wait = waitForDOM(document, "#headers-panel");
+  wait = waitForDOM(document, "#response-panel .editor-mount iframe");
   EventUtils.sendMouseEvent({ type: "mousedown" },
-    document.querySelectorAll(".request-list-item")[0]);
-  yield wait;
-  wait = waitForDOM(document, "#response-panel .editor-mount iframe");
-  EventUtils.sendMouseEvent({ type: "click" },
-    document.querySelector("#response-tab"));
+    document.querySelector(".network-details-panel-toggle"));
+  document.querySelector("#response-tab").click();
   let [editor] = yield wait;
   yield once(editor, "DOMContentLoaded");
   yield waitForDOM(editor.contentDocument, ".CodeMirror-code");
   let text = editor.contentDocument
           .querySelector(".CodeMirror-line").textContent;
 
   ok(text.includes("\u0411\u0440\u0430\u0442\u0430\u043d"),
     "The text shown in the source editor is correct.");
--- a/devtools/client/netmonitor/test/browser_net_cyrillic-02.js
+++ b/devtools/client/netmonitor/test/browser_net_cyrillic-02.js
@@ -7,45 +7,35 @@
  * Tests if cyrillic text is rendered correctly in the source editor
  * when loaded directly from an HTML page.
  */
 
 add_task(function* () {
   let { tab, monitor } = yield initNetMonitor(CYRILLIC_URL);
   info("Starting test... ");
 
-  let { document, gStore, windowRequire } = monitor.panelWin;
-  let Actions = windowRequire("devtools/client/netmonitor/actions/index");
-  let {
-    getDisplayedRequests,
-    getSortedRequests,
-  } = windowRequire("devtools/client/netmonitor/selectors/index");
+  let { document, NetMonitorView } = monitor.panelWin;
+  let { RequestsMenu } = NetMonitorView;
+
+  RequestsMenu.lazyUpdate = false;
 
   let wait = waitForNetworkEvents(monitor, 1);
   tab.linkedBrowser.reload();
   yield wait;
 
-  verifyRequestItemTarget(
-    document,
-    getDisplayedRequests(gStore.getState()),
-    getSortedRequests(gStore.getState()).get(0),
-    "GET",
-    CYRILLIC_URL,
-    {
+  verifyRequestItemTarget(RequestsMenu, RequestsMenu.getItemAtIndex(0),
+    "GET", CYRILLIC_URL, {
       status: 200,
       statusText: "OK"
     });
 
-  wait = waitForDOM(document, "#headers-panel");
+  wait = waitForDOM(document, "#response-panel .editor-mount iframe");
   EventUtils.sendMouseEvent({ type: "mousedown" },
-    document.querySelectorAll(".request-list-item")[0]);
-  yield wait;
-  wait = waitForDOM(document, "#response-panel .editor-mount iframe");
-  EventUtils.sendMouseEvent({ type: "click" },
-    document.querySelector("#response-tab"));
+    document.querySelector(".network-details-panel-toggle"));
+  document.querySelector("#response-tab").click();
   let [editor] = yield wait;
   yield once(editor, "DOMContentLoaded");
   yield waitForDOM(editor.contentDocument, ".CodeMirror-code");
   let text = editor.contentDocument
           .querySelector(".CodeMirror-code").textContent;
 
   ok(text.includes("\u0411\u0440\u0430\u0442\u0430\u043d"),
     "The text shown in the source editor is correct.");
--- a/devtools/client/netmonitor/test/browser_net_filter-01.js
+++ b/devtools/client/netmonitor/test/browser_net_filter-01.js
@@ -124,47 +124,46 @@ const EXPECTED_REQUESTS = [
       fuzzyUrl: true,
       status: 101,
       statusText: "Switching Protocols",
     }
   }
 ];
 
 add_task(function* () {
+  let Actions = require("devtools/client/netmonitor/actions/index");
+
   let { monitor } = yield initNetMonitor(FILTERING_URL);
-  let { document, gStore, windowRequire } = monitor.panelWin;
-  let Actions = windowRequire("devtools/client/netmonitor/actions/index");
-  let {
-    getDisplayedRequests,
-    getSelectedRequest,
-    getSortedRequests,
-  } = windowRequire("devtools/client/netmonitor/selectors/index");
-
-  gStore.dispatch(Actions.batchEnable(false));
+  let { gStore } = monitor.panelWin;
 
   function setFreetextFilter(value) {
     gStore.dispatch(Actions.setRequestFilterText(value));
   }
 
   info("Starting test... ");
 
+  let { document, NetMonitorView } = monitor.panelWin;
+  let { RequestsMenu } = NetMonitorView;
+
+  RequestsMenu.lazyUpdate = false;
+
   let wait = waitForNetworkEvents(monitor, 9);
   loadCommonFrameScript();
   yield performRequestsInContent(REQUESTS_WITH_MEDIA_AND_FLASH_AND_WS);
   yield wait;
 
   EventUtils.sendMouseEvent({ type: "mousedown" },
-    document.querySelectorAll(".request-list-item")[0]);
+    document.querySelector(".network-details-panel-toggle"));
 
-  isnot(getSelectedRequest(gStore.getState()), null,
+  isnot(RequestsMenu.selectedItem, null,
     "There should be a selected item in the requests menu.");
-  is(getSelectedIndex(gStore.getState()), 0,
+  is(RequestsMenu.selectedIndex, 0,
     "The first item should be selected in the requests menu.");
   is(!!document.querySelector(".network-details-panel"), true,
-    "The network details panel should render correctly.");
+      "The network details panel should render correctly.");
 
   // First test with single filters...
   testFilterButtons(monitor, "all");
   testContents([1, 1, 1, 1, 1, 1, 1, 1, 1]);
 
   EventUtils.sendMouseEvent({ type: "click" },
     document.querySelector("#requests-menu-filter-html-button"));
   testFilterButtons(monitor, "html");
@@ -302,50 +301,38 @@ add_task(function* () {
   testFilterButtonsCustom(monitor, [0, 1, 1, 0, 0, 0, 0, 0, 0, 1]);
   EventUtils.sendMouseEvent({ type: "click" },
     document.querySelector("#requests-menu-filter-all-button"));
   testFilterButtons(monitor, "all");
   testContents([1, 1, 1, 1, 1, 1, 1, 1, 1]);
 
   yield teardown(monitor);
 
-  function getSelectedIndex(state) {
-    if (!state.requests.selectedId) {
-      return -1;
-    }
-    return getSortedRequests(state).findIndex(r => r.id === state.requests.selectedId);
-  }
+  function testContents(visibility) {
+    isnot(RequestsMenu.selectedItem, null,
+      "There should still be a selected item after filtering.");
+    is(RequestsMenu.selectedIndex, 0,
+      "The first item should be still selected after filtering.");
+    is(!!document.querySelector(".network-details-panel"), true,
+      "The network details panel should render correctly.");
 
-  function testContents(visibility) {
-    isnot(getSelectedRequest(gStore.getState()), undefined,
-      "There should still be a selected item after filtering.");
-    is(getSelectedIndex(gStore.getState()), 0,
-      "The first item should be still selected after filtering.");
-
-    const items = getSortedRequests(gStore.getState());
-    const visibleItems = getDisplayedRequests(gStore.getState());
+    const items = RequestsMenu.items;
+    const visibleItems = RequestsMenu.visibleItems;
 
     is(items.size, visibility.length,
       "There should be a specific amount of items in the requests menu.");
     is(visibleItems.size, visibility.filter(e => e).length,
       "There should be a specific amount of visible items in the requests menu.");
 
     for (let i = 0; i < visibility.length; i++) {
       let itemId = items.get(i).id;
       let shouldBeVisible = !!visibility[i];
       let isThere = visibleItems.some(r => r.id == itemId);
       is(isThere, shouldBeVisible,
         `The item at index ${i} has visibility=${shouldBeVisible}`);
 
       if (shouldBeVisible) {
         let { method, url, data } = EXPECTED_REQUESTS[i];
-        verifyRequestItemTarget(
-          document,
-          getDisplayedRequests(gStore.getState()),
-          getSortedRequests(gStore.getState()).get(i),
-          method,
-          url,
-          data
-        );
+        verifyRequestItemTarget(RequestsMenu, items.get(i), method, url, data);
       }
     }
   }
 });
--- a/devtools/client/netmonitor/test/browser_net_filter-02.js
+++ b/devtools/client/netmonitor/test/browser_net_filter-02.js
@@ -131,37 +131,32 @@ const EXPECTED_REQUESTS = [
 
 add_task(function* () {
   let { monitor } = yield initNetMonitor(FILTERING_URL);
   info("Starting test... ");
 
   // It seems that this test may be slow on Ubuntu builds running on ec2.
   requestLongerTimeout(2);
 
-  let { document, gStore, windowRequire } = monitor.panelWin;
-  let Actions = windowRequire("devtools/client/netmonitor/actions/index");
-  let {
-    getDisplayedRequests,
-    getSelectedRequest,
-    getSortedRequests,
-  } = windowRequire("devtools/client/netmonitor/selectors/index");
+  let { document, NetMonitorView } = monitor.panelWin;
+  let { RequestsMenu } = NetMonitorView;
 
-  gStore.dispatch(Actions.batchEnable(false));
+  RequestsMenu.lazyUpdate = false;
 
   let wait = waitForNetworkEvents(monitor, 9);
   loadCommonFrameScript();
   yield performRequestsInContent(REQUESTS_WITH_MEDIA_AND_FLASH_AND_WS);
   yield wait;
 
   EventUtils.sendMouseEvent({ type: "mousedown" },
-    document.querySelectorAll(".request-list-item")[0]);
+    document.querySelector(".network-details-panel-toggle"));
 
-  isnot(getSelectedRequest(gStore.getState()), null,
+  isnot(RequestsMenu.selectedItem, null,
     "There should be a selected item in the requests menu.");
-  is(getSelectedIndex(gStore.getState()), 0,
+  is(RequestsMenu.selectedIndex, 0,
     "The first item should be selected in the requests menu.");
   is(!!document.querySelector(".network-details-panel"), true,
     "The network details panel should be visible after toggle button was pressed.");
 
   testFilterButtons(monitor, "all");
   testContents([1, 1, 1, 1, 1, 1, 1, 1, 1]);
 
   info("Testing html filtering.");
@@ -193,33 +188,26 @@ add_task(function* () {
   EventUtils.sendMouseEvent({ type: "click" },
     document.querySelector("#requests-menu-filter-all-button"));
   testFilterButtons(monitor, "all");
   testContents([1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1,
                 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1]);
 
   yield teardown(monitor);
 
-  function getSelectedIndex(state) {
-    if (!state.requests.selectedId) {
-      return -1;
-    }
-    return getSortedRequests(state).findIndex(r => r.id === state.requests.selectedId);
-  }
-
   function testContents(visibility) {
-    isnot(getSelectedRequest(gStore.getState()), null,
+    isnot(RequestsMenu.selectedItem, null,
       "There should still be a selected item after filtering.");
-    is(getSelectedIndex(gStore.getState()), 0,
+    is(RequestsMenu.selectedIndex, 0,
       "The first item should be still selected after filtering.");
     is(!!document.querySelector(".network-details-panel"), true,
       "The network details panel should still be visible after filtering.");
 
-    const items = getSortedRequests(gStore.getState());
-    const visibleItems = getDisplayedRequests(gStore.getState());
+    const items = RequestsMenu.items;
+    const visibleItems = RequestsMenu.visibleItems;
 
     is(items.size, visibility.length,
       "There should be a specific amount of items in the requests menu.");
     is(visibleItems.size, visibility.filter(e => e).length,
       "There should be a specific amount of visible items in the requests menu.");
 
     for (let i = 0; i < visibility.length; i++) {
       let itemId = items.get(i).id;
@@ -228,21 +216,14 @@ add_task(function* () {
       is(isThere, shouldBeVisible,
         `The item at index ${i} has visibility=${shouldBeVisible}`);
     }
 
     for (let i = 0; i < EXPECTED_REQUESTS.length; i++) {
       let { method, url, data } = EXPECTED_REQUESTS[i];
       for (let j = i; j < visibility.length; j += EXPECTED_REQUESTS.length) {
         if (visibility[j]) {
-          verifyRequestItemTarget(
-            document,
-            getDisplayedRequests(gStore.getState()),
-            getSortedRequests(gStore.getState()).get(i),
-            method,
-            url,
-            data
-          );
+          verifyRequestItemTarget(RequestsMenu, items.get(j), method, url, data);
         }
       }
     }
   }
 });
--- a/devtools/client/netmonitor/test/browser_net_filter-03.js
+++ b/devtools/client/netmonitor/test/browser_net_filter-03.js
@@ -22,44 +22,39 @@ const REQUESTS_WITH_MEDIA = BASIC_REQUES
 
 add_task(function* () {
   let { monitor } = yield initNetMonitor(FILTERING_URL);
   info("Starting test... ");
 
   // It seems that this test may be slow on Ubuntu builds running on ec2.
   requestLongerTimeout(2);
 
-  let { document, gStore, windowRequire } = monitor.panelWin;
-  let Actions = windowRequire("devtools/client/netmonitor/actions/index");
-  let {
-    getDisplayedRequests,
-    getSelectedRequest,
-    getSortedRequests,
-  } = windowRequire("devtools/client/netmonitor/selectors/index");
+  let { document, NetMonitorView } = monitor.panelWin;
+  let { RequestsMenu } = NetMonitorView;
 
-  gStore.dispatch(Actions.batchEnable(false));
+  RequestsMenu.lazyUpdate = false;
 
   // The test assumes that the first HTML request here has a longer response
   // body than the other HTML requests performed later during the test.
   let requests = Cu.cloneInto(REQUESTS_WITH_MEDIA, {});
   let newres = "res=<p>" + new Array(10).join(Math.random(10)) + "</p>";
   requests[0].url = requests[0].url.replace("res=undefined", newres);
 
   loadCommonFrameScript();
 
   let wait = waitForNetworkEvents(monitor, 7);
   yield performRequestsInContent(requests);
   yield wait;
 
   EventUtils.sendMouseEvent({ type: "mousedown" },
-    document.querySelectorAll(".request-list-item")[0]);
+    document.querySelector(".network-details-panel-toggle"));
 
-  isnot(getSelectedRequest(gStore.getState()), null,
+  isnot(RequestsMenu.selectedItem, null,
     "There should be a selected item in the requests menu.");
-  is(getSelectedIndex(gStore.getState()), 0,
+  is(RequestsMenu.selectedIndex, 0,
     "The first item should be selected in the requests menu.");
   is(!!document.querySelector(".network-details-panel"), true,
     "The network details panel should be visible after toggle button was pressed.");
 
   testFilterButtons(monitor, "all");
   testContents([0, 1, 2, 3, 4, 5, 6], 7, 0);
 
   info("Sorting by size, ascending.");
@@ -98,29 +93,22 @@ add_task(function* () {
 
   function resetSorting() {
     EventUtils.sendMouseEvent({ type: "click" },
       document.querySelector("#requests-menu-waterfall-button"));
     EventUtils.sendMouseEvent({ type: "click" },
       document.querySelector("#requests-menu-size-button"));
   }
 
-  function getSelectedIndex(state) {
-    if (!state.requests.selectedId) {
-      return -1;
-    }
-    return getSortedRequests(state).findIndex(r => r.id === state.requests.selectedId);
-  }
-
   function testContents(order, visible, selection) {
-    isnot(getSelectedRequest(gStore.getState()), null,
+    isnot(RequestsMenu.selectedItem, null,
       "There should still be a selected item after filtering.");
-    is(getSelectedIndex(gStore.getState()), selection,
+    is(RequestsMenu.selectedIndex, selection,
       "The first item should be still selected after filtering.");
     is(!!document.querySelector(".network-details-panel"), true,
       "The network details panel should still be visible after filtering.");
 
-    is(getSortedRequests(gStore.getState()).length, order.length,
+    is(RequestsMenu.items.length, order.length,
       "There should be a specific amount of items in the requests menu.");
-    is(getDisplayedRequests(gStore.getState()).length, visible,
+    is(RequestsMenu.visibleItems.length, visible,
       "There should be a specific amount of visible items in the requests menu.");
   }
 });
--- a/devtools/client/netmonitor/test/browser_net_filter-04.js
+++ b/devtools/client/netmonitor/test/browser_net_filter-04.js
@@ -30,21 +30,20 @@ const REQUESTS_WITH_MEDIA_AND_FLASH_AND_
 ]);
 
 add_task(function* () {
   Services.prefs.setCharPref("devtools.netmonitor.filters", '["js", "bogus"]');
 
   let { monitor } = yield initNetMonitor(FILTERING_URL);
   info("Starting test... ");
 
-  let { gStore, windowRequire } = monitor.panelWin;
-  let Actions = windowRequire("devtools/client/netmonitor/actions/index");
-  let { Prefs } = windowRequire("devtools/client/netmonitor/prefs");
+  let { Prefs, NetMonitorView } = monitor.panelWin;
+  let { RequestsMenu } = NetMonitorView;
 
-  gStore.dispatch(Actions.batchEnable(false));
+  RequestsMenu.lazyUpdate = false;
 
   is(Prefs.filters.length, 2,
     "All filter types were loaded as an array from the preferences.");
   is(Prefs.filters[0], "js",
     "The first filter type is correct.");
   is(Prefs.filters[1], "bogus",
     "The second filter type is invalid, but loaded anyway.");
 
--- a/devtools/client/netmonitor/test/browser_net_footer-summary.js
+++ b/devtools/client/netmonitor/test/browser_net_footer-summary.js
@@ -8,48 +8,49 @@
  */
 
 add_task(function* () {
   requestLongerTimeout(2);
 
   let { tab, monitor } = yield initNetMonitor(FILTERING_URL);
   info("Starting test... ");
 
-  let { document, gStore, windowRequire } = monitor.panelWin;
-  let Actions = windowRequire("devtools/client/netmonitor/actions/index");
+  let { $, NetMonitorView, gStore, windowRequire } = monitor.panelWin;
+  let { RequestsMenu } = NetMonitorView;
+
   let { getDisplayedRequestsSummary } =
     windowRequire("devtools/client/netmonitor/selectors/index");
   let { L10N } = windowRequire("devtools/client/netmonitor/l10n");
   let { PluralForm } = windowRequire("devtools/shared/plural-form");
 
-  gStore.dispatch(Actions.batchEnable(false));
+  RequestsMenu.lazyUpdate = false;
   testStatus();
 
   for (let i = 0; i < 2; i++) {
     info(`Performing requests in batch #${i}`);
     let wait = waitForNetworkEvents(monitor, 8);
     yield ContentTask.spawn(tab.linkedBrowser, {}, function* () {
       content.wrappedJSObject.performRequests('{ "getMedia": true, "getFlash": true }');
     });
     yield wait;
 
     testStatus();
 
     let buttons = ["html", "css", "js", "xhr", "fonts", "images", "media", "flash"];
     for (let button of buttons) {
-      let buttonEl = document.querySelector(`#requests-menu-filter-${button}-button`);
+      let buttonEl = $(`#requests-menu-filter-${button}-button`);
       EventUtils.sendMouseEvent({ type: "click" }, buttonEl);
       testStatus();
     }
   }
 
   yield teardown(monitor);
 
   function testStatus() {
-    let value = document.querySelector("#requests-menu-network-summary-button").textContent;
+    let value = $("#requests-menu-network-summary-button").textContent;
     info("Current summary: " + value);
 
     let state = gStore.getState();
     let totalRequestsCount = state.requests.requests.size;
     let requestsSummary = getDisplayedRequestsSummary(state);
     info(`Current requests: ${requestsSummary.count} of ${totalRequestsCount}.`);
 
     if (!totalRequestsCount || !requestsSummary.count) {
--- a/devtools/client/netmonitor/test/browser_net_frame.js
+++ b/devtools/client/netmonitor/test/browser_net_frame.js
@@ -152,58 +152,47 @@ add_task(function* () {
 
   // the initNetMonitor function clears the network request list after the
   // page is loaded. That's why we first load a bogus page from SIMPLE_URL,
   // and only then load the real thing from TOP_URL - we want to catch
   // all the requests the page is making, not only the XHRs.
   // We can't use about:blank here, because initNetMonitor checks that the
   // page has actually made at least one request.
   let { tab, monitor } = yield initNetMonitor(SIMPLE_URL);
-
-  let { document, gStore, windowRequire } = monitor.panelWin;
-  let Actions = windowRequire("devtools/client/netmonitor/actions/index");
-  let {
-    getDisplayedRequests,
-    getSortedRequests,
-  } = windowRequire("devtools/client/netmonitor/selectors/index");
-
-  gStore.dispatch(Actions.batchEnable(false));
+  let { NetMonitorView } = monitor.panelWin;
+  let { RequestsMenu } = NetMonitorView;
+  RequestsMenu.lazyUpdate = false;
 
   tab.linkedBrowser.loadURI(TOP_URL, null, null);
 
   yield waitForNetworkEvents(monitor, REQUEST_COUNT);
 
-  is(gStore.getState().requests.requests.size, REQUEST_COUNT,
+  is(RequestsMenu.itemCount, REQUEST_COUNT,
     "All the page events should be recorded.");
 
   // While there is a defined order for requests in each document separately, the requests
   // from different documents may interleave in various ways that change per test run, so
   // there is not a single order when considering all the requests together.
   let currentTop = 0;
   let currentSub = 0;
   for (let i = 0; i < REQUEST_COUNT; i++) {
-    let requestItem = getSortedRequests(gStore.getState()).get(i);
+    let requestItem = RequestsMenu.getItemAtIndex(i);
 
     let itemUrl = requestItem.url;
     let itemCauseUri = requestItem.cause.loadingDocumentUri;
     let spec;
     if (itemUrl == SUB_URL || itemCauseUri == SUB_URL) {
       spec = EXPECTED_REQUESTS_SUB[currentSub++];
     } else {
       spec = EXPECTED_REQUESTS_TOP[currentTop++];
     }
     let { method, url, causeType, causeUri, stack } = spec;
 
-    verifyRequestItemTarget(
-      document,
-      getDisplayedRequests(gStore.getState()),
-      requestItem,
-      method,
-      url,
-      { cause: { type: causeType, loadingDocumentUri: causeUri } }
+    verifyRequestItemTarget(RequestsMenu, requestItem,
+      method, url, { cause: { type: causeType, loadingDocumentUri: causeUri } }
     );
 
     let { stacktrace } = requestItem.cause;
     let stackLen = stacktrace ? stacktrace.length : 0;
 
     if (stack) {
       ok(stacktrace, `Request #${i} has a stacktrace`);
       ok(stackLen > 0,
--- a/devtools/client/netmonitor/test/browser_net_header-docs.js
+++ b/devtools/client/netmonitor/test/browser_net_header-docs.js
@@ -8,35 +8,34 @@ const HeadersMDN = require("devtools/cli
 /**
  * Tests if "Learn More" links are correctly displayed
  * next to headers.
  */
 add_task(function* () {
   let { tab, monitor } = yield initNetMonitor(POST_DATA_URL);
   info("Starting test... ");
 
-  let { document, gStore, windowRequire } = monitor.panelWin;
-  let Actions = windowRequire("devtools/client/netmonitor/actions/index");
-  let {
-    getDisplayedRequests,
-    getSortedRequests,
-  } = windowRequire("devtools/client/netmonitor/selectors/index");
+  let { document, NetMonitorView } = monitor.panelWin;
+  let { RequestsMenu } = NetMonitorView;
 
-  gStore.dispatch(Actions.batchEnable(false));
+  RequestsMenu.lazyUpdate = false;
 
   let wait = waitForNetworkEvents(monitor, 0, 2);
   yield ContentTask.spawn(tab.linkedBrowser, {}, function* () {
     content.wrappedJSObject.performRequests();
   });
   yield wait;
 
+  let origItem = RequestsMenu.getItemAtIndex(0);
+  RequestsMenu.selectedItem = origItem;
+
   EventUtils.sendMouseEvent({ type: "click" },
     document.querySelectorAll(".request-list-item")[0]);
 
-  testShowLearnMore(getSortedRequests(gStore.getState()).get(0));
+  testShowLearnMore(origItem);
 
   return teardown(monitor);
 
   /*
    * Tests that a "Learn More" button is only shown if
    * and only if a header is documented in MDN.
    */
   function testShowLearnMore(data) {
--- a/devtools/client/netmonitor/test/browser_net_html-preview.js
+++ b/devtools/client/netmonitor/test/browser_net_html-preview.js
@@ -6,28 +6,28 @@
 /**
  * Tests if html responses show and properly populate a "Preview" tab.
  */
 
 add_task(function* () {
   let { tab, monitor } = yield initNetMonitor(CONTENT_TYPE_URL);
   info("Starting test... ");
 
-  let { document, gStore, windowRequire } = monitor.panelWin;
-  let Actions = windowRequire("devtools/client/netmonitor/actions/index");
+  let { document, NetMonitorView } = monitor.panelWin;
+  let { RequestsMenu } = NetMonitorView;
 
-  gStore.dispatch(Actions.batchEnable(false));
+  RequestsMenu.lazyUpdate = false;
 
   let wait = waitForNetworkEvents(monitor, 6);
   yield ContentTask.spawn(tab.linkedBrowser, {}, function* () {
     content.wrappedJSObject.performRequests();
   });
   yield wait;
 
-  EventUtils.sendMouseEvent({ type: "click" },
+  EventUtils.sendMouseEvent({ type: "mousedown" },
     document.querySelector(".network-details-panel-toggle"));
 
   ok(document.querySelector("#headers-tab[aria-selected=true]"),
     "The headers tab in the details panel should be selected.");
   ok(!document.querySelector("#preview-tab"),
     "The preview tab should be hidden for non html responses.");
   ok(!document.querySelector("#preview-panel"),
     "The preview panel is hidden for non html responses.");
@@ -37,17 +37,19 @@ add_task(function* () {
   document.querySelector("#preview-tab").click();
 
   ok(document.querySelector("#preview-tab[aria-selected=true]"),
     "The preview tab in the details panel should be selected.");
   ok(document.querySelector("#preview-panel"),
     "The preview panel should be visible now.");
 
   let iframe = document.querySelector("#preview-panel iframe");
+  console.log(123)
   yield once(iframe, "DOMContentLoaded");
+  console.log(123)
 
   ok(iframe,
     "There should be a response preview iframe available.");
   ok(iframe.contentDocument,
     "The iframe's content document should be available.");
   is(iframe.contentDocument.querySelector("blink").textContent, "Not Found",
     "The iframe's content document should be loaded and correct.");
 
--- a/devtools/client/netmonitor/test/browser_net_icon-preview.js
+++ b/devtools/client/netmonitor/test/browser_net_icon-preview.js
@@ -3,26 +3,26 @@
 
 "use strict";
 
 /**
  * Tests if image responses show a thumbnail in the requests menu.
  */
 
 add_task(function* () {
+  let Actions = require("devtools/client/netmonitor/actions/index");
+
   let { tab, monitor } = yield initNetMonitor(CONTENT_TYPE_WITHOUT_CACHE_URL);
   info("Starting test... ");
 
-  let { document, gStore, windowRequire, NetMonitorController } =
-    monitor.panelWin;
-  let Actions = windowRequire("devtools/client/netmonitor/actions/index");
-  let { ACTIVITY_TYPE } = windowRequire("devtools/client/netmonitor/constants");
-  let { EVENTS } = windowRequire("devtools/client/netmonitor/events");
+  let { $, $all, EVENTS, ACTIVITY_TYPE, NetMonitorView, NetMonitorController,
+        gStore } = monitor.panelWin;
+  let { RequestsMenu } = NetMonitorView;
 
-  gStore.dispatch(Actions.batchEnable(false));
+  RequestsMenu.lazyUpdate = false;
 
   let wait = waitForEvents();
   yield performRequests();
   yield wait;
 
   info("Checking the image thumbnail when all items are shown.");
   checkImageThumbnail();
 
@@ -58,16 +58,16 @@ add_task(function* () {
   }
 
   function* reloadAndPerformRequests() {
     yield NetMonitorController.triggerActivity(ACTIVITY_TYPE.RELOAD.WITH_CACHE_ENABLED);
     yield performRequests();
   }
 
   function checkImageThumbnail() {
-    is(document.querySelectorAll(".requests-menu-icon[data-type=thumbnail]").length, 1,
+    is($all(".requests-menu-icon[data-type=thumbnail]").length, 1,
       "There should be only one image request with a thumbnail displayed.");
-    is(document.querySelector(".requests-menu-icon[data-type=thumbnail]").src, TEST_IMAGE_DATA_URI,
+    is($(".requests-menu-icon[data-type=thumbnail]").src, TEST_IMAGE_DATA_URI,
       "The image requests-menu-icon thumbnail is displayed correctly.");
-    is(document.querySelector(".requests-menu-icon[data-type=thumbnail]").hidden, false,
+    is($(".requests-menu-icon[data-type=thumbnail]").hidden, false,
       "The image requests-menu-icon thumbnail should not be hidden.");
   }
 });
--- a/devtools/client/netmonitor/test/browser_net_image-tooltip.js
+++ b/devtools/client/netmonitor/test/browser_net_image-tooltip.js
@@ -8,99 +8,94 @@ const IMAGE_TOOLTIP_REQUESTS = 1;
 
 /**
  * Tests if image responses show a popup in the requests menu when hovered.
  */
 add_task(function* test() {
   let { tab, monitor } = yield initNetMonitor(IMAGE_TOOLTIP_URL);
   info("Starting test... ");
 
-  let { document, gStore, windowRequire, NetMonitorController } = monitor.panelWin;
-  let Actions = windowRequire("devtools/client/netmonitor/actions/index");
-  let { ACTIVITY_TYPE } = windowRequire("devtools/client/netmonitor/constants");
-  let { EVENTS } = windowRequire("devtools/client/netmonitor/events");
-  let {
-    getDisplayedRequests,
-    getSortedRequests,
-  } = windowRequire("devtools/client/netmonitor/selectors/index");
-  let toolboxDoc = monitor._toolbox.doc;
-
-  gStore.dispatch(Actions.batchEnable(false));
+  let { $, EVENTS, ACTIVITY_TYPE, NetMonitorView, NetMonitorController } =
+    monitor.panelWin;
+  let { RequestsMenu } = NetMonitorView;
+  RequestsMenu.lazyUpdate = true;
 
   let onEvents = waitForNetworkEvents(monitor, IMAGE_TOOLTIP_REQUESTS);
   let onThumbnail = monitor.panelWin.once(EVENTS.RESPONSE_IMAGE_THUMBNAIL_DISPLAYED);
+
   yield performRequests();
   yield onEvents;
   yield onThumbnail;
 
   info("Checking the image thumbnail after a few requests were made...");
-  yield showTooltipAndVerify(toolboxDoc,
-    document.querySelectorAll(".request-list-item")[0]);
+  yield showTooltipAndVerify(RequestsMenu.tooltip, RequestsMenu.getItemAtIndex(0));
 
   // Hide tooltip before next test, to avoid the situation that tooltip covers
   // the icon for the request of the next test.
   info("Checking the image thumbnail gets hidden...");
-  yield hideTooltipAndVerify(monitor._toolbox.doc,
-    document.querySelectorAll(".request-list-item")[0]);
+  yield hideTooltipAndVerify(RequestsMenu.tooltip, RequestsMenu.getItemAtIndex(0));
 
   // +1 extra document reload
   onEvents = waitForNetworkEvents(monitor, IMAGE_TOOLTIP_REQUESTS + 1);
   onThumbnail = monitor.panelWin.once(EVENTS.RESPONSE_IMAGE_THUMBNAIL_DISPLAYED);
 
   info("Reloading the debuggee and performing all requests again...");
   yield NetMonitorController.triggerActivity(ACTIVITY_TYPE.RELOAD.WITH_CACHE_ENABLED);
   yield performRequests();
   yield onEvents;
   yield onThumbnail;
 
   info("Checking the image thumbnail after a reload.");
-  yield showTooltipAndVerify(toolboxDoc,
-    document.querySelectorAll(".request-list-item")[1]);
+  yield showTooltipAndVerify(RequestsMenu.tooltip, RequestsMenu.getItemAtIndex(1));
 
   info("Checking if the image thumbnail is hidden when mouse leaves the menu widget");
-  let requestsListContents = document.querySelector(".requests-menu-contents");
-  EventUtils.synthesizeMouse(requestsListContents, 0, 0, { type: "mouseout" }, monitor.panelWin);
-  yield waitUntil(() => !toolboxDoc.querySelector(".tooltip-container.tooltip-visible"));
+  let requestsMenuEl = $(".requests-menu-contents");
+  let onHidden = RequestsMenu.tooltip.once("hidden");
+  EventUtils.synthesizeMouse(requestsMenuEl, 0, 0, {type: "mouseout"}, monitor.panelWin);
+  yield onHidden;
 
   yield teardown(monitor);
 
   function performRequests() {
     return ContentTask.spawn(tab.linkedBrowser, {}, function* () {
       content.wrappedJSObject.performRequests();
     });
   }
 
   /**
-   * Show a tooltip on the {target} and verify that it was displayed
+   * Show a tooltip on the {requestItem} and verify that it was displayed
    * with the expected content.
    */
-  function* showTooltipAndVerify(toolboxDoc, target) {
-    let anchor = target.querySelector(".requests-menu-file");
-    yield showTooltipOn(toolboxDoc, anchor);
+  function* showTooltipAndVerify(tooltip, requestItem) {
+    let anchor = $(".requests-menu-file", getItemTarget(RequestsMenu, requestItem));
+    yield showTooltipOn(tooltip, anchor);
 
     info("Tooltip was successfully opened for the image request.");
-    is(toolboxDoc.querySelector(".tooltip-panel img").src, TEST_IMAGE_DATA_URI,
+    is(tooltip.panel.querySelector("img").src, TEST_IMAGE_DATA_URI,
       "The tooltip's image content is displayed correctly.");
   }
 
   /**
    * Trigger a tooltip over an element by sending mousemove event.
    * @return a promise that resolves when the tooltip is shown
    */
-  function* showTooltipOn(toolboxDoc, element) {
+  function showTooltipOn(tooltip, element) {
+    let onShown = tooltip.once("shown");
     let win = element.ownerDocument.defaultView;
-    EventUtils.synthesizeMouseAtCenter(element, { type: "mousemove" }, win);
-    yield waitUntil(() => toolboxDoc.querySelector(".tooltip-panel img"));
+    EventUtils.synthesizeMouseAtCenter(element, {type: "mousemove"}, win);
+    return onShown;
   }
 
   /**
-   * Hide a tooltip on the {target} and verify that it was closed.
+   * Hide a tooltip on the {requestItem} and verify that it was closed.
    */
-  function* hideTooltipAndVerify(toolboxDoc, target) {
+  function* hideTooltipAndVerify(tooltip, requestItem) {
     // Hovering over the "method" column hides the tooltip.
-    let anchor = target.querySelector(".requests-menu-method");
+    let anchor = $(".requests-menu-method", getItemTarget(RequestsMenu, requestItem));
+
+    let onTooltipHidden = tooltip.once("hidden");
     let win = anchor.ownerDocument.defaultView;
-    EventUtils.synthesizeMouseAtCenter(anchor, { type: "mousemove" }, win);
+    EventUtils.synthesizeMouseAtCenter(anchor, {type: "mousemove"}, win);
+    yield onTooltipHidden;
 
-    yield waitUntil(() => !toolboxDoc.querySelector(".tooltip-container.tooltip-visible"));
     info("Tooltip was successfully closed.");
   }
 });
--- a/devtools/client/netmonitor/test/browser_net_json-b64.js
+++ b/devtools/client/netmonitor/test/browser_net_json-b64.js
@@ -7,32 +7,31 @@
  * Tests if JSON responses encoded in base64 are handled correctly.
  */
 
 add_task(function* () {
   let { L10N } = require("devtools/client/netmonitor/l10n");
   let { tab, monitor } = yield initNetMonitor(JSON_B64_URL);
   info("Starting test... ");
 
-  let { document, gStore, windowRequire } = monitor.panelWin;
-  let Actions = windowRequire("devtools/client/netmonitor/actions/index");
+  let { document, NetMonitorView } = monitor.panelWin;
+  let { RequestsMenu } = NetMonitorView;
 
-  gStore.dispatch(Actions.batchEnable(false));
+  RequestsMenu.lazyUpdate = false;
 
   let wait = waitForNetworkEvents(monitor, 1);
   yield ContentTask.spawn(tab.linkedBrowser, {}, function* () {
     content.wrappedJSObject.performRequests();
   });
   yield wait;
 
   wait = waitForDOM(document, "#response-panel");
-  EventUtils.sendMouseEvent({ type: "click" },
+  EventUtils.sendMouseEvent({ type: "mousedown" },
     document.querySelector(".network-details-panel-toggle"));
-  EventUtils.sendMouseEvent({ type: "click" },
-    document.querySelector("#response-tab"));
+  document.querySelector("#response-tab").click();
   yield wait;
 
   let tabpanel = document.querySelector("#response-panel");
 
   is(tabpanel.querySelector(".response-error-header") === null, true,
     "The response error header doesn't have the intended visibility.");
   let jsonView = tabpanel.querySelector(".tree-section .treeLabel") || {};
   is(jsonView.textContent === L10N.getStr("jsonScopeName"), true,
--- a/devtools/client/netmonitor/test/browser_net_json-long.js
+++ b/devtools/client/netmonitor/test/browser_net_json-long.js
@@ -12,52 +12,42 @@ add_task(function* () {
 
   let { tab, monitor } = yield initNetMonitor(JSON_LONG_URL);
   info("Starting test... ");
 
   // This is receiving over 80 KB of json and will populate over 6000 items
   // in a variables view instance. Debug builds are slow.
   requestLongerTimeout(4);
 
-  let { document, gStore, windowRequire } = monitor.panelWin;
-  let Actions = windowRequire("devtools/client/netmonitor/actions/index");
-  let {
-    getDisplayedRequests,
-    getSortedRequests,
-  } = windowRequire("devtools/client/netmonitor/selectors/index");
+  let { document, NetMonitorView } = monitor.panelWin;
+  let { RequestsMenu } = NetMonitorView;
 
-  gStore.dispatch(Actions.batchEnable(false));
+  RequestsMenu.lazyUpdate = false;
 
   let wait = waitForNetworkEvents(monitor, 1);
   yield ContentTask.spawn(tab.linkedBrowser, {}, function* () {
     content.wrappedJSObject.performRequests();
   });
   yield wait;
 
-  verifyRequestItemTarget(
-    document,
-    getDisplayedRequests(gStore.getState()),
-    getSortedRequests(gStore.getState()).get(0),
-    "GET",
-    CONTENT_TYPE_SJS + "?fmt=json-long",
-    {
+  verifyRequestItemTarget(RequestsMenu, RequestsMenu.getItemAtIndex(0),
+    "GET", CONTENT_TYPE_SJS + "?fmt=json-long", {
       status: 200,
       statusText: "OK",
       type: "json",
       fullMimeType: "text/json; charset=utf-8",
       size: L10N.getFormatStr("networkMenu.sizeKB",
         L10N.numberWithDecimals(85975 / 1024, 2)),
       time: true
     });
 
   wait = waitForDOM(document, "#response-panel");
-  EventUtils.sendMouseEvent({ type: "click" },
+  EventUtils.sendMouseEvent({ type: "mousedown" },
     document.querySelector(".network-details-panel-toggle"));
-  EventUtils.sendMouseEvent({ type: "click" },
-    document.querySelector("#response-tab"));
+  document.querySelector("#response-tab").click();
   yield wait;
 
   testResponseTab();
 
   yield teardown(monitor);
 
   function testResponseTab() {
     let tabpanel = document.querySelector("#response-panel");
--- a/devtools/client/netmonitor/test/browser_net_json-malformed.js
+++ b/devtools/client/netmonitor/test/browser_net_json-malformed.js
@@ -7,49 +7,39 @@
  * Tests if malformed JSON responses are handled correctly.
  */
 
 add_task(function* () {
   let { L10N } = require("devtools/client/netmonitor/l10n");
   let { tab, monitor } = yield initNetMonitor(JSON_MALFORMED_URL);
   info("Starting test... ");
 
-  let { document, gStore, windowRequire } = monitor.panelWin;
-  let Actions = windowRequire("devtools/client/netmonitor/actions/index");
-  let {
-    getDisplayedRequests,
-    getSortedRequests,
-  } = windowRequire("devtools/client/netmonitor/selectors/index");
+  let { document, NetMonitorView } = monitor.panelWin;
+  let { RequestsMenu } = NetMonitorView;
 
-  gStore.dispatch(Actions.batchEnable(false));
+  RequestsMenu.lazyUpdate = false;
 
   let wait = waitForNetworkEvents(monitor, 1);
   yield ContentTask.spawn(tab.linkedBrowser, {}, function* () {
     content.wrappedJSObject.performRequests();
   });
   yield wait;
 
-  verifyRequestItemTarget(
-    document,
-    getDisplayedRequests(gStore.getState()),
-    getSortedRequests(gStore.getState()).get(0),
-    "GET",
-    CONTENT_TYPE_SJS + "?fmt=json-malformed",
-    {
+  verifyRequestItemTarget(RequestsMenu, RequestsMenu.getItemAtIndex(0),
+    "GET", CONTENT_TYPE_SJS + "?fmt=json-malformed", {
       status: 200,
       statusText: "OK",
       type: "json",
       fullMimeType: "text/json; charset=utf-8"
     });
 
   wait = waitForDOM(document, "#response-panel .editor-mount iframe");
-  EventUtils.sendMouseEvent({ type: "click" },
+  EventUtils.sendMouseEvent({ type: "mousedown" },
     document.querySelector(".network-details-panel-toggle"));
-  EventUtils.sendMouseEvent({ type: "click" },
-    document.querySelector("#response-tab"));
+  document.querySelector("#response-tab").click();
   let [editor] = yield wait;
   yield once(editor, "DOMContentLoaded");
   yield waitForDOM(editor.contentDocument, ".CodeMirror-code");
 
   let tabpanel = document.querySelector("#response-panel");
   is(tabpanel.querySelector(".response-error-header") === null, false,
     "The response error header doesn't have the intended visibility.");
   is(tabpanel.querySelector(".response-error-header").textContent,
--- a/devtools/client/netmonitor/test/browser_net_json-null.js
+++ b/devtools/client/netmonitor/test/browser_net_json-null.js
@@ -8,24 +8,20 @@ const { L10N } = require("devtools/clien
 /**
  * Tests if JSON responses containing null values are properly displayed.
  */
 
 add_task(function* () {
   let { tab, monitor } = yield initNetMonitor(JSON_BASIC_URL + "?name=null");
   info("Starting test... ");
 
-  let { document, gStore, windowRequire } = monitor.panelWin;
-  let Actions = windowRequire("devtools/client/netmonitor/actions/index");
-  let {
-    getDisplayedRequests,
-    getSortedRequests,
-  } = windowRequire("devtools/client/netmonitor/selectors/index");
+  let { document, NetMonitorView } = monitor.panelWin;
+  let { RequestsMenu } = NetMonitorView;
 
-  gStore.dispatch(Actions.batchEnable(false));
+  RequestsMenu.lazyUpdate = false;
 
   let wait = waitForNetworkEvents(monitor, 1);
   yield ContentTask.spawn(tab.linkedBrowser, {}, function* () {
     content.wrappedJSObject.performRequests();
   });
   yield wait;
 
   yield openResponsePanel(document);
@@ -68,14 +64,15 @@ function checkResponsePanelDisplaysJSON(
 }
 
 /**
  * Open the netmonitor details panel and switch to the response tab.
  * Returns a promise that will resolve when the response panel DOM element is available.
  */
 function openResponsePanel(document) {
   let onReponsePanelReady = waitForDOM(document, "#response-panel");
-  EventUtils.sendMouseEvent({ type: "click" },
-    document.querySelector(".network-details-panel-toggle"));
-  EventUtils.sendMouseEvent({ type: "click" },
-    document.querySelector("#response-tab"));
+  EventUtils.sendMouseEvent(
+    { type: "mousedown" },
+    document.querySelector(".network-details-panel-toggle")
+  );
+  document.querySelector("#response-tab").click();
   return onReponsePanelReady;
 }
--- a/devtools/client/netmonitor/test/browser_net_json_custom_mime.js
+++ b/devtools/client/netmonitor/test/browser_net_json_custom_mime.js
@@ -8,51 +8,41 @@
  */
 
 add_task(function* () {
   let { L10N } = require("devtools/client/netmonitor/l10n");
 
   let { tab, monitor } = yield initNetMonitor(JSON_CUSTOM_MIME_URL);
   info("Starting test... ");
 
-  let { document, gStore, windowRequire } = monitor.panelWin;
-  let Actions = windowRequire("devtools/client/netmonitor/actions/index");
-  let {
-    getDisplayedRequests,
-    getSortedRequests,
-  } = windowRequire("devtools/client/netmonitor/selectors/index");
+  let { document, NetMonitorView } = monitor.panelWin;
+  let { RequestsMenu } = NetMonitorView;
 
-  gStore.dispatch(Actions.batchEnable(false));
+  RequestsMenu.lazyUpdate = false;
 
   let wait = waitForNetworkEvents(monitor, 1);
   yield ContentTask.spawn(tab.linkedBrowser, {}, function* () {
     content.wrappedJSObject.performRequests();
   });
   yield wait;
 
-  verifyRequestItemTarget(
-    document,
-    getDisplayedRequests(gStore.getState()),
-    getSortedRequests(gStore.getState()).get(0),
-    "GET",
-    CONTENT_TYPE_SJS + "?fmt=json-custom-mime",
-    {
+  verifyRequestItemTarget(RequestsMenu, RequestsMenu.getItemAtIndex(0),
+    "GET", CONTENT_TYPE_SJS + "?fmt=json-custom-mime", {
       status: 200,
       statusText: "OK",
       type: "x-bigcorp-json",
       fullMimeType: "text/x-bigcorp-json; charset=utf-8",
       size: L10N.getFormatStrWithNumbers("networkMenu.sizeB", 41),
       time: true
     });
 
   wait = waitForDOM(document, "#response-panel");
-  EventUtils.sendMouseEvent({ type: "click" },
+  EventUtils.sendMouseEvent({ type: "mousedown" },
     document.querySelector(".network-details-panel-toggle"));
-  EventUtils.sendMouseEvent({ type: "click" },
-    document.querySelector("#response-tab"));
+  document.querySelector("#response-tab").click();
   yield wait;
 
   testResponseTab();
 
   yield teardown(monitor);
 
   function testResponseTab() {
     let tabpanel = document.querySelector("#response-panel");
--- a/devtools/client/netmonitor/test/browser_net_json_text_mime.js
+++ b/devtools/client/netmonitor/test/browser_net_json_text_mime.js
@@ -8,51 +8,41 @@
  */
 
 add_task(function* () {
   let { L10N } = require("devtools/client/netmonitor/l10n");
 
   let { tab, monitor } = yield initNetMonitor(JSON_TEXT_MIME_URL);
   info("Starting test... ");
 
-  let { document, gStore, windowRequire } = monitor.panelWin;
-  let Actions = windowRequire("devtools/client/netmonitor/actions/index");
-  let {
-    getDisplayedRequests,
-    getSortedRequests,
-  } = windowRequire("devtools/client/netmonitor/selectors/index");
+  let { document, NetMonitorView } = monitor.panelWin;
+  let { RequestsMenu } = NetMonitorView;
 
-  gStore.dispatch(Actions.batchEnable(false));
+  RequestsMenu.lazyUpdate = false;
 
   let wait = waitForNetworkEvents(monitor, 1);
   yield ContentTask.spawn(tab.linkedBrowser, {}, function* () {
     content.wrappedJSObject.performRequests();
   });
   yield wait;
 
-  verifyRequestItemTarget(
-    document,
-    getDisplayedRequests(gStore.getState()),
-    getSortedRequests(gStore.getState()).get(0),
-    "GET",
-    CONTENT_TYPE_SJS + "?fmt=json-text-mime",
-    {
+  verifyRequestItemTarget(RequestsMenu, RequestsMenu.getItemAtIndex(0),
+    "GET", CONTENT_TYPE_SJS + "?fmt=json-text-mime", {
       status: 200,
       statusText: "OK",
       type: "plain",
       fullMimeType: "text/plain; charset=utf-8",
       size: L10N.getFormatStrWithNumbers("networkMenu.sizeB", 41),
       time: true
     });
 
   wait = waitForDOM(document, "#response-panel");
-  EventUtils.sendMouseEvent({ type: "click" },
+  EventUtils.sendMouseEvent({ type: "mousedown" },
     document.querySelector(".network-details-panel-toggle"));
-  EventUtils.sendMouseEvent({ type: "click" },
-    document.querySelector("#response-tab"));
+  document.querySelector("#response-tab").click();
   yield wait;
 
   testResponseTab();
 
   yield teardown(monitor);
 
   function testResponseTab() {
     let tabpanel = document.querySelector("#response-panel");
--- a/devtools/client/netmonitor/test/browser_net_jsonp.js
+++ b/devtools/client/netmonitor/test/browser_net_jsonp.js
@@ -8,72 +8,56 @@
  */
 
 add_task(function* () {
   let { L10N } = require("devtools/client/netmonitor/l10n");
 
   let { tab, monitor } = yield initNetMonitor(JSONP_URL);
   info("Starting test... ");
 
-  let { document, gStore, windowRequire } = monitor.panelWin;
-  let Actions = windowRequire("devtools/client/netmonitor/actions/index");
-  let {
-    getDisplayedRequests,
-    getSortedRequests,
-  } = windowRequire("devtools/client/netmonitor/selectors/index");
+  let { document, NetMonitorView } = monitor.panelWin;
+  let { RequestsMenu } = NetMonitorView;
 
-  gStore.dispatch(Actions.batchEnable(false));
+  RequestsMenu.lazyUpdate = false;
 
   let wait = waitForNetworkEvents(monitor, 2);
   yield ContentTask.spawn(tab.linkedBrowser, {}, function* () {
     content.wrappedJSObject.performRequests();
   });
   yield wait;
 
-  verifyRequestItemTarget(
-    document,
-    getDisplayedRequests(gStore.getState()),
-    getSortedRequests(gStore.getState()).get(0),
-    "GET",
-    CONTENT_TYPE_SJS + "?fmt=jsonp&jsonp=$_0123Fun",
-    {
+  verifyRequestItemTarget(RequestsMenu, RequestsMenu.getItemAtIndex(0),
+    "GET", CONTENT_TYPE_SJS + "?fmt=jsonp&jsonp=$_0123Fun", {
       status: 200,
       statusText: "OK",
       type: "json",
       fullMimeType: "text/json; charset=utf-8",
       size: L10N.getFormatStrWithNumbers("networkMenu.sizeB", 41),
       time: true
     });
-  verifyRequestItemTarget(
-    document,
-    getDisplayedRequests(gStore.getState()),
-    getSortedRequests(gStore.getState()).get(1),
-    "GET",
-    CONTENT_TYPE_SJS + "?fmt=jsonp2&jsonp=$_4567Sad",
-    {
+  verifyRequestItemTarget(RequestsMenu, RequestsMenu.getItemAtIndex(1),
+    "GET", CONTENT_TYPE_SJS + "?fmt=jsonp2&jsonp=$_4567Sad", {
       status: 200,
       statusText: "OK",
       type: "json",
       fullMimeType: "text/json; charset=utf-8",
       size: L10N.getFormatStrWithNumbers("networkMenu.sizeB", 54),
       time: true
     });
 
   wait = waitForDOM(document, "#response-panel");
-  EventUtils.sendMouseEvent({ type: "click" },
+  EventUtils.sendMouseEvent({ type: "mousedown" },
     document.querySelector(".network-details-panel-toggle"));
-  EventUtils.sendMouseEvent({ type: "click" },
-    document.querySelector("#response-tab"));
+  document.querySelector("#response-tab").click();
   yield wait;
 
   testResponseTab("$_0123Fun", "\"Hello JSONP!\"");
 
   wait = waitForDOM(document, "#response-panel .tree-section");
-  EventUtils.sendMouseEvent({ type: "mousedown" },
-    document.querySelectorAll(".request-list-item")[1]);
+  RequestsMenu.selectedIndex = 1;
   yield wait;
 
   testResponseTab("$_4567Sad", "\"Hello weird JSONP!\"");
 
   yield teardown(monitor);
 
   function testResponseTab(func, greeting) {
     let tabpanel = document.querySelector("#response-panel");
--- a/devtools/client/netmonitor/test/browser_net_large-response.js
+++ b/devtools/client/netmonitor/test/browser_net_large-response.js
@@ -12,47 +12,37 @@ const HTML_LONG_URL = CONTENT_TYPE_SJS +
 add_task(function* () {
   let { tab, monitor } = yield initNetMonitor(CUSTOM_GET_URL);
   info("Starting test... ");
 
   // This test could potentially be slow because over 100 KB of stuff
   // is going to be requested and displayed in the source editor.
   requestLongerTimeout(2);
 
-  let { document, gStore, windowRequire } = monitor.panelWin;
-  let Actions = windowRequire("devtools/client/netmonitor/actions/index");
-  let {
-    getDisplayedRequests,
-    getSortedRequests,
-  } = windowRequire("devtools/client/netmonitor/selectors/index");
+  let { document, NetMonitorView } = monitor.panelWin;
+  let { RequestsMenu } = NetMonitorView;
 
-  gStore.dispatch(Actions.batchEnable(false));
+  RequestsMenu.lazyUpdate = false;
 
   let wait = waitForNetworkEvents(monitor, 1);
   yield ContentTask.spawn(tab.linkedBrowser, HTML_LONG_URL, function* (url) {
     content.wrappedJSObject.performRequests(1, url);
   });
   yield wait;
 
-  verifyRequestItemTarget(
-    document,
-    getDisplayedRequests(gStore.getState()),
-    getSortedRequests(gStore.getState()).get(0),
-    "GET",
-    CONTENT_TYPE_SJS + "?fmt=html-long",
-    {
+  verifyRequestItemTarget(RequestsMenu, RequestsMenu.getItemAtIndex(0),
+    "GET", CONTENT_TYPE_SJS + "?fmt=html-long", {
       status: 200,
       statusText: "OK"
     });
 
   let waitDOM = waitForDOM(document, "#response-panel .editor-mount iframe");
-  EventUtils.sendMouseEvent({ type: "click" },
+  EventUtils.sendMouseEvent({ type: "mousedown" },
     document.querySelector(".network-details-panel-toggle"));
-  EventUtils.sendMouseEvent({ type: "click" },
-    document.querySelector("#response-tab"));
+  document.querySelector("#response-tab").click();
   let [editor] = yield waitDOM;
   yield once(editor, "DOMContentLoaded");
   yield waitForDOM(editor.contentDocument, ".CodeMirror-code");
 
   let text = editor.contentDocument
         .querySelector(".CodeMirror-line").textContent;
 
   ok(text.match(/^<p>/), "The text shown in the source editor is incorrect.");
--- a/devtools/client/netmonitor/test/browser_net_open_request_in_tab.js
+++ b/devtools/client/netmonitor/test/browser_net_open_request_in_tab.js
@@ -6,41 +6,32 @@
 /**
  * Tests if Open in new tab works.
  */
 
 add_task(function* () {
   let { tab, monitor } = yield initNetMonitor(CUSTOM_GET_URL);
   info("Starting test...");
 
-  let { document, gStore, windowRequire } = monitor.panelWin;
-  let Actions = windowRequire("devtools/client/netmonitor/actions/index");
-  let {
-    getDisplayedRequests,
-    getSortedRequests,
-  } = windowRequire("devtools/client/netmonitor/selectors/index");
+  let { NetMonitorView } = monitor.panelWin;
+  let { RequestsMenu } = NetMonitorView;
 
-  gStore.dispatch(Actions.batchEnable(false));
+  RequestsMenu.lazyUpdate = false;
 
   let wait = waitForNetworkEvents(monitor, 1);
   yield ContentTask.spawn(tab.linkedBrowser, {}, function* () {
     content.wrappedJSObject.performRequests(1);
   });
   yield wait;
 
-  EventUtils.sendMouseEvent({ type: "mousedown" },
-    document.querySelectorAll(".request-list-item")[0]);
-  EventUtils.sendMouseEvent({ type: "contextmenu" },
-    document.querySelectorAll(".request-list-item")[0]);
+  let requestItem = RequestsMenu.getItemAtIndex(0);
+  RequestsMenu.selectedItem = requestItem;
 
   let onTabOpen = once(gBrowser.tabContainer, "TabOpen", false);
-  // Context menu is appending in XUL document, we must select it from
-  // _toolbox.doc
-  monitor._toolbox.doc
-    .querySelector("#request-menu-context-newtab").click();
+  RequestsMenu.contextMenu.openRequestInTab();
   yield onTabOpen;
 
   ok(true, "A new tab has been opened");
 
   yield teardown(monitor);
 
   gBrowser.removeCurrentTab();
 });
--- a/devtools/client/netmonitor/test/browser_net_page-nav.js
+++ b/devtools/client/netmonitor/test/browser_net_page-nav.js
@@ -7,18 +7,17 @@
  * Tests if page navigation ("close", "navigate", etc.) triggers an appropriate
  * action in the network monitor.
  */
 
 add_task(function* () {
   let { tab, monitor } = yield initNetMonitor(SIMPLE_URL);
   info("Starting test... ");
 
-  let { windowRequire } = monitor.panelWin;
-  let { EVENTS } = windowRequire("devtools/client/netmonitor/events");
+  let { EVENTS } = monitor.panelWin;
 
   yield testNavigate();
   yield testNavigateBack();
   yield testClose();
 
   function* testNavigate() {
     info("Navigating forward...");
 
--- a/devtools/client/netmonitor/test/browser_net_pane-collapse.js
+++ b/devtools/client/netmonitor/test/browser_net_pane-collapse.js
@@ -6,44 +6,43 @@
 /**
  * Tests if the network monitor panes collapse properly.
  */
 
 add_task(function* () {
   let { tab, monitor } = yield initNetMonitor(SIMPLE_URL);
   info("Starting test... ");
 
-  let { document, windowRequire } = monitor.panelWin;
-  let { Prefs } = windowRequire("devtools/client/netmonitor/prefs");
+  let { document, Prefs } = monitor.panelWin;
   let detailsPaneToggleButton = document.querySelector(".network-details-panel-toggle");
 
   let wait = waitForNetworkEvents(monitor, 1);
   tab.linkedBrowser.reload();
   yield wait;
 
   ok(!document.querySelector(".network-details-panel") &&
      detailsPaneToggleButton.classList.contains("pane-collapsed"),
     "The details panel should initially be hidden.");
 
-  EventUtils.sendMouseEvent({ type: "click" }, detailsPaneToggleButton);
+  EventUtils.sendMouseEvent({ type: "mousedown" }, detailsPaneToggleButton);
 
   is(~~(document.querySelector(".network-details-panel").clientWidth),
     Prefs.networkDetailsWidth,
     "The details panel has an incorrect width.");
   ok(document.querySelector(".network-details-panel") &&
      !detailsPaneToggleButton.classList.contains("pane-collapsed"),
     "The details panel should at this point be visible.");
 
-  EventUtils.sendMouseEvent({ type: "click" }, detailsPaneToggleButton);
+  EventUtils.sendMouseEvent({ type: "mousedown" }, detailsPaneToggleButton);
 
   ok(!document.querySelector(".network-details-panel") &&
      detailsPaneToggleButton.classList.contains("pane-collapsed"),
     "The details panel should not be visible after collapsing.");
 
-  EventUtils.sendMouseEvent({ type: "click" }, detailsPaneToggleButton);
+  EventUtils.sendMouseEvent({ type: "mousedown" }, detailsPaneToggleButton);
 
   is(~~(document.querySelector(".network-details-panel").clientWidth),
     Prefs.networkDetailsWidth,
     "The details panel has an incorrect width after uncollapsing.");
   ok(document.querySelector(".network-details-panel") &&
      !detailsPaneToggleButton.classList.contains("pane-collapsed"),
     "The details panel should be visible again after uncollapsing.");
 
--- a/devtools/client/netmonitor/test/browser_net_pane-toggle.js
+++ b/devtools/client/netmonitor/test/browser_net_pane-toggle.js
@@ -6,79 +6,67 @@
 /**
  * Tests if toggling the details pane works as expected.
  */
 
 add_task(function* () {
   let { tab, monitor } = yield initNetMonitor(SIMPLE_URL);
   info("Starting test... ");
 
-  let { document, gStore, windowRequire } = monitor.panelWin;
-  let Actions = windowRequire("devtools/client/netmonitor/actions/index");
-  let { EVENTS } = windowRequire("devtools/client/netmonitor/events");
-  let {
-    getSelectedRequest,
-    getSortedRequests,
-  } = windowRequire("devtools/client/netmonitor/selectors/index");
-
-  gStore.dispatch(Actions.batchEnable(false));
+  let { document, NetMonitorView } = monitor.panelWin;
+  let { RequestsMenu } = NetMonitorView;
+  let { NETWORK_EVENT } = monitor.panelWin.EVENTS;
+  RequestsMenu.lazyUpdate = false;
 
   let toggleButton = document.querySelector(".network-details-panel-toggle");
 
   is(toggleButton.hasAttribute("disabled"), true,
     "The pane toggle button should be disabled when the frontend is opened.");
   is(toggleButton.classList.contains("pane-collapsed"), true,
     "The pane toggle button should indicate that the details pane is " +
     "collapsed when the frontend is opened.");
   is(!!document.querySelector(".network-details-panel"), false,
     "The details pane should be hidden when the frontend is opened.");
-  is(getSelectedRequest(gStore.getState()), null,
+  is(RequestsMenu.selectedItem, null,
     "There should be no selected item in the requests menu.");
 
-  let networkEvent = monitor.panelWin.once(EVENTS.NETWORK_EVENT);
+  let networkEvent = monitor.panelWin.once(NETWORK_EVENT);
   tab.linkedBrowser.reload();
   yield networkEvent;
 
   is(toggleButton.hasAttribute("disabled"), false,
     "The pane toggle button should be enabled after the first request.");
   is(toggleButton.classList.contains("pane-collapsed"), true,
     "The pane toggle button should still indicate that the details pane is " +
     "collapsed after the first request.");
   is(!!document.querySelector(".network-details-panel"), false,
     "The details pane should still be hidden after the first request.");
-  is(getSelectedRequest(gStore.getState()), null,
+  is(RequestsMenu.selectedItem, null,
     "There should still be no selected item in the requests menu.");
 
-  EventUtils.sendMouseEvent({ type: "click" }, toggleButton);
+  EventUtils.sendMouseEvent({ type: "mousedown" }, toggleButton);
 
   is(toggleButton.hasAttribute("disabled"), false,
     "The pane toggle button should still be enabled after being pressed.");
   is(toggleButton.classList.contains("pane-collapsed"), false,
     "The pane toggle button should now indicate that the details pane is " +
     "not collapsed anymore after being pressed.");
   is(!!document.querySelector(".network-details-panel"), true,
     "The details pane should not be hidden after toggle button was pressed.");
-  isnot(getSelectedRequest(gStore.getState()), null,
+  isnot(RequestsMenu.selectedItem, null,
     "There should be a selected item in the requests menu.");
-  is(getSelectedIndex(gStore.getState()), 0,
+  is(RequestsMenu.selectedIndex, 0,
     "The first item should be selected in the requests menu.");
 
-  EventUtils.sendMouseEvent({ type: "click" }, toggleButton);
+  EventUtils.sendMouseEvent({ type: "mousedown" }, toggleButton);
 
   is(toggleButton.hasAttribute("disabled"), false,
     "The pane toggle button should still be enabled after being pressed again.");
   is(toggleButton.classList.contains("pane-collapsed"), true,
     "The pane toggle button should now indicate that the details pane is " +
     "collapsed after being pressed again.");
   is(!!document.querySelector(".network-details-panel"), false,
     "The details pane should now be hidden after the toggle button was pressed again.");
-  is(getSelectedRequest(gStore.getState()), null,
+  is(RequestsMenu.selectedItem, null,
     "There should now be no selected item in the requests menu.");
 
   yield teardown(monitor);
-
-  function getSelectedIndex(state) {
-    if (!state.requests.selectedId) {
-      return -1;
-    }
-    return getSortedRequests(state).findIndex(r => r.id === state.requests.selectedId);
-  }
 });
--- a/devtools/client/netmonitor/test/browser_net_persistent_logs.js
+++ b/devtools/client/netmonitor/test/browser_net_persistent_logs.js
@@ -7,39 +7,40 @@
  * Tests if the network monitor leaks on initialization and sudden destruction.
  * You can also use this initialization format as a template for other tests.
  */
 
 add_task(function* () {
   let { tab, monitor } = yield initNetMonitor(SINGLE_GET_URL);
   info("Starting test... ");
 
-  let { document, windowRequire } = monitor.panelWin;
+  let { NetMonitorView } = monitor.panelWin;
+  let { RequestsMenu } = NetMonitorView;
 
   Services.prefs.setBoolPref("devtools.webconsole.persistlog", false);
 
   yield reloadAndWait();
 
-  is(document.querySelectorAll(".request-list-item").length, 2,
-    "The request list should have two items at this point.");
+  is(RequestsMenu.itemCount, 2,
+    "The request menu should have two items at this point.");
 
   yield reloadAndWait();
 
   // Since the reload clears the log, we still expect two requests in the log
-  is(document.querySelectorAll(".request-list-item").length, 2,
-    "The request list should still have two items at this point.");
+  is(RequestsMenu.itemCount, 2,
+    "The request menu should still have two items at this point.");
 
   // Now we toggle the persistence logs on
   Services.prefs.setBoolPref("devtools.webconsole.persistlog", true);
 
   yield reloadAndWait();
 
   // Since we togged the persistence logs, we expect four items after the reload
-  is(document.querySelectorAll(".request-list-item").length, 4,
-    "The request list should now have four items at this point.");
+  is(RequestsMenu.itemCount, 4,
+    "The request menu should now have four items at this point.");
 
   Services.prefs.setBoolPref("devtools.webconsole.persistlog", false);
   return teardown(monitor);
 
   /**
    * Reload the page and wait for 2 GET requests. Race-free.
    */
   function reloadAndWait() {
--- a/devtools/client/netmonitor/test/browser_net_post-data-01.js
+++ b/devtools/client/netmonitor/test/browser_net_post-data-01.js
@@ -11,74 +11,58 @@ add_task(function* () {
   let { L10N } = require("devtools/client/netmonitor/l10n");
 
   // Set a higher panel height in order to get full CodeMirror content
   Services.prefs.setIntPref("devtools.toolbox.footer.height", 400);
 
   let { tab, monitor } = yield initNetMonitor(POST_DATA_URL);
   info("Starting test... ");
 
-  let { document, gStore, windowRequire } = monitor.panelWin;
-  let Actions = windowRequire("devtools/client/netmonitor/actions/index");
-  let {
-    getDisplayedRequests,
-    getSortedRequests,
-  } = windowRequire("devtools/client/netmonitor/selectors/index");
+  let { document, NetMonitorView } = monitor.panelWin;
+  let { RequestsMenu } = NetMonitorView;
 
-  gStore.dispatch(Actions.batchEnable(false));
+  RequestsMenu.lazyUpdate = false;
 
   let wait = waitForNetworkEvents(monitor, 0, 2);
   yield ContentTask.spawn(tab.linkedBrowser, {}, function* () {
     content.wrappedJSObject.performRequests();
   });
   yield wait;
 
-  verifyRequestItemTarget(
-    document,
-    getDisplayedRequests(gStore.getState()),
-    getSortedRequests(gStore.getState()).get(0),
-    "POST",
-    SIMPLE_SJS + "?foo=bar&baz=42&type=urlencoded",
-    {
+  verifyRequestItemTarget(RequestsMenu, RequestsMenu.getItemAtIndex(0),
+    "POST", SIMPLE_SJS + "?foo=bar&baz=42&type=urlencoded", {
       status: 200,
       statusText: "Och Aye",
       type: "plain",
       fullMimeType: "text/plain; charset=utf-8",
       size: L10N.getFormatStrWithNumbers("networkMenu.sizeB", 12),
       time: true
     });
-   verifyRequestItemTarget(
-    document,
-    getDisplayedRequests(gStore.getState()),
-    getSortedRequests(gStore.getState()).get(1),
-    "POST",
-    SIMPLE_SJS + "?foo=bar&baz=42&type=multipart",
-    {
+  verifyRequestItemTarget(RequestsMenu, RequestsMenu.getItemAtIndex(1),
+    "POST", SIMPLE_SJS + "?foo=bar&baz=42&type=multipart", {
       status: 200,
       statusText: "Och Aye",
       type: "plain",
       fullMimeType: "text/plain; charset=utf-8",
       size: L10N.getFormatStrWithNumbers("networkMenu.sizeB", 12),
       time: true
     });
 
   // Wait for all tree sections updated by react
   wait = waitForDOM(document, "#params-panel .tree-section", 2);
   EventUtils.sendMouseEvent({ type: "mousedown" },
-    document.querySelectorAll(".request-list-item")[0]);
-  EventUtils.sendMouseEvent({ type: "click" },
-    document.querySelector("#params-tab"));
+    document.querySelector(".network-details-panel-toggle"));
+  document.querySelector("#params-tab").click();
   yield wait;
   yield testParamsTab("urlencoded");
 
   // Wait for all tree sections and editor updated by react
   let waitForSections = waitForDOM(document, "#params-panel .tree-section", 2);
   let waitForEditor = waitForDOM(document, "#params-panel .editor-mount iframe");
-  EventUtils.sendMouseEvent({ type: "mousedown" },
-    document.querySelectorAll(".request-list-item")[1]);
+  RequestsMenu.selectedIndex = 1;
   let [, editorFrames] = yield Promise.all([waitForSections, waitForEditor]);
   yield once(editorFrames[0], "DOMContentLoaded");
   yield waitForDOM(editorFrames[0].contentDocument, ".CodeMirror-code");
   yield testParamsTab("multipart");
 
   return teardown(monitor);
 
   function* testParamsTab(type) {
--- a/devtools/client/netmonitor/test/browser_net_post-data-02.js
+++ b/devtools/client/netmonitor/test/browser_net_post-data-02.js
@@ -9,37 +9,32 @@
  */
 
 add_task(function* () {
   let { L10N } = require("devtools/client/netmonitor/l10n");
 
   let { tab, monitor } = yield initNetMonitor(POST_RAW_URL);
   info("Starting test... ");
 
-  let { document, gStore, windowRequire } = monitor.panelWin;
-  let Actions = windowRequire("devtools/client/netmonitor/actions/index");
-  let {
-    getDisplayedRequests,
-    getSortedRequests,
-  } = windowRequire("devtools/client/netmonitor/selectors/index");
+  let { document, NetMonitorView } = monitor.panelWin;
+  let { RequestsMenu } = NetMonitorView;
 
-  gStore.dispatch(Actions.batchEnable(false));
+  RequestsMenu.lazyUpdate = false;
 
   let wait = waitForNetworkEvents(monitor, 0, 1);
   yield ContentTask.spawn(tab.linkedBrowser, {}, function* () {
     content.wrappedJSObject.performRequests();
   });
   yield wait;
 
   // Wait for all tree view updated by react
   wait = waitForDOM(document, "#params-panel .tree-section");
   EventUtils.sendMouseEvent({ type: "mousedown" },
-    document.querySelectorAll(".request-list-item")[0]);
-  EventUtils.sendMouseEvent({ type: "click" },
-    document.querySelector("#params-tab"));
+    document.querySelector(".network-details-panel-toggle"));
+  document.querySelector("#params-tab").click();
   yield wait;
 
   let tabpanel = document.querySelector("#params-panel");
 
   ok(tabpanel.querySelector(".treeTable"),
     "The request params doesn't have the indended visibility.");
   ok(tabpanel.querySelector(".editor-mount") === null,
     "The request post data doesn't have the indended visibility.");
--- a/devtools/client/netmonitor/test/browser_net_post-data-03.js
+++ b/devtools/client/netmonitor/test/browser_net_post-data-03.js
@@ -9,33 +9,32 @@
  */
 
 add_task(function* () {
   let { L10N } = require("devtools/client/netmonitor/l10n");
 
   let { tab, monitor } = yield initNetMonitor(POST_RAW_WITH_HEADERS_URL);
   info("Starting test... ");
 
-  let { document, gStore, windowRequire } = monitor.panelWin;
-  let Actions = windowRequire("devtools/client/netmonitor/actions/index");
+  let { document, NetMonitorView } = monitor.panelWin;
+  let { RequestsMenu } = NetMonitorView;
 
-  gStore.dispatch(Actions.batchEnable(false));
+  RequestsMenu.lazyUpdate = false;
 
   let wait = waitForNetworkEvents(monitor, 0, 1);
   yield ContentTask.spawn(tab.linkedBrowser, {}, function* () {
     content.wrappedJSObject.performRequests();
   });
   yield wait;
 
   // Wait for all tree view updated by react
   wait = waitForDOM(document, "#headers-panel");
-  EventUtils.sendMouseEvent({ type: "click" },
+  EventUtils.sendMouseEvent({ type: "mousedown" },
     document.querySelector(".network-details-panel-toggle"));
-  EventUtils.sendMouseEvent({ type: "click" },
-    document.querySelector("#headers-tab"));
+  document.querySelector("#headers-tab").click();
   yield wait;
 
   let tabpanel = document.querySelector("#headers-panel");
 
   is(tabpanel.querySelectorAll(".tree-section .treeLabel").length, 3,
     "There should be 3 header sections displayed in this tabpanel.");
 
   is(tabpanel.querySelectorAll(".tree-section .treeLabel")[2].textContent,
@@ -54,18 +53,17 @@ add_task(function* () {
     "The first request header value was incorrect.");
   is(labels[labels.length - 1].textContent, "custom-header",
     "The second request header name was incorrect.");
   is(values[values.length - 1].textContent, "\"hello world!\"",
     "The second request header value was incorrect.");
 
   // Wait for all tree sections updated by react
   wait = waitForDOM(document, "#params-panel .tree-section");
-  EventUtils.sendMouseEvent({ type: "click" },
-    document.querySelector("#params-tab"));
+  document.querySelector("#params-tab").click();
   yield wait;
 
   tabpanel = document.querySelector("#params-panel");
 
   ok(tabpanel.querySelector(".treeTable"),
     "The params tree view should be displayed.");
   ok(tabpanel.querySelector(".editor-mount") === null,
     "The post data shouldn't be displayed.");
--- a/devtools/client/netmonitor/test/browser_net_post-data-04.js
+++ b/devtools/client/netmonitor/test/browser_net_post-data-04.js
@@ -9,33 +9,32 @@
  */
 
 add_task(function* () {
   let { L10N } = require("devtools/client/netmonitor/l10n");
 
   let { tab, monitor } = yield initNetMonitor(POST_JSON_URL);
   info("Starting test... ");
 
-  let { document, gStore, windowRequire } = monitor.panelWin;
-  let Actions = windowRequire("devtools/client/netmonitor/actions/index");
+  let { document, NetMonitorView } = monitor.panelWin;
+  let { RequestsMenu } = NetMonitorView;
 
-  gStore.dispatch(Actions.batchEnable(false));
+  RequestsMenu.lazyUpdate = false;
 
   let wait = waitForNetworkEvents(monitor, 0, 1);
   yield ContentTask.spawn(tab.linkedBrowser, {}, function* () {
     content.wrappedJSObject.performRequests();
   });
   yield wait;
 
   // Wait for all tree view updated by react
   wait = waitForDOM(document, "#params-panel .tree-section");
-  EventUtils.sendMouseEvent({ type: "click" },
+  EventUtils.sendMouseEvent({ type: "mousedown" },
     document.querySelector(".network-details-panel-toggle"));
-  EventUtils.sendMouseEvent({ type: "click" },
-    document.querySelector("#params-tab"));
+  document.querySelector("#params-tab").click();
   yield wait;
 
   let tabpanel = document.querySelector("#params-panel");
 
   ok(tabpanel.querySelector(".treeTable"),
     "The request params doesn't have the indended visibility.");
   ok(tabpanel.querySelector(".editor-mount") === null,
     "The request post data doesn't have the indended visibility.");
--- a/devtools/client/netmonitor/test/browser_net_prefs-and-l10n.js
+++ b/devtools/client/netmonitor/test/browser_net_prefs-and-l10n.js
@@ -8,32 +8,34 @@
  */
 
 add_task(function* () {
   let { L10N } = require("devtools/client/netmonitor/l10n");
 
   let { monitor } = yield initNetMonitor(SIMPLE_URL);
   info("Starting test... ");
 
-  let { windowRequire } = monitor.panelWin;
-  let { Prefs } = windowRequire("devtools/client/netmonitor/prefs");
+  ok(monitor.panelWin.Prefs,
+    "Should have a preferences object available on the panel window.");
 
   testL10N();
   testPrefs();
 
   return teardown(monitor);
 
   function testL10N() {
     is(typeof L10N.getStr("netmonitor.security.enabled"), "string",
       "The getStr() method didn't return a valid string.");
     is(typeof L10N.getFormatStr("networkMenu.totalMS", "foo"), "string",
       "The getFormatStr() method didn't return a valid string.");
   }
 
   function testPrefs() {
+    let { Prefs } = monitor.panelWin;
+
     is(Prefs.networkDetailsWidth,
       Services.prefs.getIntPref("devtools.netmonitor.panes-network-details-width"),
       "Getting a pref should work correctly.");
 
     let previousValue = Prefs.networkDetailsWidth;
     let bogusValue = ~~(Math.random() * 100);
     Prefs.networkDetailsWidth = bogusValue;
     is(Prefs.networkDetailsWidth,
--- a/devtools/client/netmonitor/test/browser_net_raw_headers.js
+++ b/devtools/client/netmonitor/test/browser_net_raw_headers.js
@@ -6,39 +6,39 @@
 /**
  * Tests if showing raw headers works.
  */
 
 add_task(function* () {
   let { tab, monitor } = yield initNetMonitor(POST_DATA_URL);
   info("Starting test... ");
 
-  let { document, gStore, windowRequire } = monitor.panelWin;
-  let Actions = windowRequire("devtools/client/netmonitor/actions/index");
-  let { getSortedRequests } = windowRequire("devtools/client/netmonitor/selectors/index");
+  let { document, NetMonitorView } = monitor.panelWin;
+  let { RequestsMenu } = NetMonitorView;
 
-  gStore.dispatch(Actions.batchEnable(false));
+  RequestsMenu.lazyUpdate = false;
 
   let wait = waitForNetworkEvents(monitor, 0, 2);
   yield ContentTask.spawn(tab.linkedBrowser, {}, function* () {
     content.wrappedJSObject.performRequests();
   });
   yield wait;
 
+  let origItem = RequestsMenu.getItemAtIndex(0);
+
   wait = waitForDOM(document, ".headers-overview");
-  EventUtils.sendMouseEvent({ type: "mousedown" },
-    document.querySelectorAll(".request-list-item")[0]);
+  RequestsMenu.selectedItem = origItem;
   yield wait;
 
   wait = waitForDOM(document, ".raw-headers-container textarea", 2);
   EventUtils.sendMouseEvent({ type: "click" },
     document.querySelectorAll(".headers-summary .tool-button")[1]);
   yield wait;
 
-  testShowRawHeaders(getSortedRequests(gStore.getState()).get(0));
+  testShowRawHeaders(origItem);
 
   EventUtils.sendMouseEvent({ type: "click" },
     document.querySelectorAll(".headers-summary .tool-button")[1]);
 
   testHideRawHeaders(document);
 
   return teardown(monitor);
 
--- a/devtools/client/netmonitor/test/browser_net_reload-button.js
+++ b/devtools/client/netmonitor/test/browser_net_reload-button.js
@@ -6,20 +6,20 @@
 /**
  * Tests if the empty-requests reload button works.
  */
 
 add_task(function* () {
   let { monitor } = yield initNetMonitor(SIMPLE_URL);
   info("Starting test... ");
 
-  let { document } = monitor.panelWin;
+  let { document, NetMonitorView } = monitor.panelWin;
+  let { RequestsMenu } = NetMonitorView;
 
   let wait = waitForNetworkEvents(monitor, 1);
-  EventUtils.sendMouseEvent({ type: "click" },
-    document.querySelector("#requests-menu-reload-notice-button"));
+  let button = document.querySelector("#requests-menu-reload-notice-button");
+  button.click();
   yield wait;
 
-  is(document.querySelectorAll(".request-list-item").length, 1,
-    "The request list should have one item after reloading");
+  is(RequestsMenu.itemCount, 1, "The request menu should have one item after reloading");
 
   return teardown(monitor);
 });
--- a/devtools/client/netmonitor/test/browser_net_reload-markers.js
+++ b/devtools/client/netmonitor/test/browser_net_reload-markers.js
@@ -6,18 +6,17 @@
 /**
  * Tests if the empty-requests reload button works.
  */
 
 add_task(function* () {
   let { monitor } = yield initNetMonitor(SIMPLE_URL);
   info("Starting test... ");
 
-  let { document, windowRequire } = monitor.panelWin;
-  let { EVENTS } = windowRequire("devtools/client/netmonitor/events");
+  let { document, EVENTS } = monitor.panelWin;
   let button = document.querySelector("#requests-menu-reload-notice-button");
   button.click();
 
   let markers = [];
 
   monitor.panelWin.on(EVENTS.TIMELINE_EVENT, (_, marker) => {
     markers.push(marker);
   });
--- a/devtools/client/netmonitor/test/browser_net_req-resp-bodies.js
+++ b/devtools/client/netmonitor/test/browser_net_req-resp-bodies.js
@@ -8,24 +8,20 @@
  */
 
 add_task(function* () {
   let { L10N } = require("devtools/client/netmonitor/l10n");
 
   let { tab, monitor } = yield initNetMonitor(JSON_LONG_URL);
   info("Starting test... ");
 
-  let { document, gStore, windowRequire } = monitor.panelWin;
-  let Actions = windowRequire("devtools/client/netmonitor/actions/index");
-  let {
-    getDisplayedRequests,
-    getSortedRequests,
-  } = windowRequire("devtools/client/netmonitor/selectors/index");
+  let { NetMonitorView } = monitor.panelWin;
+  let { RequestsMenu } = NetMonitorView;
 
-  gStore.dispatch(Actions.batchEnable(false));
+  RequestsMenu.lazyUpdate = false;
 
   // Perform first batch of requests.
   let wait = waitForNetworkEvents(monitor, 1);
   yield ContentTask.spawn(tab.linkedBrowser, {}, function* () {
     content.wrappedJSObject.performRequests();
   });
   yield wait;
 
@@ -52,24 +48,19 @@ add_task(function* () {
     content.wrappedJSObject.performRequests();
   });
   yield wait;
 
   verifyRequest(1);
 
   return teardown(monitor);
 
-  function verifyRequest(index) {
-    verifyRequestItemTarget(
-      document,
-      getDisplayedRequests(gStore.getState()),
-      getSortedRequests(gStore.getState()).get(index),
-      "GET",
-      CONTENT_TYPE_SJS + "?fmt=json-long",
-      {
+  function verifyRequest(offset) {
+    verifyRequestItemTarget(RequestsMenu, RequestsMenu.getItemAtIndex(offset),
+      "GET", CONTENT_TYPE_SJS + "?fmt=json-long", {
         status: 200,
         statusText: "OK",
         type: "json",
         fullMimeType: "text/json; charset=utf-8",
         size: L10N.getFormatStr("networkMenu.sizeKB",
           L10N.numberWithDecimals(85975 / 1024, 2)),
         time: true
       });
--- a/devtools/client/netmonitor/test/browser_net_resend.js
+++ b/devtools/client/netmonitor/test/browser_net_resend.js
@@ -11,55 +11,53 @@ const ADD_QUERY = "t1=t2";
 const ADD_HEADER = "Test-header: true";
 const ADD_UA_HEADER = "User-Agent: Custom-Agent";
 const ADD_POSTDATA = "&t3=t4";
 
 add_task(function* () {
   let { tab, monitor } = yield initNetMonitor(POST_DATA_URL);
   info("Starting test... ");
 
-  let { document, gStore, windowRequire } = monitor.panelWin;
+  let { panelWin } = monitor;
+  let { document, gStore, NetMonitorView, windowRequire } = panelWin;
   let Actions = windowRequire("devtools/client/netmonitor/actions/index");
-  let {
-    getSelectedRequest,
-    getSortedRequests,
-  } = windowRequire("devtools/client/netmonitor/selectors/index");
+  let { RequestsMenu } = NetMonitorView;
 
-  gStore.dispatch(Actions.batchEnable(false));
+  RequestsMenu.lazyUpdate = false;
 
   let wait = waitForNetworkEvents(monitor, 0, 2);
   yield ContentTask.spawn(tab.linkedBrowser, {}, function* () {
     content.wrappedJSObject.performRequests();
   });
   yield wait;
 
-  let origItem = getSortedRequests(gStore.getState()).get(0);
+  let origItem = RequestsMenu.getItemAtIndex(0);
 
-  gStore.dispatch(Actions.selectRequest(origItem.id));
+  RequestsMenu.selectedItem = origItem;
 
   // add a new custom request cloned from selected request
   gStore.dispatch(Actions.cloneSelectedRequest());
 
   testCustomForm(origItem);
 
-  let customItem = getSelectedRequest(gStore.getState());
+  let customItem = RequestsMenu.selectedItem;
   testCustomItem(customItem, origItem);
 
   // edit the custom request
   yield editCustomForm();
   // FIXME: reread the customItem, it's been replaced by a new object (immutable!)
-  customItem = getSelectedRequest(gStore.getState());
+  customItem = RequestsMenu.selectedItem;
   testCustomItemChanged(customItem, origItem);
 
   // send the new request
   wait = waitForNetworkEvents(monitor, 0, 1);
   gStore.dispatch(Actions.sendCustomRequest());
   yield wait;
 
-  let sentItem = getSelectedRequest(gStore.getState());
+  let sentItem = RequestsMenu.selectedItem;
   testSentRequest(sentItem, origItem);
 
   return teardown(monitor);
 
   function testCustomItem(item, orig) {
     is(item.method, orig.method, "item is showing the same method as original request");
     is(item.url, orig.url, "item is showing the same URL as original request");
   }
@@ -70,17 +68,16 @@ add_task(function* () {
 
     is(url, expectedUrl, "menu item is updated to reflect url entered in form");
   }
 
   /*
    * Test that the New Request form was populated correctly
    */
   function testCustomForm(data) {
-    yield waitUntil(() => document.querySelector(".custom-request-panel"));
     is(document.getElementById("custom-method-value").value, data.method,
        "new request form showing correct method");
 
     is(document.getElementById("custom-url-value").value, data.url,
        "new request form showing correct url");
 
     let query = document.getElementById("custom-query-value");
     is(query.value, "foo=bar\nbaz=42\ntype=urlencoded",
@@ -95,17 +92,17 @@ add_task(function* () {
     is(postData.value, data.requestPostData.postData.text,
        "new request form showing correct post data");
   }
 
   /*
    * Add some params and headers to the request form
    */
   function* editCustomForm() {
-    monitor.panelWin.focus();
+    panelWin.focus();
 
     let query = document.getElementById("custom-query-value");
     let queryFocus = once(query, "focus", false);
     // Bug 1195825: Due to some unexplained dark-matter with promise,
     // focus only works if delayed by one tick.
     executeSoon(() => query.focus());
     yield queryFocus;
 
@@ -152,12 +149,12 @@ add_task(function* () {
 
     is(data.requestPostData.postData.text,
        origData.requestPostData.postData.text + ADD_POSTDATA,
        "post data added to sent request");
   }
 
   function type(string) {
     for (let ch of string) {
-      EventUtils.synthesizeKey(ch, {}, monitor.panelWin);
+      EventUtils.synthesizeKey(ch, {}, panelWin);
     }
   }
 });
--- a/devtools/client/netmonitor/test/browser_net_resend_cors.js
+++ b/devtools/client/netmonitor/test/browser_net_resend_cors.js
@@ -7,49 +7,46 @@
  * Tests if resending a CORS request avoids the security checks and doesn't send
  * a preflight OPTIONS request (bug 1270096 and friends)
  */
 
 add_task(function* () {
   let { tab, monitor } = yield initNetMonitor(CORS_URL);
   info("Starting test... ");
 
-  let { document, gStore, windowRequire } = monitor.panelWin;
+  let { gStore, NetMonitorView, windowRequire } = monitor.panelWin;
   let Actions = windowRequire("devtools/client/netmonitor/actions/index");
-  let {
-    getDisplayedRequests,
-    getSortedRequests,
-  } = windowRequire("devtools/client/netmonitor/selectors/index");
+  let { RequestsMenu } = NetMonitorView;
 
-  gStore.dispatch(Actions.batchEnable(false));
+  RequestsMenu.lazyUpdate = false;
 
   let requestUrl = "http://test1.example.com" + CORS_SJS_PATH;
 
   info("Waiting for OPTIONS, then POST");
   let wait = waitForNetworkEvents(monitor, 1, 1);
   yield ContentTask.spawn(tab.linkedBrowser, requestUrl, function* (url) {
     content.wrappedJSObject.performRequests(url, "triggering/preflight", "post-data");
   });
   yield wait;
 
   const METHODS = ["OPTIONS", "POST"];
-  const ITEMS = METHODS.map((val, i) => getSortedRequests(gStore.getState()).get(i));
+  const ITEMS = METHODS.map((val, i) => RequestsMenu.getItemAtIndex(i));
 
   // Check the requests that were sent
   ITEMS.forEach((item, i) => {
     is(item.method, METHODS[i], `The ${item.method} request has the right method`);
     is(item.url, requestUrl, `The ${item.method} request has the right URL`);
   });
 
   // Resend both requests without modification. Wait for resent OPTIONS, then POST.
   // POST is supposed to have no preflight OPTIONS request this time (CORS is disabled)
   let onRequests = waitForNetworkEvents(monitor, 1, 0);
   ITEMS.forEach((item) => {
     info(`Selecting the ${item.method} request`);
-    gStore.dispatch(Actions.selectRequest(item.id))
+    RequestsMenu.selectedItem = item;
 
     info("Cloning the selected request into a custom clone");
     gStore.dispatch(Actions.cloneSelectedRequest());
 
     info("Sending the cloned request (without change)");
     gStore.dispatch(Actions.sendCustomRequest());
   });
 
--- a/devtools/client/netmonitor/test/browser_net_resend_headers.js
+++ b/devtools/client/netmonitor/test/browser_net_resend_headers.js
@@ -6,24 +6,20 @@
 /**
  * Test if custom request headers are not ignored (bug 1270096 and friends)
  */
 
 add_task(function* () {
   let { monitor } = yield initNetMonitor(SIMPLE_SJS);
   info("Starting test... ");
 
-  let { document, gStore, windowRequire, NetMonitorController } = monitor.panelWin;
-  let Actions = windowRequire("devtools/client/netmonitor/actions/index");
-  let {
-    getDisplayedRequests,
-    getSortedRequests,
-  } = windowRequire("devtools/client/netmonitor/selectors/index");
+  let { NetMonitorView, NetMonitorController } = monitor.panelWin;
+  let { RequestsMenu } = NetMonitorView;
 
-  gStore.dispatch(Actions.batchEnable(false));
+  RequestsMenu.lazyUpdate = false;
 
   let requestUrl = SIMPLE_SJS;
   let requestHeaders = [
     { name: "Host", value: "fakehost.example.com" },
     { name: "User-Agent", value: "Testzilla" },
     { name: "Referer", value: "http://example.com/referrer" },
     { name: "Accept", value: "application/jarda"},
     { name: "Accept-Encoding", value: "compress, identity, funcoding" },
@@ -34,17 +30,17 @@ add_task(function* () {
   NetMonitorController.webConsoleClient.sendHTTPRequest({
     url: requestUrl,
     method: "POST",
     headers: requestHeaders,
     body: "Hello"
   });
   yield wait;
 
-  let item = getSortedRequests(gStore.getState()).get(0);
+  let item = RequestsMenu.getItemAtIndex(0);
   is(item.method, "POST", "The request has the right method");
   is(item.url, requestUrl, "The request has the right URL");
 
   for (let { name, value } of item.requestHeaders.headers) {
     info(`Request header: ${name}: ${value}`);
   }
 
   function hasRequestHeader(name, value) {
--- a/devtools/client/netmonitor/test/browser_net_security-details.js
+++ b/devtools/client/netmonitor/test/browser_net_security-details.js
@@ -4,34 +4,33 @@
 "use strict";
 
 /**
  * Test that Security details tab contains the expected data.
  */
 
 add_task(function* () {
   let { tab, monitor } = yield initNetMonitor(CUSTOM_GET_URL);
-  let { document, gStore, windowRequire } = monitor.panelWin;
-  let Actions = windowRequire("devtools/client/netmonitor/actions/index");
+  let { document, NetMonitorView } = monitor.panelWin;
+  let { RequestsMenu } = NetMonitorView;
 
-  gStore.dispatch(Actions.batchEnable(false));
+  RequestsMenu.lazyUpdate = false;
 
   info("Performing a secure request.");
   const REQUESTS_URL = "https://example.com" + CORS_SJS_PATH;
   let wait = waitForNetworkEvents(monitor, 1);
   yield ContentTask.spawn(tab.linkedBrowser, REQUESTS_URL, function* (url) {
     content.wrappedJSObject.performRequests(1, url);
   });
   yield wait;
 
   wait = waitForDOM(document, "#security-panel");
-  EventUtils.sendMouseEvent({ type: "click" },
+  EventUtils.sendMouseEvent({ type: "mousedown" },
     document.querySelector(".network-details-panel-toggle"));
-  EventUtils.sendMouseEvent({ type: "click" },
-    document.querySelector("#security-tab"));
+  document.querySelector("#security-tab").click();
   yield wait;
 
   let tabpanel = document.querySelector("#security-panel");
   let textboxes = tabpanel.querySelectorAll(".textbox-input");
 
   // Connection
   // The protocol will be TLS but the exact version depends on which protocol
   // the test server example.com supports.
--- a/devtools/client/netmonitor/test/browser_net_security-error.js
+++ b/devtools/client/netmonitor/test/browser_net_security-error.js
@@ -4,35 +4,32 @@
 "use strict";
 
 /**
  * Test that Security details tab shows an error message with broken connections.
  */
 
 add_task(function* () {
   let { tab, monitor } = yield initNetMonitor(CUSTOM_GET_URL);
-  let { document, gStore, windowRequire } = monitor.panelWin;
-  let Actions = windowRequire("devtools/client/netmonitor/actions/index");
-  let { EVENTS } = windowRequire("devtools/client/netmonitor/events");
+  let { document, EVENTS, NetMonitorView } = monitor.panelWin;
 
-  gStore.dispatch(Actions.batchEnable(false));
+  NetMonitorView.RequestsMenu.lazyUpdate = false;
 
   info("Requesting a resource that has a certificate problem.");
 
   let wait = waitForSecurityBrokenNetworkEvent();
   yield ContentTask.spawn(tab.linkedBrowser, {}, function* () {
     content.wrappedJSObject.performRequests(1, "https://nocert.example.com");
   });
   yield wait;
 
   wait = waitForDOM(document, "#security-panel");
-  EventUtils.sendMouseEvent({ type: "click" },
+  EventUtils.sendMouseEvent({ type: "mousedown" },
     document.querySelector(".network-details-panel-toggle"));
-  EventUtils.sendMouseEvent({ type: "click" },
-    document.querySelector("#security-tab"));
+  document.querySelector("#security-tab").click();
   yield wait;
 
   let errormsg = document.querySelector(".security-info-value");
   isnot(errormsg.textContent, "", "Error message is not empty.");
 
   return teardown(monitor);
 
   /**
--- a/devtools/client/netmonitor/test/browser_net_security-icon-click.js
+++ b/devtools/client/netmonitor/test/browser_net_security-icon-click.js
@@ -4,56 +4,55 @@
 "use strict";
 
 /**
  * Test that clicking on the security indicator opens the security details tab.
  */
 
 add_task(function* () {
   let { tab, monitor } = yield initNetMonitor(CUSTOM_GET_URL);
-  let { document, gStore, windowRequire } = monitor.panelWin;
-  let Actions = windowRequire("devtools/client/netmonitor/actions/index");
-  let {
-    getDisplayedRequests,
-    getSortedRequests,
-  } = windowRequire("devtools/client/netmonitor/selectors/index");
+  let { document, NetMonitorView } = monitor.panelWin;
+  let { RequestsMenu } = NetMonitorView;
 
-  gStore.dispatch(Actions.batchEnable(false));
+  RequestsMenu.lazyUpdate = false;
 
   info("Requesting a resource over HTTPS.");
   yield performRequestAndWait("https://example.com" + CORS_SJS_PATH + "?request_2");
   yield performRequestAndWait("https://example.com" + CORS_SJS_PATH + "?request_1");
 
-  is(gStore.getState().requests.requests.size, 2, "Two events event logged.");
-
+  is(RequestsMenu.itemCount, 2, "Two events event logged.");
+console.log(123)
   yield clickAndTestSecurityIcon();
+console.log(123)
 
   info("Selecting headers panel again.");
-  EventUtils.sendMouseEvent({ type: "click" },
+  EventUtils.sendMouseEvent({ type: "mousedown" },
     document.querySelector("#headers-tab"));
 
   info("Sorting the items by filename.");
   EventUtils.sendMouseEvent({ type: "click" },
     document.querySelector("#requests-menu-file-button"));
 
   info("Testing that security icon can be clicked after the items were sorted.");
-
+console.log(123)
   yield clickAndTestSecurityIcon();
+console.log(123)
 
   return teardown(monitor);
 
   function* performRequestAndWait(url) {
     let wait = waitForNetworkEvents(monitor, 1);
     yield ContentTask.spawn(tab.linkedBrowser, { url }, function* (args) {
       content.wrappedJSObject.performRequests(1, args.url);
     });
     return wait;
   }
 
   function* clickAndTestSecurityIcon() {
+    let item = RequestsMenu.getItemAtIndex(0);
     let icon = document.querySelector(".requests-security-state-icon");
 
     info("Clicking security icon of the first request and waiting for panel update.");
     EventUtils.synthesizeMouseAtCenter(icon, {}, monitor.panelWin);
 
     ok(document.querySelector("#security-tab[aria-selected=true]"), "Security tab is selected.");
   }
 });
--- a/devtools/client/netmonitor/test/browser_net_security-redirect.js
+++ b/devtools/client/netmonitor/test/browser_net_security-redirect.js
@@ -5,38 +5,35 @@
 
 /**
  * Test a http -> https redirect shows secure icon only for redirected https
  * request.
  */
 
 add_task(function* () {
   let { tab, monitor } = yield initNetMonitor(CUSTOM_GET_URL);
-  let { document, gStore, windowRequire } = monitor.panelWin;
-  let Actions = windowRequire("devtools/client/netmonitor/actions/index");
-  let {
-    getDisplayedRequests,
-    getSortedRequests,
-  } = windowRequire("devtools/client/netmonitor/selectors/index");
-
-  gStore.dispatch(Actions.batchEnable(false));
+  let { $, NetMonitorView } = monitor.panelWin;
+  let { RequestsMenu } = NetMonitorView;
+  RequestsMenu.lazyUpdate = false;
 
   let wait = waitForNetworkEvents(monitor, 2);
   yield ContentTask.spawn(tab.linkedBrowser, HTTPS_REDIRECT_SJS, function* (url) {
     content.wrappedJSObject.performRequests(1, url);
   });
   yield wait;
 
-  is(gStore.getState().requests.requests.size, 2, "There were two requests due to redirect.");
+  is(RequestsMenu.itemCount, 2, "There were two requests due to redirect.");
+
+  let initial = RequestsMenu.getItemAtIndex(0);
+  let redirect = RequestsMenu.getItemAtIndex(1);
 
-  let initial = getSortedRequests(gStore.getState()).get(0);
-  let redirect = getSortedRequests(gStore.getState()).get(1);
-
-  let initialSecurityIcon = document.querySelectorAll(".requests-security-state-icon")[0];
-  let redirectSecurityIcon = document.querySelectorAll(".requests-security-state-icon")[1];
+  let initialSecurityIcon =
+    $(".requests-security-state-icon", getItemTarget(RequestsMenu, initial));
+  let redirectSecurityIcon =
+    $(".requests-security-state-icon", getItemTarget(RequestsMenu, redirect));
 
   ok(initialSecurityIcon.classList.contains("security-state-insecure"),
      "Initial request was marked insecure.");
 
   ok(redirectSecurityIcon.classList.contains("security-state-secure"),
      "Redirected request was marked secure.");
 
   yield teardown(monitor);
--- a/devtools/client/netmonitor/test/browser_net_security-state.js
+++ b/devtools/client/netmonitor/test/browser_net_security-state.js
@@ -12,35 +12,30 @@ add_task(function* () {
   const EXPECTED_SECURITY_STATES = {
     "test1.example.com": "security-state-insecure",
     "example.com": "security-state-secure",
     "nocert.example.com": "security-state-broken",
     "localhost": "security-state-local",
   };
 
   let { tab, monitor } = yield initNetMonitor(CUSTOM_GET_URL);
-  let { document, gStore, windowRequire } = monitor.panelWin;
-  let Actions = windowRequire("devtools/client/netmonitor/actions/index");
-  let {
-    getDisplayedRequests,
-    getSortedRequests,
-  } = windowRequire("devtools/client/netmonitor/selectors/index");
-
-  gStore.dispatch(Actions.batchEnable(false));
+  let { $, EVENTS, NetMonitorView } = monitor.panelWin;
+  let { RequestsMenu } = NetMonitorView;
+  RequestsMenu.lazyUpdate = false;
 
   yield performRequests();
 
-  for (let subitemNode of Array.from(document.querySelectorAll(
-    "requests-menu-subitem.requests-menu-security-and-domain"))) {
-    let domain = subitemNode.querySelector(".requests-menu-domain").textContent;
+  for (let item of RequestsMenu.items) {
+    let target = getItemTarget(RequestsMenu, item);
+    let domain = $(".requests-menu-domain", target).textContent;
 
     info("Found a request to " + domain);
     ok(domain in EXPECTED_SECURITY_STATES, "Domain " + domain + " was expected.");
 
-    let classes = subitemNode.querySelector(".requests-security-state-icon").classList;
+    let classes = $(".requests-security-state-icon", target).classList;
     let expectedClass = EXPECTED_SECURITY_STATES[domain];
 
     info("Classes of security state icon are: " + classes);
     info("Security state icon is expected to contain class: " + expectedClass);
     ok(classes.contains(expectedClass), "Icon contained the correct class name.");
   }
 
   return teardown(monitor);
@@ -85,19 +80,17 @@ add_task(function* () {
     yield done;
 
     done = waitForSecurityBrokenNetworkEvent(true);
     info("Requesting a resource over HTTP to localhost.");
     yield executeRequests(1, "http://localhost" + CORS_SJS_PATH);
     yield done;
 
     const expectedCount = Object.keys(EXPECTED_SECURITY_STATES).length;
-    is(gStore.getState().requests.requests.size,
-      expectedCount,
-      expectedCount + " events logged.");
+    is(RequestsMenu.itemCount, expectedCount, expectedCount + " events logged.");
   }
 
   /**
    * Returns a promise that's resolved once a request with security issues is
    * completed.
    */
   function waitForSecurityBrokenNetworkEvent(networkError) {
     let awaitedEvents = [
--- a/devtools/client/netmonitor/test/browser_net_security-tab-deselect.js
+++ b/devtools/client/netmonitor/test/browser_net_security-tab-deselect.js
@@ -5,20 +5,20 @@
 
 /**
  * Test that security details tab is no longer selected if an insecure request
  * is selected.
  */
 
 add_task(function* () {
   let { tab, monitor } = yield initNetMonitor(CUSTOM_GET_URL);
-  let { document, gStore, windowRequire } = monitor.panelWin;
-  let Actions = windowRequire("devtools/client/netmonitor/actions/index");
+  let { document, NetMonitorView } = monitor.panelWin;
+  let { RequestsMenu } = NetMonitorView;
 
-  gStore.dispatch(Actions.batchEnable(false));
+  RequestsMenu.lazyUpdate = false;
 
   info("Performing requests.");
   let wait = waitForNetworkEvents(monitor, 2);
   const REQUEST_URLS = [
     "https://example.com" + CORS_SJS_PATH,
     "http://example.com" + CORS_SJS_PATH,
   ];
   yield ContentTask.spawn(tab.linkedBrowser, REQUEST_URLS, function* (urls) {
--- a/devtools/client/netmonitor/test/browser_net_security-tab-visibility.js
+++ b/devtools/client/netmonitor/test/browser_net_security-tab-visibility.js
@@ -27,21 +27,20 @@ add_task(function* () {
       isBroken: true,
       visibleOnNewEvent: false,
       visibleOnSecurityInfo: true,
       visibleOnceComplete: true,
     }
   ];
 
   let { tab, monitor } = yield initNetMonitor(CUSTOM_GET_URL);
-  let { document, gStore, windowRequire } = monitor.panelWin;
-  let Actions = windowRequire("devtools/client/netmonitor/actions/index");
-  let { getSelectedRequest } = windowRequire("devtools/client/netmonitor/selectors/index");
+  let { document, EVENTS, NetMonitorView } = monitor.panelWin;
+  let { RequestsMenu } = NetMonitorView;
 
-  gStore.dispatch(Actions.batchEnable(false));
+  RequestsMenu.lazyUpdate = false;
 
   for (let testcase of TEST_DATA) {
     info("Testing Security tab visibility for " + testcase.desc);
     let onNewItem = monitor.panelWin.once(EVENTS.NETWORK_EVENT);
     let onSecurityInfo = monitor.panelWin.once(EVENTS.RECEIVED_SECURITY_INFO);
     let onComplete = testcase.isBroken ?
                        waitForSecurityBrokenNetworkEvent() :
                        waitForNetworkEvents(monitor, 1);
@@ -50,43 +49,42 @@ add_task(function* () {
     yield ContentTask.spawn(tab.linkedBrowser, testcase.uri, function* (url) {
       content.wrappedJSObject.performRequests(1, url);
     });
 
     info("Waiting for new network event.");
     yield onNewItem;
 
     info("Selecting the request.");
-    EventUtils.sendMouseEvent({ type: "mousedown" },
-      document.querySelectorAll(".request-list-item")[0]);
+    RequestsMenu.selectedIndex = 0;
 
-    is(getSelectedRequest(gStore.getState()).securityState, undefined,
+    is(RequestsMenu.selectedItem.securityState, undefined,
        "Security state has not yet arrived.");
     is(!!document.querySelector("#security-tab"), testcase.visibleOnNewEvent,
       "Security tab is " + (testcase.visibleOnNewEvent ? "visible" : "hidden") +
       " after new request was added to the menu.");
 
     info("Waiting for security information to arrive.");
     yield onSecurityInfo;
 
-    ok(getSelectedRequest(gStore.getState()).securityState,
+    ok(RequestsMenu.selectedItem.securityState,
        "Security state arrived.");
     is(!!document.querySelector("#security-tab"), testcase.visibleOnSecurityInfo,
        "Security tab is " + (testcase.visibleOnSecurityInfo ? "visible" : "hidden") +
        " after security information arrived.");
 
     info("Waiting for request to complete.");
     yield onComplete;
 
     is(!!document.querySelector("#security-tab"), testcase.visibleOnceComplete,
        "Security tab is " + (testcase.visibleOnceComplete ? "visible" : "hidden") +
        " after request has been completed.");
 
     info("Clearing requests.");
-    gStore.dispatch(Actions.clearRequests());
+    RequestsMenu.clear();
   }
 
   return teardown(monitor);
 
   /**
    * Returns a promise that's resolved once a request with security issues is
    * completed.
    */
--- a/devtools/client/netmonitor/test/browser_net_security-warnings.js
+++ b/devtools/client/netmonitor/test/browser_net_security-warnings.js
@@ -12,20 +12,20 @@ const TEST_CASES = [
     desc: "no warnings",
     uri: "https://example.com" + CORS_SJS_PATH,
     warnCipher: null,
   },
 ];
 
 add_task(function* () {
   let { tab, monitor } = yield initNetMonitor(CUSTOM_GET_URL);
-  let { document, gStore, windowRequire } = monitor.panelWin;
-  let Actions = windowRequire("devtools/client/netmonitor/actions/index");
+  let { document, NetMonitorView } = monitor.panelWin;
+  let { RequestsMenu } = NetMonitorView;
 
-  gStore.dispatch(Actions.batchEnable(false));
+  RequestsMenu.lazyUpdate = false;
 
   for (let test of TEST_CASES) {
     info("Testing site with " + test.desc);
 
     info("Performing request to " + test.uri);
     let wait = waitForNetworkEvents(monitor, 1);
     yield ContentTask.spawn(tab.linkedBrowser, test.uri, function* (url) {
       content.wrappedJSObject.performRequests(1, url);
@@ -36,22 +36,21 @@ add_task(function* () {
     wait = waitForDOM(document, ".tabs");
     EventUtils.sendMouseEvent({ type: "mousedown" },
       document.querySelectorAll(".request-list-item")[0]);
     yield wait;
 
     if (!document.querySelector("#security-tab[aria-selected=true]")) {
       info("Selecting security tab.");
       wait = waitForDOM(document, "#security-panel .properties-view");
-      EventUtils.sendMouseEvent({ type: "click" },
-        document.querySelector("#security-tab"));
+      document.querySelector("#security-tab").click();
       yield wait;
     }
 
     is(document.querySelector("#security-warning-cipher"),
       test.warnCipher,
       "Cipher suite warning is hidden.");
 
-    gStore.dispatch(Actions.clearRequests());
+    RequestsMenu.clear();
   }
 
   return teardown(monitor);
 });
--- a/devtools/client/netmonitor/test/browser_net_send-beacon-other-tab.js
+++ b/devtools/client/netmonitor/test/browser_net_send-beacon-other-tab.js
@@ -4,34 +4,31 @@
 "use strict";
 
 /**
  * Tests if beacons from other tabs are properly ignored.
  */
 
 add_task(function* () {
   let { tab, monitor } = yield initNetMonitor(SIMPLE_URL);
-  let { gStore, windowRequire } = monitor.panelWin;
-  let Actions = windowRequire("devtools/client/netmonitor/actions/index");
-  let { getSortedRequests } = windowRequire("devtools/client/netmonitor/selectors/index");
-
-  gStore.dispatch(Actions.batchEnable(false));
+  let { RequestsMenu } = monitor.panelWin.NetMonitorView;
+  RequestsMenu.lazyUpdate = false;
 
   let beaconTab = yield addTab(SEND_BEACON_URL);
   info("Beacon tab added successfully.");
 
-  is(gStore.getState().requests.requests.size, 0, "The requests menu should be empty.");
+  is(RequestsMenu.itemCount, 0, "The requests menu should be empty.");
 
   let wait = waitForNetworkEvents(monitor, 1);
   yield ContentTask.spawn(beaconTab.linkedBrowser, {}, function* () {
     content.wrappedJSObject.performRequest();
   });
   tab.linkedBrowser.reload();
   yield wait;
 
-  is(gStore.getState().requests.requests.size, 1, "Only the reload should be recorded.");
-  let request = getSortedRequests(gStore.getState()).get(0);
+  is(RequestsMenu.itemCount, 1, "Only the reload should be recorded.");
+  let request = RequestsMenu.getItemAtIndex(0);
   is(request.method, "GET", "The method is correct.");
   is(request.status, "200", "The status is correct.");
 
   yield removeTab(beaconTab);
   return teardown(monitor);
 });
--- a/devtools/client/netmonitor/test/browser_net_send-beacon.js
+++ b/devtools/client/netmonitor/test/browser_net_send-beacon.js
@@ -4,30 +4,28 @@
 "use strict";
 
 /**
  * Tests if beacons are handled correctly.
  */
 
 add_task(function* () {
   let { tab, monitor } = yield initNetMonitor(SEND_BEACON_URL);
- let { gStore, windowRequire } = monitor.panelWin;
-  let Actions = windowRequire("devtools/client/netmonitor/actions/index");
-  let { getSortedRequests } = windowRequire("devtools/client/netmonitor/selectors/index");
+  let { RequestsMenu } = monitor.panelWin.NetMonitorView;
 
-  gStore.dispatch(Actions.batchEnable(false));
+  RequestsMenu.lazyUpdate = false;
 
-  is(gStore.getState().requests.requests.size, 0, "The requests menu should be empty.");
+  is(RequestsMenu.itemCount, 0, "The requests menu should be empty.");
 
   let wait = waitForNetworkEvents(monitor, 1);
   yield ContentTask.spawn(tab.linkedBrowser, {}, function* () {
     content.wrappedJSObject.performRequest();
   });
   yield wait;
 
-  is(gStore.getState().requests.requests.size, 1, "The beacon should be recorded.");
-  let request = getSortedRequests(gStore.getState()).get(0);
+  is(RequestsMenu.itemCount, 1, "The beacon should be recorded.");
+  let request = RequestsMenu.getItemAtIndex(0);
   is(request.method, "POST", "The method is correct.");
   ok(request.url.endsWith("beacon_request"), "The URL is correct.");
   is(request.status, "404", "The status is correct.");
 
   return teardown(monitor);
 });
--- a/devtools/client/netmonitor/test/browser_net_service-worker-status.js
+++ b/devtools/client/netmonitor/test/browser_net_service-worker-status.js
@@ -11,24 +11,18 @@
 const URL = EXAMPLE_URL.replace("http:", "https:");
 
 const TEST_URL = URL + "service-workers/status-codes.html";
 
 add_task(function* () {
   let { tab, monitor } = yield initNetMonitor(TEST_URL, null, true);
   info("Starting test... ");
 
-  let { document, gStore, windowRequire } = monitor.panelWin;
-  let Actions = windowRequire("devtools/client/netmonitor/actions/index");
-  let {
-    getDisplayedRequests,
-    getSortedRequests,
-  } = windowRequire("devtools/client/netmonitor/selectors/index");
-
-  gStore.dispatch(Actions.batchEnable(false));
+  let { NetMonitorView } = monitor.panelWin;
+  let { RequestsMenu } = NetMonitorView;
 
   const REQUEST_DATA = [
     {
       method: "GET",
       uri: URL + "service-workers/test/200",
       details: {
         status: 200,
         statusText: "OK (service worker)",
@@ -49,27 +43,21 @@ add_task(function* () {
   let wait = waitForNetworkEvents(monitor, REQUEST_DATA.length);
   yield ContentTask.spawn(tab.linkedBrowser, {}, function* () {
     content.wrappedJSObject.performRequests();
   });
   yield wait;
 
   let index = 0;
   for (let request of REQUEST_DATA) {
-    let item = getSortedRequests(gStore.getState()).get(index);
+    let item = RequestsMenu.getItemAtIndex(index);
 
     info(`Verifying request #${index}`);
-    yield verifyRequestItemTarget(
-      document,
-      getDisplayedRequests(gStore.getState()),
-      item,
-      request.method,
-      request.uri,
-      request.details
-    );
+    yield verifyRequestItemTarget(RequestsMenu, item,
+      request.method, request.uri, request.details);
 
     let { stacktrace } = item.cause;
     let stackLen = stacktrace ? stacktrace.length : 0;
 
     ok(stacktrace, `Request #${index} has a stacktrace`);
     ok(stackLen >= request.stackFunctions.length,
       `Request #${index} has a stacktrace with enough (${stackLen}) items`);
 
--- a/devtools/client/netmonitor/test/browser_net_simple-request-data.js
+++ b/devtools/client/netmonitor/test/browser_net_simple-request-data.js
@@ -8,40 +8,34 @@
  */
 
 function test() {
   let { L10N } = require("devtools/client/netmonitor/l10n");
 
   initNetMonitor(SIMPLE_SJS).then(({ tab, monitor }) => {
     info("Starting test... ");
 
-    let { document, gStore, windowRequire } = monitor.panelWin;
-    let Actions = windowRequire("devtools/client/netmonitor/actions/index");
-    let { EVENTS } = windowRequire("devtools/client/netmonitor/events");
-    let {
-      getDisplayedRequests,
-      getSelectedRequest,
-      getSortedRequests,
-    } = windowRequire("devtools/client/netmonitor/selectors/index");
+    let { NetMonitorView } = monitor.panelWin;
+    let { RequestsMenu } = NetMonitorView;
 
-    gStore.dispatch(Actions.batchEnable(false));
+    RequestsMenu.lazyUpdate = false;
 
     waitForNetworkEvents(monitor, 1)
       .then(() => teardown(monitor))
       .then(finish);
 
-    monitor.panelWin.once(EVENTS.NETWORK_EVENT, () => {
-      is(getSelectedRequest(gStore.getState()), null,
+    monitor.panelWin.once(monitor.panelWin.EVENTS.NETWORK_EVENT, () => {
+      is(RequestsMenu.selectedItem, null,
         "There shouldn't be any selected item in the requests menu.");
-      is(gStore.getState().requests.requests.size, 1,
+      is(RequestsMenu.itemCount, 1,
         "The requests menu should not be empty after the first request.");
       is(!!document.querySelector(".network-details-panel"), false,
         "The network details panel should still be hidden after first request.");
 
-      let requestItem = getSortedRequests(gStore.getState()).get(0);
+      let requestItem = RequestsMenu.getItemAtIndex(0);
 
       is(typeof requestItem.id, "string",
         "The attached request id is incorrect.");
       isnot(requestItem.id, "",
         "The attached request id should not be empty.");
 
       is(typeof requestItem.startedMillis, "number",
         "The attached startedMillis is incorrect.");
@@ -69,212 +63,156 @@ function test() {
 
       is(requestItem.headersSize, undefined,
         "The headersSize should not yet be set.");
       is(requestItem.transferredSize, undefined,
         "The transferredSize should not yet be set.");
       is(requestItem.contentSize, undefined,
         "The contentSize should not yet be set.");
 
+      is(requestItem.mimeType, undefined,
+        "The mimeType should not yet be set.");
       is(requestItem.responseContent, undefined,
         "The responseContent should not yet be set.");
 
       is(requestItem.totalTime, undefined,
         "The totalTime should not yet be set.");
       is(requestItem.eventTimings, undefined,
         "The eventTimings should not yet be set.");
 
-      verifyRequestItemTarget(
-        document,
-        getDisplayedRequests(gStore.getState()),
-        requestItem,
-        "GET",
-        SIMPLE_SJS
-      );
+      verifyRequestItemTarget(RequestsMenu, requestItem, "GET", SIMPLE_SJS);
     });
 
-    monitor.panelWin.once(EVENTS.RECEIVED_REQUEST_HEADERS, () => {
-      let requestItem = getSortedRequests(gStore.getState()).get(0);
+    monitor.panelWin.once(monitor.panelWin.EVENTS.RECEIVED_REQUEST_HEADERS, () => {
+      let requestItem = RequestsMenu.getItemAtIndex(0);
       ok(requestItem.requestHeaders,
         "There should be a requestHeaders data available.");
       is(requestItem.requestHeaders.headers.length, 10,
         "The requestHeaders data has an incorrect |headers| property.");
       isnot(requestItem.requestHeaders.headersSize, 0,
         "The requestHeaders data has an incorrect |headersSize| property.");
       // Can't test for the exact request headers size because the value may
       // vary across platforms ("User-Agent" header differs).
 
-      verifyRequestItemTarget(
-        document,
-        getDisplayedRequests(gStore.getState()),
-        requestItem,
-        "GET",
-        SIMPLE_SJS
-      );
+      verifyRequestItemTarget(requestItem, "GET", SIMPLE_SJS);
     });
 
-    monitor.panelWin.once(EVENTS.RECEIVED_REQUEST_COOKIES, () => {
-      let requestItem = getSortedRequests(gStore.getState()).get(0);
+    monitor.panelWin.once(monitor.panelWin.EVENTS.RECEIVED_REQUEST_COOKIES, () => {
+      let requestItem = RequestsMenu.getItemAtIndex(0);
 
       ok(requestItem.requestCookies,
         "There should be a requestCookies data available.");
       is(requestItem.requestCookies.cookies.length, 2,
         "The requestCookies data has an incorrect |cookies| property.");
 
-      verifyRequestItemTarget(
-        document,
-        getDisplayedRequests(gStore.getState()),
-        requestItem,
-        "GET",
-        SIMPLE_SJS
-      );
+      verifyRequestItemTarget(RequestsMenu, requestItem, "GET", SIMPLE_SJS);
     });
 
-    monitor.panelWin.once(EVENTS.RECEIVED_REQUEST_POST_DATA, () => {
+    monitor.panelWin.once(monitor.panelWin.EVENTS.RECEIVED_REQUEST_POST_DATA, () => {
       ok(false, "Trap listener: this request doesn't have any post data.");
     });
 
-    monitor.panelWin.once(EVENTS.RECEIVED_RESPONSE_HEADERS, () => {
-      let requestItem = getSortedRequests(gStore.getState()).get(0);
+    monitor.panelWin.once(monitor.panelWin.EVENTS.RECEIVED_RESPONSE_HEADERS, () => {
+      let requestItem = RequestsMenu.getItemAtIndex(0);
 
       ok(requestItem.responseHeaders,
         "There should be a responseHeaders data available.");
       is(requestItem.responseHeaders.headers.length, 10,
         "The responseHeaders data has an incorrect |headers| property.");
       is(requestItem.responseHeaders.headersSize, 330,
         "The responseHeaders data has an incorrect |headersSize| property.");
 
-      verifyRequestItemTarget(
-        document,
-        getDisplayedRequests(gStore.getState()),
-        requestItem,
-        "GET",
-        SIMPLE_SJS
-      );
+      verifyRequestItemTarget(RequestsMenu, requestItem, "GET", SIMPLE_SJS);
     });
 
-    monitor.panelWin.once(EVENTS.RECEIVED_RESPONSE_COOKIES, () => {
-      let requestItem = getSortedRequests(gStore.getState()).get(0);
+    monitor.panelWin.once(monitor.panelWin.EVENTS.RECEIVED_RESPONSE_COOKIES, () => {
+      let requestItem = RequestsMenu.getItemAtIndex(0);
 
       ok(requestItem.responseCookies,
         "There should be a responseCookies data available.");
       is(requestItem.responseCookies.cookies.length, 2,
         "The responseCookies data has an incorrect |cookies| property.");
 
-      verifyRequestItemTarget(
-        document,
-        getDisplayedRequests(gStore.getState()),
-        requestItem,
-        "GET",
-        SIMPLE_SJS
-      );
+      verifyRequestItemTarget(RequestsMenu, requestItem, "GET", SIMPLE_SJS);
     });
 
-    monitor.panelWin.once(EVENTS.STARTED_RECEIVING_RESPONSE, () => {
-      let requestItem = getSortedRequests(gStore.getState()).get(0);
+    monitor.panelWin.once(monitor.panelWin.EVENTS.STARTED_RECEIVING_RESPONSE, () => {
+      let requestItem = RequestsMenu.getItemAtIndex(0);
 
       is(requestItem.httpVersion, "HTTP/1.1",
         "The httpVersion data has an incorrect value.");
       is(requestItem.status, "200",
         "The status data has an incorrect value.");
       is(requestItem.statusText, "Och Aye",
         "The statusText data has an incorrect value.");
       is(requestItem.headersSize, 330,
         "The headersSize data has an incorrect value.");
 
-      verifyRequestItemTarget(
-        document,
-        getDisplayedRequests(gStore.getState()),
-        requestItem,
-        "GET",
-        SIMPLE_SJS,
-        {
-          status: "200",
-          statusText: "Och Aye"
-        }
-      );
+      verifyRequestItemTarget(RequestsMenu, requestItem, "GET", SIMPLE_SJS, {
+        status: "200",
+        statusText: "Och Aye"
+      });
     });
 
-    monitor.panelWin.once(EVENTS.UPDATING_RESPONSE_CONTENT, () => {
-      let requestItem = getSortedRequests(gStore.getState()).get(0);
+    monitor.panelWin.once(monitor.panelWin.EVENTS.UPDATING_RESPONSE_CONTENT, () => {
+      let requestItem = RequestsMenu.getItemAtIndex(0);
 
       is(requestItem.transferredSize, "12",
         "The transferredSize data has an incorrect value.");
       is(requestItem.contentSize, "12",
         "The contentSize data has an incorrect value.");
       is(requestItem.mimeType, "text/plain; charset=utf-8",
         "The mimeType data has an incorrect value.");
 
-      verifyRequestItemTarget(
-        document,
-        getDisplayedRequests(gStore.getState()),
-        requestItem,
-        "GET",
-        SIMPLE_SJS,
-        {
-          type: "plain",
-          fullMimeType: "text/plain; charset=utf-8",
-          transferred: L10N.getFormatStrWithNumbers("networkMenu.sizeB", 12),
-          size: L10N.getFormatStrWithNumbers("networkMenu.sizeB", 12),
-        }
-      );
+      verifyRequestItemTarget(RequestsMenu, requestItem, "GET", SIMPLE_SJS, {
+        type: "plain",
+        fullMimeType: "text/plain; charset=utf-8",
+        transferred: L10N.getFormatStrWithNumbers("networkMenu.sizeB", 12),
+        size: L10N.getFormatStrWithNumbers("networkMenu.sizeB", 12),
+      });
     });
 
-    monitor.panelWin.once(EVENTS.RECEIVED_RESPONSE_CONTENT, () => {
-      let requestItem = getSortedRequests(gStore.getState()).get(0);
+    monitor.panelWin.once(monitor.panelWin.EVENTS.RECEIVED_RESPONSE_CONTENT, () => {
+      let requestItem = RequestsMenu.getItemAtIndex(0);
 
       ok(requestItem.responseContent,
         "There should be a responseContent data available.");
       is(requestItem.responseContent.content.mimeType,
         "text/plain; charset=utf-8",
         "The responseContent data has an incorrect |content.mimeType| property.");
       is(requestItem.responseContent.content.text,
         "Hello world!",
         "The responseContent data has an incorrect |content.text| property.");
       is(requestItem.responseContent.content.size,
         12,
         "The responseContent data has an incorrect |content.size| property.");
 
-      verifyRequestItemTarget(
-        document,
-        getDisplayedRequests(gStore.getState()),
-        requestItem,
-        "GET",
-        SIMPLE_SJS,
-        {
-          type: "plain",
-          fullMimeType: "text/plain; charset=utf-8",
-          transferred: L10N.getFormatStrWithNumbers("networkMenu.sizeB", 12),
-          size: L10N.getFormatStrWithNumbers("networkMenu.sizeB", 12),
-        }
-      );
+      verifyRequestItemTarget(RequestsMenu, requestItem, "GET", SIMPLE_SJS, {
+        type: "plain",
+        fullMimeType: "text/plain; charset=utf-8",
+        transferred: L10N.getFormatStrWithNumbers("networkMenu.sizeB", 12),
+        size: L10N.getFormatStrWithNumbers("networkMenu.sizeB", 12),
+      });
     });
 
-    monitor.panelWin.once(EVENTS.UPDATING_EVENT_TIMINGS, () => {
-      let requestItem = getSortedRequests(gStore.getState()).get(0);
+    monitor.panelWin.once(monitor.panelWin.EVENTS.UPDATING_EVENT_TIMINGS, () => {
+      let requestItem = RequestsMenu.getItemAtIndex(0);
 
       is(typeof requestItem.totalTime, "number",
         "The attached totalTime is incorrect.");
       ok(requestItem.totalTime >= 0,
         "The attached totalTime should be positive.");
 
-      verifyRequestItemTarget(
-        document,
-        getDisplayedRequests(gStore.getState()),
-        requestItem,
-        "GET",
-        SIMPLE_SJS,
-        {
-          time: true
-        }
-      );
+      verifyRequestItemTarget(RequestsMenu, requestItem, "GET", SIMPLE_SJS, {
+        time: true
+      });
     });
 
-    monitor.panelWin.once(EVENTS.RECEIVED_EVENT_TIMINGS, () => {
-      let requestItem = getSortedRequests(gStore.getState()).get(0);
+    monitor.panelWin.once(monitor.panelWin.EVENTS.RECEIVED_EVENT_TIMINGS, () => {
+      let requestItem = RequestsMenu.getItemAtIndex(0);
 
       ok(requestItem.eventTimings,
         "There should be a eventTimings data available.");
       is(typeof requestItem.eventTimings.timings.blocked, "number",
         "The eventTimings data has an incorrect |timings.blocked| property.");
       is(typeof requestItem.eventTimings.timings.dns, "number",
         "The eventTimings data has an incorrect |timings.dns| property.");
       is(typeof requestItem.eventTimings.timings.connect, "number",
@@ -283,23 +221,16 @@ function test() {
         "The eventTimings data has an incorrect |timings.send| property.");
       is(typeof requestItem.eventTimings.timings.wait, "number",
         "The eventTimings data has an incorrect |timings.wait| property.");
       is(typeof requestItem.eventTimings.timings.receive, "number",
         "The eventTimings data has an incorrect |timings.receive| property.");
       is(typeof requestItem.eventTimings.totalTime, "number",
         "The eventTimings data has an incorrect |totalTime| property.");
 
-      verifyRequestItemTarget(
-        document,
-        getDisplayedRequests(gStore.getState()),
-        requestItem,
-        "GET",
-        SIMPLE_SJS,
-        {
-          time: true
-        }
-      );
+      verifyRequestItemTarget(RequestsMenu, requestItem, "GET", SIMPLE_SJS, {
+        time: true
+      });
     });
 
     tab.linkedBrowser.reload();
   });
 }
--- a/devtools/client/netmonitor/test/browser_net_simple-request-details.js
+++ b/devtools/client/netmonitor/test/browser_net_simple-request-details.js
@@ -8,61 +8,50 @@
  */
 
 add_task(function* () {
   let { L10N } = require("devtools/client/netmonitor/l10n");
 
   let { tab, monitor } = yield initNetMonitor(SIMPLE_SJS);
   info("Starting test... ");
 
-  let { document, gStore, windowRequire } = monitor.panelWin;
-  let Actions = windowRequire("devtools/client/netmonitor/actions/index");
-  let { EVENTS } = windowRequire("devtools/client/netmonitor/events");
-  let {
-    getDisplayedRequests,
-    getSortedRequests,
-  } = windowRequire("devtools/client/netmonitor/selectors/index");
-
-  gStore.dispatch(Actions.batchEnable(false));
+  let { document, EVENTS, Editor, NetMonitorView } = monitor.panelWin;
+  let { RequestsMenu } = NetMonitorView;
+  RequestsMenu.lazyUpdate = false;
 
   let wait = waitForNetworkEvents(monitor, 1);
   tab.linkedBrowser.reload();
   yield wait;
 
-  is(getSelectedRequest(gStore.getState()), undefined,
+  is(RequestsMenu.selectedItem, null,
     "There shouldn't be any selected item in the requests menu.");
-  is(gStore.getState().requests.requests.size, 1,
+  is(RequestsMenu.itemCount, 1,
     "The requests menu should not be empty after the first request.");
   is(!!document.querySelector(".network-details-panel"), false,
     "The network details panel should still be hidden after first request.");
 
-  EventUtils.sendMouseEvent({ type: "click" },
+  let onTabUpdated = monitor.panelWin.once(EVENTS.TAB_UPDATED);
+  EventUtils.sendMouseEvent({ type: "mousedown" },
     document.querySelector(".network-details-panel-toggle"));
+  yield onTabUpdated;
 
-  isnot(getSelectedRequest(gStore.getState()), undefined,
+  isnot(RequestsMenu.selectedItem, null,
     "There should be a selected item in the requests menu.");
-  is(getSelectedIndex(gStore.getState()), 0,
+  is(RequestsMenu.selectedIndex, 0,
     "The first item should be selected in the requests menu.");
   is(!!document.querySelector(".network-details-panel"), true,
     "The network details panel should not be hidden after toggle button was pressed.");
 
   testHeadersTab();
   yield testCookiesTab();
   testParamsTab();
   yield testResponseTab();
   testTimingsTab();
   return teardown(monitor);
 
-  function getSelectedIndex(state) {
-    if (!state.requests.selectedId) {
-      return -1;
-    }
-    return getSortedRequests(state).findIndex(r => r.id === state.requests.selectedId);
-  }
-
   function testHeadersTab() {
     let tabEl = document.querySelectorAll("#details-pane tab")[0];
     let tabpanel = document.querySelectorAll("#details-pane tabpanel")[0];
 
     is(tabEl.getAttribute("selected"), "true",
       "The headers tab in the network details pane should be selected.");
 
     is(tabpanel.querySelector("#headers-summary-url-value").getAttribute("value"),
--- a/devtools/client/netmonitor/test/browser_net_simple-request.js
+++ b/devtools/client/netmonitor/test/browser_net_simple-request.js
@@ -11,68 +11,63 @@
  * 2) Side panel toggle button
  * 3) Empty user message visibility
  * 4) Number of requests displayed
  */
 add_task(function* () {
   let { tab, monitor } = yield initNetMonitor(SIMPLE_URL);
   info("Starting test... ");
 
-  let { document, gStore, windowRequire } = monitor.panelWin;
-  let Actions = windowRequire("devtools/client/netmonitor/actions/index");
-  let { EVENTS } = windowRequire("devtools/client/netmonitor/events");
-  let {
-    getDisplayedRequests,
-    getSortedRequests,
-  } = windowRequire("devtools/client/netmonitor/selectors/index");
+  let { document, NetMonitorView } = monitor.panelWin;
+  let { RequestsMenu } = NetMonitorView;
 
-  gStore.dispatch(Actions.batchEnable(false));
+  RequestsMenu.lazyUpdate = false;
 
   is(document.querySelector(".network-details-panel-toggle").hasAttribute("disabled"),
     true,
     "The pane toggle button should be disabled when the frontend is opened.");
   ok(document.querySelector("#requests-menu-empty-notice"),
     "An empty notice should be displayed when the frontend is opened.");
-  is(gStore.getState().requests.requests.size, 0,
+  is(RequestsMenu.itemCount, 0,
     "The requests menu should be empty when the frontend is opened.");
   is(!!document.querySelector(".network-details-panel"), false,
     "The network details panel should be hidden when the frontend is opened.");
 
   yield reloadAndWait();
 
   is(document.querySelector(".network-details-panel-toggle").hasAttribute("disabled"),
     false,
     "The pane toggle button should be enabled after the first request.");
   ok(!document.querySelector("#requests-menu-empty-notice"),
     "The empty notice should be hidden after the first request.");
-  is(gStore.getState().requests.requests.size, 1,
+  is(RequestsMenu.itemCount, 1,
     "The requests menu should not be empty after the first request.");
   is(!!document.querySelector(".network-details-panel"), false,
     "The network details panel should still be hidden after the first request.");
 
   yield reloadAndWait();
 
   is(document.querySelector(".network-details-panel-toggle").hasAttribute("disabled"),
     false,
     "The pane toggle button should be still be enabled after a reload.");
   ok(!document.querySelector("#requests-menu-empty-notice"),
     "The empty notice should be still hidden after a reload.");
-  is(gStore.getState().requests.requests.size, 1,
+  is(RequestsMenu.itemCount, 1,
     "The requests menu should not be empty after a reload.");
   is(!!document.querySelector(".network-details-panel"), false,
     "The network details panel should still be hidden after a reload.");
 
-  gStore.dispatch(Actions.clearRequests());
+  RequestsMenu.clear();
 
   is(document.querySelector(".network-details-panel-toggle").hasAttribute("disabled"),
     true,
     "The pane toggle button should be disabled when after clear.");
   ok(document.querySelector("#requests-menu-empty-notice"),
     "An empty notice should be displayed again after clear.");
-  is(gStore.getState().requests.requests.size, 0,
+  is(RequestsMenu.itemCount, 0,
     "The requests menu should be empty after clear.");
   is(!!document.querySelector(".network-details-panel"), false,
     "The network details panel should still be hidden after clear.");
 
   return teardown(monitor);
 
   function* reloadAndWait() {
     let wait = waitForNetworkEvents(monitor, 1);
--- a/devtools/client/netmonitor/test/browser_net_sort-01.js
+++ b/devtools/client/netmonitor/test/browser_net_sort-01.js
@@ -1,236 +1,219 @@
 /* Any copyright is dedicated to the Public Domain.
    http://creativecommons.org/publicdomain/zero/1.0/ */
 
 "use strict";
 
 /**
- * Test if sorting columns in the network table works correctly with new requests.
+ * Test if the sorting mechanism works correctly.
  */
 
 add_task(function* () {
   let { L10N } = require("devtools/client/netmonitor/l10n");
 
-  let { monitor } = yield initNetMonitor(SORTING_URL);
+  let { tab, monitor } = yield initNetMonitor(STATUS_CODES_URL);
   info("Starting test... ");
 
-  // It seems that this test may be slow on debug builds. This could be because
-  // of the heavy dom manipulation associated with sorting.
-  requestLongerTimeout(2);
-
-  let { document, gStore, windowRequire } = monitor.panelWin;
-  let Actions = windowRequire("devtools/client/netmonitor/actions/index");
-  let {
-    getDisplayedRequests,
-    getSelectedRequest,
-    getSortedRequests,
-  } = windowRequire("devtools/client/netmonitor/selectors/index");
-
-  gStore.dispatch(Actions.batchEnable(false));
+  let { $all, NetMonitorView } = monitor.panelWin;
+  let { RequestsMenu } = NetMonitorView;
 
-  // Loading the frame script and preparing the xhr request URLs so we can
-  // generate some requests later.
-  loadCommonFrameScript();
-  let requests = [{
-    url: "sjs_sorting-test-server.sjs?index=1&" + Math.random(),
-    method: "GET1"
-  }, {
-    url: "sjs_sorting-test-server.sjs?index=5&" + Math.random(),
-    method: "GET5"
-  }, {
-    url: "sjs_sorting-test-server.sjs?index=2&" + Math.random(),
-    method: "GET2"
-  }, {
-    url: "sjs_sorting-test-server.sjs?index=4&" + Math.random(),
-    method: "GET4"
-  }, {
-    url: "sjs_sorting-test-server.sjs?index=3&" + Math.random(),
-    method: "GET3"
-  }];
+  RequestsMenu.lazyUpdate = false;
 
   let wait = waitForNetworkEvents(monitor, 5);
-  yield performRequestsInContent(requests);
+  yield ContentTask.spawn(tab.linkedBrowser, {}, function* () {
+    content.wrappedJSObject.performRequests();
+  });
   yield wait;
 
-  EventUtils.sendMouseEvent({ type: "click" },
-    document.querySelector(".network-details-panel-toggle"));
+  testContents([0, 1, 2, 3, 4]);
+
+  info("Testing swap(0, 0)");
+  RequestsMenu.swapItemsAtIndices(0, 0);
+  RequestsMenu.refreshZebra();
+  testContents([0, 1, 2, 3, 4]);
+
+  info("Testing swap(0, 1)");
+  RequestsMenu.swapItemsAtIndices(0, 1);
+  RequestsMenu.refreshZebra();
+  testContents([1, 0, 2, 3, 4]);
 
-  isnot(getSelectedRequest(gStore.getState()), undefined,
-    "There should be a selected item in the requests menu.");
-  is(getSelectedIndex(gStore.getState()), 0,
-    "The first item should be selected in the requests menu.");
-  is(!!document.querySelector(".network-details-panel"), true,
-    "The network details panel should be visible after toggle button was pressed.");
+  info("Testing swap(0, 2)");
+  RequestsMenu.swapItemsAtIndices(0, 2);
+  RequestsMenu.refreshZebra();
+  testContents([1, 2, 0, 3, 4]);
+
+  info("Testing swap(0, 3)");
+  RequestsMenu.swapItemsAtIndices(0, 3);
+  RequestsMenu.refreshZebra();
+  testContents([1, 2, 3, 0, 4]);
+
+  info("Testing swap(0, 4)");
+  RequestsMenu.swapItemsAtIndices(0, 4);
+  RequestsMenu.refreshZebra();
+  testContents([1, 2, 3, 4, 0]);
 
-  testHeaders();
-  testContents([0, 2, 4, 3, 1], 0);
+  info("Testing swap(1, 0)");
+  RequestsMenu.swapItemsAtIndices(1, 0);
+  RequestsMenu.refreshZebra();
+  testContents([0, 2, 3, 4, 1]);
+
+  info("Testing swap(1, 1)");
+  RequestsMenu.swapItemsAtIndices(1, 1);
+  RequestsMenu.refreshZebra();
+  testContents([0, 2, 3, 4, 1]);
+
+  info("Testing swap(1, 2)");
+  RequestsMenu.swapItemsAtIndices(1, 2);
+  RequestsMenu.refreshZebra();
+  testContents([0, 1, 3, 4, 2]);
 
-  info("Testing status sort, ascending.");
-  EventUtils.sendMouseEvent({ type: "click" },
-    document.querySelector("#requests-menu-status-button"));
-  testHeaders("status", "ascending");
-  testContents([0, 1, 2, 3, 4], 0);
+  info("Testing swap(1, 3)");
+  RequestsMenu.swapItemsAtIndices(1, 3);
+  RequestsMenu.refreshZebra();
+  testContents([0, 3, 1, 4, 2]);
+
+  info("Testing swap(1, 4)");
+  RequestsMenu.swapItemsAtIndices(1, 4);
+  RequestsMenu.refreshZebra();
+  testContents([0, 3, 4, 1, 2]);
 
-  info("Performing more requests.");
-  wait = waitForNetworkEvents(monitor, 5);
-  yield performRequestsInContent(requests);
-  yield wait;
+  info("Testing swap(2, 0)");
+  RequestsMenu.swapItemsAtIndices(2, 0);
+  RequestsMenu.refreshZebra();
+  testContents([2, 3, 4, 1, 0]);
+
+  info("Testing swap(2, 1)");
+  RequestsMenu.swapItemsAtIndices(2, 1);
+  RequestsMenu.refreshZebra();
+  testContents([1, 3, 4, 2, 0]);
 
-  info("Testing status sort again, ascending.");
-  testHeaders("status", "ascending");
-  testContents([0, 1, 2, 3, 4, 5, 6, 7, 8, 9], 0);
+  info("Testing swap(2, 2)");
+  RequestsMenu.swapItemsAtIndices(2, 2);
+  RequestsMenu.refreshZebra();
+  testContents([1, 3, 4, 2, 0]);
+
+  info("Testing swap(2, 3)");
+  RequestsMenu.swapItemsAtIndices(2, 3);
+  RequestsMenu.refreshZebra();
+  testContents([1, 2, 4, 3, 0]);
+
+  info("Testing swap(2, 4)");
+  RequestsMenu.swapItemsAtIndices(2, 4);
+  RequestsMenu.refreshZebra();
+  testContents([1, 4, 2, 3, 0]);
 
-  info("Testing status sort, descending.");
-  EventUtils.sendMouseEvent({ type: "click" },
-    document.querySelector("#requests-menu-status-button"));
-  testHeaders("status", "descending");
-  testContents([9, 8, 7, 6, 5, 4, 3, 2, 1, 0], 9);
+  info("Testing swap(3, 0)");
+  RequestsMenu.swapItemsAtIndices(3, 0);
+  RequestsMenu.refreshZebra();
+  testContents([1, 4, 2, 0, 3]);
 
-  info("Performing more requests.");
-  wait = waitForNetworkEvents(monitor, 5);
-  yield performRequestsInContent(requests);
-  yield wait;
+  info("Testing swap(3, 1)");
+  RequestsMenu.swapItemsAtIndices(3, 1);
+  RequestsMenu.refreshZebra();
+  testContents([3, 4, 2, 0, 1]);
+
+  info("Testing swap(3, 2)");
+  RequestsMenu.swapItemsAtIndices(3, 2);
+  RequestsMenu.refreshZebra();
+  testContents([2, 4, 3, 0, 1]);
 
-  info("Testing status sort again, descending.");
-  testHeaders("status", "descending");
-  testContents([14, 13, 12, 11, 10, 9, 8, 7, 6, 5, 4, 3, 2, 1, 0], 14);
+  info("Testing swap(3, 3)");
+  RequestsMenu.swapItemsAtIndices(3, 3);
+  RequestsMenu.refreshZebra();
+  testContents([2, 4, 3, 0, 1]);
+
+  info("Testing swap(3, 4)");
+  RequestsMenu.swapItemsAtIndices(3, 4);
+  RequestsMenu.refreshZebra();
+  testContents([2, 3, 4, 0, 1]);
+
+  info("Testing swap(4, 0)");
+  RequestsMenu.swapItemsAtIndices(4, 0);
+  RequestsMenu.refreshZebra();
+  testContents([2, 3, 0, 4, 1]);
 
-  info("Testing status sort yet again, ascending.");
-  EventUtils.sendMouseEvent({ type: "click" },
-    document.querySelector("#requests-menu-status-button"));
-  testHeaders("status", "ascending");
-  testContents([0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14], 0);
+  info("Testing swap(4, 1)");
+  RequestsMenu.swapItemsAtIndices(4, 1);
+  RequestsMenu.refreshZebra();
+  testContents([2, 3, 0, 1, 4]);
+
+  info("Testing swap(4, 2)");
+  RequestsMenu.swapItemsAtIndices(4, 2);
+  RequestsMenu.refreshZebra();
+  testContents([4, 3, 0, 1, 2]);
 
-  info("Testing status sort yet again, descending.");
-  EventUtils.sendMouseEvent({ type: "click" },
-    document.querySelector("#requests-menu-status-button"));
-  testHeaders("status", "descending");
-  testContents([14, 13, 12, 11, 10, 9, 8, 7, 6, 5, 4, 3, 2, 1, 0], 14);
+  info("Testing swap(4, 3)");
+  RequestsMenu.swapItemsAtIndices(4, 3);
+  RequestsMenu.refreshZebra();
+  testContents([3, 4, 0, 1, 2]);
+
+  info("Testing swap(4, 4)");
+  RequestsMenu.swapItemsAtIndices(4, 4);
+  RequestsMenu.refreshZebra();
+  testContents([3, 4, 0, 1, 2]);
+
+  info("Clearing sort.");
+  RequestsMenu.sortBy();
+  testContents([0, 1, 2, 3, 4]);
 
   return teardown(monitor);
 
-  function testHeaders(sortType, direction) {
-    let doc = monitor.panelWin.document;
-    let target = doc.querySelector("#requests-menu-" + sortType + "-button");
-    let headers = doc.querySelectorAll(".requests-menu-header-button");
-
-    for (let header of headers) {
-      if (header != target) {
-        ok(!header.hasAttribute("data-sorted"),
-          "The " + header.id + " header does not have a 'data-sorted' attribute.");
-        ok(!header.getAttribute("title"),
-          "The " + header.id + " header does not have a 'title' attribute.");
-      } else {
-        is(header.getAttribute("data-sorted"), direction,
-          "The " + header.id + " header has a correct 'data-sorted' attribute.");
-        is(header.getAttribute("title"), direction == "ascending"
-          ? L10N.getStr("networkMenu.sortedAsc")
-          : L10N.getStr("networkMenu.sortedDesc"),
-          "The " + header.id + " header has a correct 'title' attribute.");
-      }
-    }
-  }
-
-  function getSelectedIndex(state) {
-    if (!state.requests.selectedId) {
-      return -1;
-    }
-    return getSortedRequests(state).findIndex(r => r.id === state.requests.selectedId);
-  }
-
-  function testContents(order, selection) {
-    isnot(getSelectedRequest(gStore.getState()), undefined,
-      "There should still be a selected item after sorting.");
-    is(getSelectedIndex(gStore.getState()), selection,
-      "The first item should be still selected after sorting.");
-    is(!!document.querySelector(".network-details-panel"), true,
-      "The network details panel should still be visible after sorting.");
-
-    is(getSortedRequests(gStore.getState()).length, order.length,
-      "There should be a specific number of items in the requests menu.");
-    is(getDisplayedRequests(gStore.getState()).length, order.length,
-      "There should be a specific number of visbile items in the requests menu.");
-    is(document.querySelectorAll(".request-list-item").length, order.length,
+  function testContents([a, b, c, d, e]) {
+    is(RequestsMenu.items.length, 5,
+      "There should be a total of 5 items in the requests menu.");
+    is(RequestsMenu.visibleItems.length, 5,
+      "There should be a total of 5 visbile items in the requests menu.");
+    is($all(".request-list-item").length, 5,
       "The visible items in the requests menu are, in fact, visible!");
 
-    for (let i = 0, len = order.length / 5; i < len; i++) {
-      verifyRequestItemTarget(
-        document,
-        getDisplayedRequests(gStore.getState()),
-        getSortedRequests(gStore.getState()).get(order[i]),
-        "GET1", SORTING_SJS + "?index=1", {
-          fuzzyUrl: true,
-          status: 101,
-          statusText: "Meh",
-          type: "1",
-          fullMimeType: "text/1",
-          transferred: L10N.getStr("networkMenu.sizeUnavailable"),
-          size: L10N.getFormatStrWithNumbers("networkMenu.sizeB", 0),
-          time: true
-        });
-    }
-    for (let i = 0, len = order.length / 5; i < len; i++) {
-      verifyRequestItemTarget(
-        document,
-        getDisplayedRequests(gStore.getState()),
-        getSortedRequests(gStore.getState()).get(order[i + len]),
-        "GET2", SORTING_SJS + "?index=2", {
-          fuzzyUrl: true,
-          status: 200,
-          statusText: "Meh",
-          type: "2",
-          fullMimeType: "text/2",
-          transferred: L10N.getFormatStrWithNumbers("networkMenu.sizeB", 19),
-          size: L10N.getFormatStrWithNumbers("networkMenu.sizeB", 19),
-          time: true
-        });
-    }
-    for (let i = 0, len = order.length / 5; i < len; i++) {
-      verifyRequestItemTarget(
-        document,
-        getDisplayedRequests(gStore.getState()),
-        getSortedRequests(gStore.getState()).get(order[i + len * 2]),
-        "GET3", SORTING_SJS + "?index=3", {
-          fuzzyUrl: true,
-          status: 300,
-          statusText: "Meh",
-          type: "3",
-          fullMimeType: "text/3",
-          transferred: L10N.getFormatStrWithNumbers("networkMenu.sizeB", 29),
-          size: L10N.getFormatStrWithNumbers("networkMenu.sizeB", 29),
-          time: true
-        });
-    }
-    for (let i = 0, len = order.length / 5; i < len; i++) {
-      verifyRequestItemTarget(
-        document,
-        getDisplayedRequests(gStore.getState()),
-        getSortedRequests(gStore.getState()).get(order[i + len * 3]),
-        "GET4", SORTING_SJS + "?index=4", {
-          fuzzyUrl: true,
-          status: 400,
-          statusText: "Meh",
-          type: "4",
-          fullMimeType: "text/4",
-          transferred: L10N.getFormatStrWithNumbers("networkMenu.sizeB", 39),
-          size: L10N.getFormatStrWithNumbers("networkMenu.sizeB", 39),
-          time: true
-        });
-    }
-    for (let i = 0, len = order.length / 5; i < len; i++) {
-      verifyRequestItemTarget(
-        document,
-        getDisplayedRequests(gStore.getState()),
-        getSortedRequests(gStore.getState()).get(order[i + len * 4]),
-        "GET5", SORTING_SJS + "?index=5", {
-          fuzzyUrl: true,
-          status: 500,
-          statusText: "Meh",
-          type: "5",
-          fullMimeType: "text/5",
-          transferred: L10N.getFormatStrWithNumbers("networkMenu.sizeB", 49),
-          size: L10N.getFormatStrWithNumbers("networkMenu.sizeB", 49),
-          time: true
-        });
-    }
+    verifyRequestItemTarget(RequestsMenu, RequestsMenu.getItemAtIndex(a),
+      "GET", STATUS_CODES_SJS + "?sts=100", {
+        status: 101,
+        statusText: "Switching Protocols",
+        type: "plain",
+        fullMimeType: "text/plain; charset=utf-8",
+        transferred: L10N.getStr("networkMenu.sizeUnavailable"),
+        size: L10N.getFormatStrWithNumbers("networkMenu.sizeB", 0),
+        time: true
+      });
+    verifyRequestItemTarget(RequestsMenu, RequestsMenu.getItemAtIndex(b),
+      "GET", STATUS_CODES_SJS + "?sts=200", {
+        status: 202,
+        statusText: "Created",
+        type: "plain",
+        fullMimeType: "text/plain; charset=utf-8",
+        transferred: L10N.getFormatStrWithNumbers("networkMenu.sizeB", 22),
+        size: L10N.getFormatStrWithNumbers("networkMenu.sizeB", 22),
+        time: true
+      });
+    verifyRequestItemTarget(RequestsMenu, RequestsMenu.getItemAtIndex(c),
+      "GET", STATUS_CODES_SJS + "?sts=300", {
+        status: 303,
+        statusText: "See Other",
+        type: "plain",
+        fullMimeType: "text/plain; charset=utf-8",
+        transferred: L10N.getFormatStrWithNumbers("networkMenu.sizeB", 22),
+        size: L10N.getFormatStrWithNumbers("networkMenu.sizeB", 0),
+        time: true
+      });
+    verifyRequestItemTarget(RequestsMenu, RequestsMenu.getItemAtIndex(d),
+      "GET", STATUS_CODES_SJS + "?sts=400", {
+        status: 404,
+        statusText: "Not Found",
+        type: "plain",
+        fullMimeType: "text/plain; charset=utf-8",
+        transferred: L10N.getFormatStrWithNumbers("networkMenu.sizeB", 22),
+        size: L10N.getFormatStrWithNumbers("networkMenu.sizeB", 22),
+        time: true
+      });
+    verifyRequestItemTarget(RequestsMenu, RequestsMenu.getItemAtIndex(e),
+      "GET", STATUS_CODES_SJS + "?sts=500", {
+        status: 501,
+        statusText: "Not Implemented",
+        type: "plain",
+        fullMimeType: "text/plain; charset=utf-8",
+        transferred: L10N.getFormatStrWithNumbers("networkMenu.sizeB", 22),
+        size: L10N.getFormatStrWithNumbers("networkMenu.sizeB", 22),
+        time: true
+      });
   }
 });
--- a/devtools/client/netmonitor/test/browser_net_sort-02.js
+++ b/devtools/client/netmonitor/test/browser_net_sort-02.js
@@ -12,25 +12,18 @@ add_task(function* () {
 
   let { monitor } = yield initNetMonitor(SORTING_URL);
   info("Starting test... ");
 
   // It seems that this test may be slow on debug builds. This could be because
   // of the heavy dom manipulation associated with sorting.
   requestLongerTimeout(2);
 
-  let { document, gStore, windowRequire } = monitor.panelWin;
-  let Actions = windowRequire("devtools/client/netmonitor/actions/index");
-  let {
-    getDisplayedRequests,
-    getSelectedRequest,
-    getSortedRequests,
-  } = windowRequire("devtools/client/netmonitor/selectors/index");
-
-  gStore.dispatch(Actions.batchEnable(false));
+  let { document, NetMonitorView } = monitor.panelWin;
+  let { RequestsMenu } = NetMonitorView;
 
   // Loading the frame script and preparing the xhr request URLs so we can
   // generate some requests later.
   loadCommonFrameScript();
   let requests = [{
     url: "sjs_sorting-test-server.sjs?index=1&" + Math.random(),
     method: "GET1"
   }, {
@@ -42,26 +35,28 @@ add_task(function* () {
   }, {
     url: "sjs_sorting-test-server.sjs?index=4&" + Math.random(),
     method: "GET4"
   }, {
     url: "sjs_sorting-test-server.sjs?index=3&" + Math.random(),
     method: "GET3"
   }];
 
+  RequestsMenu.lazyUpdate = false;
+
   let wait = waitForNetworkEvents(monitor, 5);
   yield performRequestsInContent(requests);
   yield wait;
 
-  EventUtils.sendMouseEvent({ type: "click" },
+  EventUtils.sendMouseEvent({ type: "mousedown" },
     document.querySelector(".network-details-panel-toggle"));
 
-  isnot(getSelectedRequest(gStore.getState()), undefined,
+  isnot(RequestsMenu.selectedItem, null,
     "There should be a selected item in the requests menu.");
-  is(getSelectedIndex(gStore.getState()), 0,
+  is(RequestsMenu.selectedIndex, 0,
     "The first item should be selected in the requests menu.");
   is(!!document.querySelector(".network-details-panel"), true,
     "The network details panel should be visible after toggle button was pressed.");
 
   testHeaders();
   testContents([0, 2, 4, 3, 1]);
 
   info("Testing status sort, ascending.");
@@ -187,23 +182,16 @@ add_task(function* () {
   info("Testing waterfall sort, ascending. Checking sort loops correctly.");
   EventUtils.sendMouseEvent({ type: "click" },
     document.querySelector("#requests-menu-waterfall-button"));
   testHeaders("waterfall", "ascending");
   testContents([0, 2, 4, 3, 1]);
 
   return teardown(monitor);
 
-  function getSelectedIndex(state) {
-    if (!state.requests.selectedId) {
-      return -1;
-    }
-    return getSortedRequests(state).findIndex(r => r.id === state.requests.selectedId);
-  }
-
   function testHeaders(sortType, direction) {
     let doc = monitor.panelWin.document;
     let target = doc.querySelector("#requests-menu-" + sortType + "-button");
     let headers = doc.querySelectorAll(".requests-menu-header-button");
 
     for (let header of headers) {
       if (header != target) {
         ok(!header.hasAttribute("data-sorted"),
@@ -217,90 +205,75 @@ add_task(function* () {
           ? L10N.getStr("networkMenu.sortedAsc")
           : L10N.getStr("networkMenu.sortedDesc"),
           "The " + header.id + " header has a correct 'title' attribute.");
       }
     }
   }
 
   function testContents([a, b, c, d, e]) {
-    isnot(getSelectedRequest(gStore.getState()), undefined,
+    isnot(RequestsMenu.selectedItem, null,
       "There should still be a selected item after sorting.");
-    is(getSelectedIndex(gStore.getState()), a,
+    is(RequestsMenu.selectedIndex, a,
       "The first item should be still selected after sorting.");
     is(!!document.querySelector(".network-details-panel"), true,
       "The network details panel should still be visible after sorting.");
 
-    is(getSortedRequests(gStore.getState()).length, 5,
+    is(RequestsMenu.items.length, 5,
       "There should be a total of 5 items in the requests menu.");
-    is(getDisplayedRequests(gStore.getState()).length, 5,
+    is(RequestsMenu.visibleItems.length, 5,
       "There should be a total of 5 visible items in the requests menu.");
     is(document.querySelectorAll(".request-list-item").length, 5,
       "The visible items in the requests menu are, in fact, visible!");
 
-    verifyRequestItemTarget(
-      document,
-      getDisplayedRequests(gStore.getState()),
-      getSortedRequests(gStore.getState()).get(a),
+    verifyRequestItemTarget(RequestsMenu, RequestsMenu.getItemAtIndex(a),
       "GET1", SORTING_SJS + "?index=1", {
         fuzzyUrl: true,
         status: 101,
         statusText: "Meh",
         type: "1",
         fullMimeType: "text/1",
         transferred: L10N.getStr("networkMenu.sizeUnavailable"),
         size: L10N.getFormatStrWithNumbers("networkMenu.sizeB", 0),
         time: true
       });
-    verifyRequestItemTarget(
-      document,
-      getDisplayedRequests(gStore.getState()),
-      getSortedRequests(gStore.getState()).get(b),
+    verifyRequestItemTarget(RequestsMenu, RequestsMenu.getItemAtIndex(b),
       "GET2", SORTING_SJS + "?index=2", {
         fuzzyUrl: true,
         status: 200,
         statusText: "Meh",
         type: "2",
         fullMimeType: "text/2",
         transferred: L10N.getFormatStrWithNumbers("networkMenu.sizeB", 19),
         size: L10N.getFormatStrWithNumbers("networkMenu.sizeB", 19),
         time: true
       });
-    verifyRequestItemTarget(
-      document,
-      getDisplayedRequests(gStore.getState()),
-      getSortedRequests(gStore.getState()).get(c),
+    verifyRequestItemTarget(RequestsMenu, RequestsMenu.getItemAtIndex(c),
       "GET3", SORTING_SJS + "?index=3", {
         fuzzyUrl: true,
         status: 300,
         statusText: "Meh",
         type: "3",
         fullMimeType: "text/3",
         transferred: L10N.getFormatStrWithNumbers("networkMenu.sizeB", 29),
         size: L10N.getFormatStrWithNumbers("networkMenu.sizeB", 29),
         time: true
       });
-    verifyRequestItemTarget(
-      document,
-      getDisplayedRequests(gStore.getState()),
-      getSortedRequests(gStore.getState()).get(d),
+    verifyRequestItemTarget(RequestsMenu, RequestsMenu.getItemAtIndex(d),
       "GET4", SORTING_SJS + "?index=4", {
         fuzzyUrl: true,
         status: 400,
         statusText: "Meh",
         type: "4",
         fullMimeType: "text/4",
         transferred: L10N.getFormatStrWithNumbers("networkMenu.sizeB", 39),
         size: L10N.getFormatStrWithNumbers("networkMenu.sizeB", 39),
         time: true
       });
-    verifyRequestItemTarget(
-      document,
-      getDisplayedRequests(gStore.getState()),
-      getSortedRequests(gStore.getState()).get(e),
+    verifyRequestItemTarget(RequestsMenu, RequestsMenu.getItemAtIndex(e),
       "GET5", SORTING_SJS + "?index=5", {
         fuzzyUrl: true,
         status: 500,
         statusText: "Meh",
         type: "5",
         fullMimeType: "text/5",
         transferred: L10N.getFormatStrWithNumbers("networkMenu.sizeB", 49),
         size: L10N.getFormatStrWithNumbers("networkMenu.sizeB", 49),
new file mode 100644
--- /dev/null
+++ b/devtools/client/netmonitor/test/browser_net_sort-03.js
@@ -0,0 +1,214 @@
+/* Any copyright is dedicated to the Public Domain.
+   http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+/**
+ * Test if sorting columns in the network table works correctly with new requests.
+ */
+
+add_task(function* () {
+  let { L10N } = require("devtools/client/netmonitor/l10n");
+
+  let { monitor } = yield initNetMonitor(SORTING_URL);
+  info("Starting test... ");
+
+  // It seems that this test may be slow on debug builds. This could be because
+  // of the heavy dom manipulation associated with sorting.
+  requestLongerTimeout(2);
+
+  let { document, NetMonitorView } = monitor.panelWin;
+  let { RequestsMenu } = NetMonitorView;
+
+  // Loading the frame script and preparing the xhr request URLs so we can
+  // generate some requests later.
+  loadCommonFrameScript();
+  let requests = [{
+    url: "sjs_sorting-test-server.sjs?index=1&" + Math.random(),
+    method: "GET1"
+  }, {
+    url: "sjs_sorting-test-server.sjs?index=5&" + Math.random(),
+    method: "GET5"
+  }, {
+    url: "sjs_sorting-test-server.sjs?index=2&" + Math.random(),
+    method: "GET2"
+  }, {
+    url: "sjs_sorting-test-server.sjs?index=4&" + Math.random(),
+    method: "GET4"
+  }, {
+    url: "sjs_sorting-test-server.sjs?index=3&" + Math.random(),
+    method: "GET3"
+  }];
+
+  RequestsMenu.lazyUpdate = false;
+
+  let wait = waitForNetworkEvents(monitor, 5);
+  yield performRequestsInContent(requests);
+  yield wait;
+
+  EventUtils.sendMouseEvent({ type: "mousedown" },
+    document.querySelector(".network-details-panel-toggle"));
+
+  isnot(RequestsMenu.selectedItem, null,
+    "There should be a selected item in the requests menu.");
+  is(RequestsMenu.selectedIndex, 0,
+    "The first item should be selected in the requests menu.");
+  is(!!document.querySelector(".network-details-panel"), true,
+    "The network details panel should be visible after toggle button was pressed.");
+
+  testHeaders();
+  testContents([0, 2, 4, 3, 1], 0);
+
+  info("Testing status sort, ascending.");
+  EventUtils.sendMouseEvent({ type: "click" },
+    document.querySelector("#requests-menu-status-button"));
+  testHeaders("status", "ascending");
+  testContents([0, 1, 2, 3, 4], 0);
+
+  info("Performing more requests.");
+  wait = waitForNetworkEvents(monitor, 5);
+  yield performRequestsInContent(requests);
+  yield wait;
+
+  info("Testing status sort again, ascending.");
+  testHeaders("status", "ascending");
+  testContents([0, 1, 2, 3, 4, 5, 6, 7, 8, 9], 0);
+
+  info("Testing status sort, descending.");
+  EventUtils.sendMouseEvent({ type: "click" },
+    document.querySelector("#requests-menu-status-button"));
+  testHeaders("status", "descending");
+  testContents([9, 8, 7, 6, 5, 4, 3, 2, 1, 0], 9);
+
+  info("Performing more requests.");
+  wait = waitForNetworkEvents(monitor, 5);
+  yield performRequestsInContent(requests);
+  yield wait;
+
+  info("Testing status sort again, descending.");
+  testHeaders("status", "descending");
+  testContents([14, 13, 12, 11, 10, 9, 8, 7, 6, 5, 4, 3, 2, 1, 0], 14);
+
+  info("Testing status sort yet again, ascending.");
+  EventUtils.sendMouseEvent({ type: "click" },
+    document.querySelector("#requests-menu-status-button"));
+  testHeaders("status", "ascending");
+  testContents([0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14], 0);
+
+  info("Testing status sort yet again, descending.");
+  EventUtils.sendMouseEvent({ type: "click" },
+    document.querySelector("#requests-menu-status-button"));
+  testHeaders("status", "descending");
+  testContents([14, 13, 12, 11, 10, 9, 8, 7, 6, 5, 4, 3, 2, 1, 0], 14);
+
+  return teardown(monitor);
+
+  function testHeaders(sortType, direction) {
+    let doc = monitor.panelWin.document;
+    let target = doc.querySelector("#requests-menu-" + sortType + "-button");
+    let headers = doc.querySelectorAll(".requests-menu-header-button");
+
+    for (let header of headers) {
+      if (header != target) {
+        ok(!header.hasAttribute("data-sorted"),
+          "The " + header.id + " header does not have a 'data-sorted' attribute.");
+        ok(!header.getAttribute("title"),
+          "The " + header.id + " header does not have a 'title' attribute.");
+      } else {
+        is(header.getAttribute("data-sorted"), direction,
+          "The " + header.id + " header has a correct 'data-sorted' attribute.");
+        is(header.getAttribute("title"), direction == "ascending"
+          ? L10N.getStr("networkMenu.sortedAsc")
+          : L10N.getStr("networkMenu.sortedDesc"),
+          "The " + header.id + " header has a correct 'title' attribute.");
+      }
+    }
+  }
+
+  function testContents(order, selection) {
+    isnot(RequestsMenu.selectedItem, null,
+      "There should still be a selected item after sorting.");
+    is(RequestsMenu.selectedIndex, selection,
+      "The first item should be still selected after sorting.");
+    is(!!document.querySelector(".network-details-panel"), true,
+      "The network details panel should still be visible after sorting.");
+
+    is(RequestsMenu.items.length, order.length,
+      "There should be a specific number of items in the requests menu.");
+    is(RequestsMenu.visibleItems.length, order.length,
+      "There should be a specific number of visbile items in the requests menu.");
+    is(document.querySelectorAll(".request-list-item").length, order.length,
+      "The visible items in the requests menu are, in fact, visible!");
+
+    for (let i = 0, len = order.length / 5; i < len; i++) {
+      verifyRequestItemTarget(RequestsMenu,
+        RequestsMenu.getItemAtIndex(order[i]),
+        "GET1", SORTING_SJS + "?index=1", {
+          fuzzyUrl: true,
+          status: 101,
+          statusText: "Meh",
+          type: "1",
+          fullMimeType: "text/1",
+          transferred: L10N.getStr("networkMenu.sizeUnavailable"),
+          size: L10N.getFormatStrWithNumbers("networkMenu.sizeB", 0),
+          time: true
+        });
+    }
+    for (let i = 0, len = order.length / 5; i < len; i++) {
+      verifyRequestItemTarget(RequestsMenu,
+        RequestsMenu.getItemAtIndex(order[i + len]),
+        "GET2", SORTING_SJS + "?index=2", {
+          fuzzyUrl: true,
+          status: 200,
+          statusText: "Meh",
+          type: "2",
+          fullMimeType: "text/2",
+          transferred: L10N.getFormatStrWithNumbers("networkMenu.sizeB", 19),
+          size: L10N.getFormatStrWithNumbers("networkMenu.sizeB", 19),
+          time: true
+        });
+    }
+    for (let i = 0, len = order.length / 5; i < len; i++) {
+      verifyRequestItemTarget(RequestsMenu,
+        RequestsMenu.getItemAtIndex(order[i + len * 2]),
+        "GET3", SORTING_SJS + "?index=3", {
+          fuzzyUrl: true,
+          status: 300,
+          statusText: "Meh",
+          type: "3",
+          fullMimeType: "text/3",
+          transferred: L10N.getFormatStrWithNumbers("networkMenu.sizeB", 29),
+          size: L10N.getFormatStrWithNumbers("networkMenu.sizeB", 29),
+          time: true
+        });
+    }
+    for (let i = 0, len = order.length / 5; i < len; i++) {
+      verifyRequestItemTarget(RequestsMenu,
+        RequestsMenu.getItemAtIndex(order[i + len * 3]),
+        "GET4", SORTING_SJS + "?index=4", {
+          fuzzyUrl: true,
+          status: 400,
+          statusText: "Meh",
+          type: "4",
+          fullMimeType: "text/4",
+          transferred: L10N.getFormatStrWithNumbers("networkMenu.sizeB", 39),
+          size: L10N.getFormatStrWithNumbers("networkMenu.sizeB", 39),
+          time: true
+        });
+    }
+    for (let i = 0, len = order.length / 5; i < len; i++) {
+      verifyRequestItemTarget(RequestsMenu,
+        RequestsMenu.getItemAtIndex(order[i + len * 4]),
+        "GET5", SORTING_SJS + "?index=5", {
+          fuzzyUrl: true,
+          status: 500,
+          statusText: "Meh",
+          type: "5",
+          fullMimeType: "text/5",
+          transferred: L10N.getFormatStrWithNumbers("networkMenu.sizeB", 49),
+          size: L10N.getFormatStrWithNumbers("networkMenu.sizeB", 49),
+          time: true
+        });
+    }
+  }
+});
--- a/devtools/client/netmonitor/test/browser_net_status-codes.js
+++ b/devtools/client/netmonitor/test/browser_net_status-codes.js
@@ -9,26 +9,21 @@
 
 add_task(function* () {
   let { L10N } = require("devtools/client/netmonitor/l10n");
 
   let { tab, monitor } = yield initNetMonitor(STATUS_CODES_URL);
 
   info("Starting test... ");
 
-  let { document, gStore, windowRequire } = monitor.panelWin;
-  let Actions = windowRequire("devtools/client/netmonitor/actions/index");
-  let {
-    getDisplayedRequests,
-    getSortedRequests,
-  } = windowRequire("devtools/client/netmonitor/selectors/index");
+  let { document, NetMonitorView } = monitor.panelWin;
+  let { RequestsMenu } = NetMonitorView;
+  let requestItems = [];
 
-  gStore.dispatch(Actions.batchEnable(false));
-
-  let requestItems = [];
+  RequestsMenu.lazyUpdate = false;
 
   const REQUEST_DATA = [
     {
       // request #0
       method: "GET",
       uri: STATUS_CODES_SJS + "?sts=100",
       details: {
         status: 101,
@@ -103,34 +98,28 @@ add_task(function* () {
   yield verifyRequests();
   yield testTab(0, testHeaders);
   yield testTab(2, testParams);
 
   return teardown(monitor);
 
   /**
    * A helper that verifies all requests show the correct information and caches
-   * request list items to requestItems array.
+   * RequestsMenu items to requestItems array.
    */
   function* verifyRequests() {
     info("Verifying requests contain correct information.");
     let index = 0;
     for (let request of REQUEST_DATA) {
-      let item = getSortedRequests(gStore.getState()).get(index);
+      let item = RequestsMenu.getItemAtIndex(index);
       requestItems[index] = item;
 
       info("Verifying request #" + index);
-      yield verifyRequestItemTarget(
-        document,
-        getDisplayedRequests(gStore.getState()),
-        item,
-        request.method,
-        request.uri,
-        request.details
-      );
+      yield verifyRequestItemTarget(RequestsMenu, item,
+        request.method, request.uri, request.details);
 
       index++;
     }
   }
 
   /**
    * A helper that opens a given tab of request details pane, selects and passes
    * all requests to the given test function.
@@ -172,18 +161,17 @@ add_task(function* () {
   }
 
   /**
    * A function that tests "Params" panel contains correct information.
    */
   function* testParams(data, index) {
     EventUtils.sendMouseEvent({ type: "mousedown" },
       document.querySelectorAll(".request-list-item")[index]);
-    EventUtils.sendMouseEvent({ type: "click" },
-      document.querySelector("#params-tab"));
+    document.querySelector("#params-tab").click();
 
     let panel = document.querySelector("#params-panel");
     let statusParamValue = data.uri.split("=").pop();
     let statusParamShownValue = "\"" + statusParamValue + "\"";
     let treeSections = panel.querySelectorAll(".tree-section");
 
     is(treeSections.length, 1,
       "There should be 1 param section displayed in this panel.");
--- a/devtools/client/netmonitor/test/browser_net_streaming-response.js
+++ b/devtools/client/netmonitor/test/browser_net_streaming-response.js
@@ -7,83 +7,72 @@
  * Tests if reponses from streaming content types (MPEG-DASH, HLS) are
  * displayed as XML or plain text
  */
 
 add_task(function* () {
   let { tab, monitor } = yield initNetMonitor(CUSTOM_GET_URL);
 
   info("Starting test... ");
-  let { document, gStore, windowRequire } = monitor.panelWin;
-  let Actions = windowRequire("devtools/client/netmonitor/actions/index");
-  let {
-    getDisplayedRequests,
-    getSortedRequests,
-  } = windowRequire("devtools/client/netmonitor/selectors/index");
-
-  gStore.dispatch(Actions.batchEnable(false));
+  let { panelWin } = monitor;
+  let { document, NetMonitorView } = panelWin;
+  let { RequestsMenu } = NetMonitorView;
 
   const REQUESTS = [
     [ "hls-m3u8", /^#EXTM3U/ ],
     [ "mpeg-dash", /^<\?xml/ ]
   ];
 
+  RequestsMenu.lazyUpdate = false;
+
   let wait = waitForNetworkEvents(monitor, REQUESTS.length);
   for (let [fmt] of REQUESTS) {
     let url = CONTENT_TYPE_SJS + "?fmt=" + fmt;
     yield ContentTask.spawn(tab.linkedBrowser, { url }, function* (args) {
       content.wrappedJSObject.performRequests(1, args.url);
     });
   }
   yield wait;
 
   REQUESTS.forEach(([ fmt ], i) => {
-    verifyRequestItemTarget(
-      document,
-      getDisplayedRequests(gStore.getState()),
-      getSortedRequests(gStore.getState()).get(i),
-      "GET",
-      CONTENT_TYPE_SJS + "?fmt=" + fmt,
-      {
+    verifyRequestItemTarget(RequestsMenu, RequestsMenu.getItemAtIndex(i),
+      "GET", CONTENT_TYPE_SJS + "?fmt=" + fmt, {
         status: 200,
         statusText: "OK"
       });
   });
 
   wait = waitForDOM(document, "#response-panel");
-  EventUtils.sendMouseEvent({ type: "click" },
+  EventUtils.sendMouseEvent({ type: "mousedown" },
     document.querySelector(".network-details-panel-toggle"));
-  EventUtils.sendMouseEvent({ type: "click" },
-    document.querySelector("#response-tab"));
+  document.querySelector("#response-tab").click();
   yield wait;
 
-  gStore.dispatch(Actions.selectRequest(null));
+  RequestsMenu.selectedIndex = -1;
 
   yield selectIndexAndWaitForEditor(0);
   // the hls-m3u8 part
   testEditorContent(REQUESTS[0]);
 
   yield selectIndexAndWaitForEditor(1);
   // the mpeg-dash part
   testEditorContent(REQUESTS[1]);
 
   return teardown(monitor);
 
   function* selectIndexAndWaitForEditor(index) {
     let editor = document.querySelector("#response-panel .editor-mount iframe");
     if (!editor) {
       let waitDOM = waitForDOM(document, "#response-panel .editor-mount iframe");
-      EventUtils.sendMouseEvent({ type: "mousedown" },
-        document.querySelectorAll(".request-list-item")[index]);
+      RequestsMenu.selectedIndex = index;
       document.querySelector("#response-tab").click();
       [editor] = yield waitDOM;
       yield once(editor, "DOMContentLoaded");
     } else {
-      EventUtils.sendMouseEvent({ type: "mousedown" },
-        document.querySelectorAll(".request-list-item")[index]);
+      RequestsMenu.selectedIndex = index;
     }
 
     yield waitForDOM(editor.contentDocument, ".CodeMirror-code");
   }
 
   function testEditorContent([ fmt, textRe ]) {
     let editor = document.querySelector("#response-panel .editor-mount iframe");
     let text = editor.contentDocument
--- a/devtools/client/netmonitor/test/browser_net_throttle.js
+++ b/devtools/client/netmonitor/test/browser_net_throttle.js
@@ -9,23 +9,17 @@ add_task(function* () {
   yield throttleTest(true);
   yield throttleTest(false);
 });
 
 function* throttleTest(actuallyThrottle) {
   requestLongerTimeout(2);
 
   let { monitor } = yield initNetMonitor(SIMPLE_URL);
-  let { document, gStore, windowRequire, NetMonitorController } = monitor.panelWin;
-  let Actions = windowRequire("devtools/client/netmonitor/actions/index");
-  let { ACTIVITY_TYPE } = windowRequire("devtools/client/netmonitor/constants");
-  let { EVENTS } = windowRequire("devtools/client/netmonitor/events");
-  let {
-    getSortedRequests,
-  } = windowRequire("devtools/client/netmonitor/selectors/index");
+  const {ACTIVITY_TYPE, EVENTS, NetMonitorController, NetMonitorView} = monitor.panelWin;
 
   info("Starting test... (actuallyThrottle = " + actuallyThrottle + ")");
 
   // When throttling, must be smaller than the length of the content
   // of SIMPLE_URL in bytes.
   const size = actuallyThrottle ? 200 : 0;
 
   const request = {
@@ -46,17 +40,17 @@ function* throttleTest(actuallyThrottle)
     deferred.resolve(response);
   });
   yield deferred.promise;
 
   let eventPromise = monitor.panelWin.once(EVENTS.RECEIVED_EVENT_TIMINGS);
   yield NetMonitorController.triggerActivity(ACTIVITY_TYPE.RELOAD.WITH_CACHE_DISABLED);
   yield eventPromise;
 
-  let requestItem = getSortedRequests(gStore.getState()).get(0);
+  let requestItem = NetMonitorView.RequestsMenu.getItemAtIndex(0);
   const reportedOneSecond = requestItem.eventTimings.timings.receive > 1000;
   if (actuallyThrottle) {
     ok(reportedOneSecond, "download reported as taking more than one second");
   } else {
     ok(!reportedOneSecond, "download reported as taking less than one second");
   }
 
   yield teardown(monitor);
--- a/devtools/client/netmonitor/test/browser_net_timing-division.js
+++ b/devtools/client/netmonitor/test/browser_net_timing-division.js
@@ -6,48 +6,44 @@
 /**
  * Tests if timing intervals are divided againts seconds when appropriate.
  */
 
 add_task(function* () {
   let { tab, monitor } = yield initNetMonitor(CUSTOM_GET_URL);
   info("Starting test... ");
 
-  let { document, gStore, windowRequire } = monitor.panelWin;
-  let Actions = windowRequire("devtools/client/netmonitor/actions/index");
-  let { getSortedRequests } = windowRequire("devtools/client/netmonitor/selectors/index");
+  let { $all, NetMonitorView } = monitor.panelWin;
+  let { RequestsMenu } = NetMonitorView;
 
-  gStore.dispatch(Actions.batchEnable(false));
+  RequestsMenu.lazyUpdate = false;
 
   let wait = waitForNetworkEvents(monitor, 2);
   // Timeout needed for having enough divisions on the time scale.
   yield ContentTask.spawn(tab.linkedBrowser, {}, function* () {
     content.wrappedJSObject.performRequests(2, null, 3000);
   });
   yield wait;
 
-  let milDivs = document.querySelectorAll(
-    ".requests-menu-timings-division[data-division-scale=millisecond]");
-  let secDivs = document.querySelectorAll(
-    ".requests-menu-timings-division[data-division-scale=second]");
-  let minDivs = document.querySelectorAll(
-    ".requests-menu-timings-division[data-division-scale=minute]");
+  let milDivs = $all(".requests-menu-timings-division[data-division-scale=millisecond]");
+  let secDivs = $all(".requests-menu-timings-division[data-division-scale=second]");
+  let minDivs = $all(".requests-menu-timings-division[data-division-scale=minute]");
 
   info("Number of millisecond divisions: " + milDivs.length);
   info("Number of second divisions: " + secDivs.length);
   info("Number of minute divisions: " + minDivs.length);
 
   milDivs.forEach(div => info(`Millisecond division: ${div.textContent}`));
   secDivs.forEach(div => info(`Second division: ${div.textContent}`));
   minDivs.forEach(div => info(`Minute division: ${div.textContent}`));
 
-  is(gStore.getState().requests.requests.size, 2, "There should be only two requests made.");
+  is(RequestsMenu.itemCount, 2, "There should be only two requests made.");
 
-  let firstRequest = getSortedRequests(gStore.getState()).get(0);
-  let lastRequest = getSortedRequests(gStore.getState()).get(1);
+  let firstRequest = RequestsMenu.getItemAtIndex(0);
+  let lastRequest = RequestsMenu.getItemAtIndex(1);
 
   info("First request happened at: " +
     firstRequest.responseHeaders.headers.find(e => e.name == "Date").value);
   info("Last request happened at: " +
     lastRequest.responseHeaders.headers.find(e => e.name == "Date").value);
 
   ok(secDivs.length,
     "There should be at least one division on the seconds time scale.");
--- a/devtools/client/netmonitor/test/browser_net_truncate.js
+++ b/devtools/client/netmonitor/test/browser_net_truncate.js
@@ -14,40 +14,31 @@ function test() {
   const URL = EXAMPLE_URL + "sjs_truncate-test-server.sjs?limit=" + RESPONSE_BODY_LIMIT;
 
   // Another slow test on Linux debug.
   requestLongerTimeout(2);
 
   initNetMonitor(URL).then(({ tab, monitor }) => {
     info("Starting test... ");
 
-    let { document, gStore, windowRequire } = monitor.panelWin;
-    let Actions = windowRequire("devtools/client/netmonitor/actions/index");
-    let { EVENTS } = windowRequire("devtools/client/netmonitor/events");
-    let {
-      getDisplayedRequests,
-      getSortedRequests,
-    } = windowRequire("devtools/client/netmonitor/selectors/index");
+    let { NetMonitorView } = monitor.panelWin;
+    let { RequestsMenu } = NetMonitorView;
 
-    gStore.dispatch(Actions.batchEnable(false));
+    RequestsMenu.lazyUpdate = false;
 
     waitForNetworkEvents(monitor, 1)
       .then(() => teardown(monitor))
       .then(finish);
 
-    monitor.panelWin.once(EVENTS.RECEIVED_RESPONSE_CONTENT, () => {
-      verifyRequestItemTarget(
-        document,
-        getDisplayedRequests(gStore.getState()),
-        getSortedRequests(gStore.getState()).get(0),
-        "GET", URL,
-        {
-          type: "plain",
-          fullMimeType: "text/plain; charset=utf-8",
-          transferred: L10N.getFormatStrWithNumbers("networkMenu.sizeMB", 2),
-          size: L10N.getFormatStrWithNumbers("networkMenu.sizeMB", 2),
-        }
-      );
+    monitor.panelWin.once(monitor.panelWin.EVENTS.RECEIVED_RESPONSE_CONTENT, () => {
+      let requestItem = RequestsMenu.getItemAtIndex(0);
+
+      verifyRequestItemTarget(RequestsMenu, requestItem, "GET", URL, {
+        type: "plain",
+        fullMimeType: "text/plain; charset=utf-8",
+        transferred: L10N.getFormatStrWithNumbers("networkMenu.sizeMB", 2),
+        size: L10N.getFormatStrWithNumbers("networkMenu.sizeMB", 2),
+      });
     });
 
     tab.linkedBrowser.reload();
   });
 }
--- a/devtools/client/netmonitor/test/head.js
+++ b/devtools/client/netmonitor/test/head.js
@@ -5,17 +5,16 @@
 
 "use strict";
 
 // shared-head.js handles imports, constants, and utility functions
 Services.scriptloader.loadSubScript(
   "chrome://mochitests/content/browser/devtools/client/framework/test/shared-head.js",
   this);
 
-const { EVENTS } = require("devtools/client/netmonitor/events");
 var { Toolbox } = require("devtools/client/framework/toolbox");
 const {
   decodeUnicodeUrl,
   getUrlBaseName,
   getUrlQuery,
   getUrlHost,
 } = require("devtools/client/netmonitor/request-utils");
 
@@ -172,20 +171,24 @@ function teardown(monitor) {
     let onDestroyed = monitor.once("destroyed");
     yield removeTab(tab);
     yield onDestroyed;
   });
 }
 
 function waitForNetworkEvents(aMonitor, aGetRequests, aPostRequests = 0) {
   let deferred = promise.defer();
+
   let panel = aMonitor.panelWin;
+  let events = panel.EVENTS;
+
   let progress = {};
   let genericEvents = 0;
   let postEvents = 0;
+
   let awaitedEventsToListeners = [
     ["UPDATING_REQUEST_HEADERS", onGenericEvent],
     ["RECEIVED_REQUEST_HEADERS", onGenericEvent],
     ["UPDATING_REQUEST_COOKIES", onGenericEvent],
     ["RECEIVED_REQUEST_COOKIES", onGenericEvent],
     ["UPDATING_REQUEST_POST_DATA", onPostEvent],
     ["RECEIVED_REQUEST_POST_DATA", onPostEvent],
     ["UPDATING_RESPONSE_HEADERS", onGenericEvent],
@@ -202,17 +205,17 @@ function waitForNetworkEvents(aMonitor, 
   function initProgressForURL(url) {
     if (progress[url]) return;
     progress[url] = {};
     awaitedEventsToListeners.forEach(([e]) => progress[url][e] = 0);
   }
 
   function updateProgressForURL(url, event) {
     initProgressForURL(url);
-    progress[url][Object.keys(EVENTS).find(e => EVENTS[e] == event)] = 1;
+    progress[url][Object.keys(events).find(e => events[e] == event)] = 1;
   }
 
   function onGenericEvent(event, actor) {
     genericEvents++;
     maybeResolve(event, actor);
   }
 
   function onPostEvent(event, actor) {
@@ -235,47 +238,49 @@ function waitForNetworkEvents(aMonitor, 
     // info("> Current state: " + JSON.stringify(progress, null, 2));
 
     // There are 15 updates which need to be fired for a request to be
     // considered finished. The "requestPostData" packet isn't fired for
     // non-POST requests.
     if (genericEvents >= (aGetRequests + aPostRequests) * 13 &&
         postEvents >= aPostRequests * 2) {
 
-      awaitedEventsToListeners.forEach(([e, l]) => panel.off(EVENTS[e], l));
+      awaitedEventsToListeners.forEach(([e, l]) => panel.off(events[e], l));
       executeSoon(deferred.resolve);
     }
   }
 
-  awaitedEventsToListeners.forEach(([e, l]) => panel.on(EVENTS[e], l));
+  awaitedEventsToListeners.forEach(([e, l]) => panel.on(events[e], l));
   return deferred.promise;
 }
 
 /**
  * Convert a store record (model) to the rendered element. Tests that need to use
  * this should be rewritten - test the rendered markup at unit level, integration
  * mochitest should check only the store state.
  */
 function getItemTarget(requestList, requestItem) {
   const items = requestList.mountPoint.querySelectorAll(".request-list-item");
   return [...items].find(el => el.dataset.id == requestItem.id);
 }
 
-function verifyRequestItemTarget(document, requestList, requestItem, aMethod,
-                                 aUrl, aData = {}) {
+function verifyRequestItemTarget(requestList, requestItem, aMethod, aUrl, aData = {}) {
   info("> Verifying: " + aMethod + " " + aUrl + " " + aData.toSource());
+  // This bloats log sizes significantly in automation (bug 992485)
+  // info("> Request: " + requestItem.toSource());
 
-  let visibleIndex = requestList.indexOf(requestItem);
+  let visibleIndex = requestList.visibleItems.indexOf(requestItem);
 
   info("Visible index of item: " + visibleIndex);
 
   let { fuzzyUrl, status, statusText, cause, type, fullMimeType,
         transferred, size, time, displayedStatus } = aData;
 
-  let target = document.querySelectorAll(".request-list-item")[visibleIndex];
+  let target = getItemTarget(requestList, requestItem);
+
   let unicodeUrl = decodeUnicodeUrl(aUrl);
   let name = getUrlBaseName(aUrl);
   let query = getUrlQuery(aUrl);
   let hostPort = getUrlHost(aUrl);
   let remoteAddress = requestItem.remoteAddress;
 
   if (fuzzyUrl) {
     ok(requestItem.method.startsWith(aMethod), "The attached method is correct.");
@@ -354,30 +359,30 @@ function verifyRequestItemTarget(documen
     let value = target.querySelector(".requests-menu-timings-total").textContent;
     let tooltip = target.querySelector(".requests-menu-timings-total").getAttribute("title");
     info("Displayed time: " + value);
     info("Tooltip time: " + tooltip);
     ok(~~(value.match(/[0-9]+/)) >= 0, "The displayed time is correct.");
     ok(~~(tooltip.match(/[0-9]+/)) >= 0, "The tooltip time is correct.");
   }
 
-  if (visibleIndex !== -1) {
-    if (visibleIndex % 2 === 0) {
+  if (visibleIndex != -1) {
+    if (visibleIndex % 2 == 0) {
       ok(target.classList.contains("even"), "Item should have 'even' class.");
       ok(!target.classList.contains("odd"), "Item shouldn't have 'odd' class.");
     } else {
       ok(!target.classList.contains("even"), "Item shouldn't have 'even' class.");
       ok(target.classList.contains("odd"), "Item should have 'odd' class.");
     }
   }
 }
 
 /**
  * Helper function for waiting for an event to fire before resolving a promise.
- * Example: waitFor(aMonitor.panelWin, EVENT_NAME);
+ * Example: waitFor(aMonitor.panelWin, aMonitor.panelWin.EVENTS.TAB_UPDATED);
  *
  * @param object subject
  *        The event emitter object that is being listened to.
  * @param string eventName
  *        The name of the event to listen to.
  * @return object
  *        Returns a promise that resolves upon firing of the event.
  */
@@ -500,8 +505,28 @@ function waitForContentMessage(name) {
 
   let def = promise.defer();
   mm.addMessageListener(name, function onMessage(msg) {
     mm.removeMessageListener(name, onMessage);
     def.resolve(msg);
   });
   return def.promise;
 }
+
+/**
+ * Open the requestMenu menu and return all of it's items in a flat array
+ * @param {netmonitorPanel} netmonitor
+ * @param {Event} event mouse event with screenX and screenX coordinates
+ * @return An array of MenuItems
+ */
+function openContextMenuAndGetAllItems(netmonitor, event) {
+  let menu = netmonitor.RequestsMenu.contextMenu.open(event);
+
+  // Flatten all menu items into a single array to make searching through it easier
+  let allItems = [].concat.apply([], menu.items.map(function addItem(item) {
+    if (item.submenu) {
+      return addItem(item.submenu.items);
+    }
+    return item;
+  }));
+
+  return allItems;
+}
--- a/devtools/client/styleeditor/test/browser_styleeditor_fetch-from-cache.js
+++ b/devtools/client/styleeditor/test/browser_styleeditor_fetch-from-cache.js
@@ -8,35 +8,32 @@
 
 const TEST_URL = TEST_BASE_HTTP + "doc_uncached.html";
 
 add_task(function* () {
   info("Opening netmonitor");
   let tab = yield addTab("about:blank");
   let target = TargetFactory.forTab(tab);
   let toolbox = yield gDevTools.showToolbox(target, "netmonitor");
-  let monitor = toolbox.getPanel("netmonitor");
-  let { gStore, windowRequire } = monitor.panelWin;
-  let Actions = windowRequire("devtools/client/netmonitor/actions/index");
-  let { getSortedRequests } = windowRequire("devtools/client/netmonitor/selectors/index");
-
-  gStore.dispatch(Actions.batchEnable(false));
+  let netmonitor = toolbox.getPanel("netmonitor");
+  let { RequestsMenu } = netmonitor.panelWin.NetMonitorView;
+  RequestsMenu.lazyUpdate = false;
 
   info("Navigating to test page");
   yield navigateTo(TEST_URL);
 
   info("Opening Style Editor");
   let styleeditor = yield toolbox.selectTool("styleeditor");
 
   info("Waiting for the source to be loaded.");
   yield styleeditor.UI.editors[0].getSourceEditor();
 
   info("Checking Netmonitor contents.");
   let items = [];
-  for (let item of getSortedRequests(gStore.getState())) {
+  for (let item of RequestsMenu.items) {
     if (item.url.endsWith("doc_uncached.css")) {
       items.push(item);
     }
   }
 
   is(items.length, 2,
      "Got two requests for doc_uncached.css after Style Editor was loaded.");
   ok(items[1].fromCache,
--- a/devtools/client/themes/netmonitor.css
+++ b/devtools/client/themes/netmonitor.css
@@ -78,18 +78,19 @@
   --timing-wait-color: rgba(94, 136, 176, 0.8); /* blue grey */
   --timing-receive-color: rgba(112, 191, 83, 0.8); /* green */
 
   --sort-ascending-image: url(chrome://devtools/skin/images/firebug/arrow-up.svg);
   --sort-descending-image: url(chrome://devtools/skin/images/firebug/arrow-down.svg);
 }
 
 .request-list-container {
-  display: flex;
-  flex-direction: column;
+  display: -moz-box;
+  -moz-box-orient: vertical;
+  -moz-box-flex: 1;
 }
 
 .request-list-empty-notice {
   margin: 0;
   padding: 12px;
   font-size: 120%;
 }
 
@@ -129,26 +130,24 @@
   flex-wrap: nowrap;
 }
 
 .theme-firebug #requests-menu-toolbar {
   height: 19px !important;
 }
 
 .requests-menu-contents {
-  display: flex;
-  flex-direction: column;
+  display: -moz-box;
+  -moz-box-orient: vertical;
+  -moz-box-flex: 1;
   overflow-x: hidden;
   overflow-y: auto;
 
   --timings-scale: 1;
   --timings-rev-scale: 1;
-
-  /* Devtools panel view height - tabbar height - toolbar height */
-  height: calc(100vh - 48px);
 }
 
 .requests-menu-subitem {
   display: flex;
   flex: none;
   box-sizing: border-box;
   align-items: center;
   padding: 3px;
@@ -1327,17 +1326,13 @@
   height: 100%;
   overflow: hidden;
 }
 
 #splitter-adjustable-box[hidden=true] {
   display: none;
 }
 
-#react-request-list-hook {
-  -moz-box-flex: 1;
-}
-
 #primed-cache-chart,
 #empty-cache-chart {
   display: -moz-box;
   -moz-box-flex: 1;
 }
--- a/devtools/client/webconsole/test/browser_netmonitor_shows_reqs_in_webconsole.js
+++ b/devtools/client/webconsole/test/browser_netmonitor_shows_reqs_in_webconsole.js
@@ -57,21 +57,17 @@ function loadDocument(browser) {
   }, {capture: true, once: true});
   BrowserTestUtils.loadURI(gBrowser.selectedBrowser, TEST_PATH);
 
   return deferred.promise;
 }
 
 function testNetmonitor(toolbox) {
   let monitor = toolbox.getCurrentPanel();
-
-  let { gStore, windowRequire } = monitor.panelWin;
-  let Actions = windowRequire("devtools/client/netmonitor/actions/index");
-  let { getSortedRequests } = windowRequire("devtools/client/netmonitor/selectors/index");
+  let { RequestsMenu } = monitor.panelWin.NetMonitorView;
+  RequestsMenu.lazyUpdate = false;
 
-  gStore.dispatch(Actions.batchEnable(false));
+  is(RequestsMenu.itemCount, 1, "Network request appears in the network panel");
 
-  is(gStore.getState().requests.requests.size, 1, "Network request appears in the network panel");
-
-  let item = getSortedRequests(gStore.getState()).get(0);
+  let item = RequestsMenu.getItemAtIndex(0);
   is(item.method, "GET", "The attached method is correct.");
   is(item.url, TEST_PATH, "The attached url is correct.");
 }
--- a/devtools/client/webconsole/test/browser_webconsole_netlogging_panel.js
+++ b/devtools/client/webconsole/test/browser_webconsole_netlogging_panel.js
@@ -17,19 +17,14 @@ add_task(function* () {
 
   const hud = yield loadPageAndGetHud(TEST_NETWORK_REQUEST_URI);
   let request = yield finishedRequest;
 
   yield hud.ui.openNetworkPanel(request.actor);
   let toolbox = gDevTools.getToolbox(hud.target);
   is(toolbox.currentToolId, "netmonitor", "Network panel was opened");
   let panel = toolbox.getCurrentPanel();
-
-  let { gStore, windowRequire } = panel.panelWin;
-  let Actions = windowRequire("devtools/client/netmonitor/actions/index");
-  let { getSelectedRequest } = windowRequire("devtools/client/netmonitor/selectors/index");
-
-  let selected = getSelectedRequest(gStore.getState());
+  let selected = panel.panelWin.NetMonitorView.RequestsMenu.selectedItem;
   is(selected.method, request.request.method,
      "The correct request is selected");
   is(selected.url, request.request.url,
      "The correct request is definitely selected");
 });
--- a/devtools/client/webconsole/test/browser_webconsole_netlogging_reset_filter.js
+++ b/devtools/client/webconsole/test/browser_webconsole_netlogging_reset_filter.js
@@ -11,16 +11,18 @@
 const TEST_FILE_URI =
   "http://example.com/browser/devtools/client/webconsole/test/" +
   "test-network.html";
 const TEST_URI = "data:text/html;charset=utf8,<p>test file URI";
 
 var hud;
 
 add_task(function* () {
+  let Actions = require("devtools/client/netmonitor/actions/index");
+
   let requests = [];
   let { browser } = yield loadTab(TEST_URI);
 
   yield pushPrefEnv();
   hud = yield openConsole();
   hud.jsterm.clearOutput();
 
   HUDService.lastFinishedRequest.callback = request => requests.push(request);
@@ -33,34 +35,30 @@ add_task(function* () {
   let htmlRequest = requests.find(e => e.request.url.endsWith("html"));
   ok(htmlRequest, "htmlRequest was a html");
 
   yield hud.ui.openNetworkPanel(htmlRequest.actor);
   let toolbox = gDevTools.getToolbox(hud.target);
   is(toolbox.currentToolId, "netmonitor", "Network panel was opened");
 
   let panel = toolbox.getCurrentPanel();
-  let { gStore, windowRequire } = panel.panelWin;
-  let Actions = windowRequire("devtools/client/netmonitor/actions/index");
-  let { getSelectedRequest } = windowRequire("devtools/client/netmonitor/selectors/index");
-
-  let selected = getSelectedRequest(gStore.getState());
+  let selected = panel.panelWin.NetMonitorView.RequestsMenu.selectedItem;
   is(selected.method, htmlRequest.request.method,
      "The correct request is selected");
   is(selected.url, htmlRequest.request.url,
      "The correct request is definitely selected");
 
   // Filter out the HTML request.
-  gStore.dispatch(Actions.toggleRequestFilterType("js"));
+  panel.panelWin.gStore.dispatch(Actions.toggleRequestFilterType("js"));
 
   yield toolbox.selectTool("webconsole");
   is(toolbox.currentToolId, "webconsole", "Web console was selected");
   yield hud.ui.openNetworkPanel(htmlRequest.actor);
 
-  selected = getSelectedRequest(gStore.getState());
+  panel.panelWin.NetMonitorView.RequestsMenu.selectedItem;
   is(selected.method, htmlRequest.request.method,
      "The correct request is selected");
   is(selected.url, htmlRequest.request.url,
      "The correct request is definitely selected");
 
   // All tests are done. Shutdown.
   HUDService.lastFinishedRequest.callback = null;
   htmlRequest = browser = requests = hud = null;
--- a/devtools/client/webconsole/test/browser_webconsole_shows_reqs_in_netmonitor.js
+++ b/devtools/client/webconsole/test/browser_webconsole_shows_reqs_in_netmonitor.js
@@ -57,21 +57,16 @@ function loadDocument(browser) {
   }, {capture: true, once: true});
   BrowserTestUtils.loadURI(gBrowser.selectedBrowser, TEST_PATH);
 
   return deferred.promise;
 }
 
 function testNetmonitor(toolbox) {
   let monitor = toolbox.getCurrentPanel();
-
-  let { gStore, windowRequire } = monitor.panelWin;
-  let Actions = windowRequire("devtools/client/netmonitor/actions/index");
-  let { getSortedRequests } = windowRequire("devtools/client/netmonitor/selectors/index");
+  let { RequestsMenu } = monitor.panelWin.NetMonitorView;
+  RequestsMenu.lazyUpdate = false;
+  is(RequestsMenu.itemCount, 1, "Network request appears in the network panel");
 
-  gStore.dispatch(Actions.batchEnable(false));
-
-  is(gStore.getState().requests.requests.size, 1, "Network request appears in the network panel");
-
-  let item = getSortedRequests(gStore.getState()).get(0);
+  let item = RequestsMenu.getItemAtIndex(0);
   is(item.method, "GET", "The request method is correct.");
   is(item.url, TEST_PATH, "The request url is correct.");
 }