diff --git a/__tests__/src/actions/annotation.test.js b/__tests__/src/actions/annotation.test.js
index 7b0be9bf5d98d4ad5d705b9ce7540cb6223430a1..4039f02d48ef1050c7bbf30430e7b793791f6d0f 100644
--- a/__tests__/src/actions/annotation.test.js
+++ b/__tests__/src/actions/annotation.test.js
@@ -31,23 +31,6 @@ describe('annotation actions', () => {
       expect(actions.receiveAnnotation(targetId, annotationId, json)).toEqual(expectedAction);
     });
   });
-  describe('fetchAnnotation', () => {
-    describe('success response', () => {
-      beforeEach(() => {
-        fetch.mockResponseOnce(JSON.stringify({ data: '12345' })); // eslint-disable-line no-undef
-      });
-      it('dispatches the REQUEST_ANNOTATION action', () => {
-        expect(actions.fetchAnnotation(
-          'https://iiif.harvardartmuseums.org/manifests/object/299843/canvas/canvas-47174896',
-          'https://iiif.harvardartmuseums.org/manifests/object/299843/list/47174896',
-        )).toEqual({
-          annotationId: 'https://iiif.harvardartmuseums.org/manifests/object/299843/list/47174896',
-          targetId: 'https://iiif.harvardartmuseums.org/manifests/object/299843/canvas/canvas-47174896',
-          type: 'mirador/REQUEST_ANNOTATION',
-        });
-      });
-    });
-  });
 
   it('handles the selectAnnotation action', () => {
     const windowId = 'wId1';
diff --git a/__tests__/src/components/WindowViewer.test.js b/__tests__/src/components/WindowViewer.test.js
index ba266aaa84c9b219a1e41d06ed427a6cef912139..373ca1d41366e07eef2a0b194a9d89f0056bff55 100644
--- a/__tests__/src/components/WindowViewer.test.js
+++ b/__tests__/src/components/WindowViewer.test.js
@@ -1,26 +1,13 @@
 import React from 'react';
 import { shallow } from 'enzyme';
-import { Utils } from 'manifesto.js/dist-esmodule/Utils';
 import { WindowViewer } from '../../../src/components/WindowViewer';
 import OSDViewer from '../../../src/containers/OpenSeadragonViewer';
 import WindowCanvasNavigationControls from '../../../src/containers/WindowCanvasNavigationControls';
-import fixture from '../../fixtures/version-2/019.json';
-import emptyCanvasFixture from '../../fixtures/version-2/emptyCanvas.json';
-import otherContentFixture from '../../fixtures/version-2/299843.json';
-
-let currentCanvases = Utils.parseManifest(fixture).getSequences()[0].getCanvases();
 
 /** create wrapper */
 function createWrapper(props) {
   return shallow(
     <WindowViewer
-      canvasIndex={0}
-      canvasLabel="label"
-      infoResponses={{}}
-      fetchInfoResponse={() => {}}
-      fetchAnnotation={() => {}}
-      currentCanvases={[currentCanvases[1]]}
-      view="single"
       windowId="xyz"
       {...props}
     />,
@@ -37,121 +24,4 @@ describe('WindowViewer', () => {
       </OSDViewer>,
     )).toBe(true);
   });
-  describe('currentInfoResponses', () => {
-    describe('returns only available infoResponses', () => {
-      it('isFetching is false', () => {
-        wrapper = createWrapper(
-          {
-            currentCanvasId: 1,
-            infoResponses: {
-              'https://stacks.stanford.edu/image/iiif/fr426cg9537%2FSC1094_s3_b14_f17_Cats_1976_0005': {
-                isFetching: false,
-              },
-              'https://stacks.stanford.edu/image/iiif/rz176rt6531%2FPC0170_s3_Tree_Calendar_20081101_152516_0410': {
-                isFetching: true,
-              },
-            },
-            view: 'book',
-          },
-        );
-        expect(wrapper.instance().currentInfoResponses().length).toEqual(1);
-      });
-      it('infoResponse is undefined', () => {
-        wrapper = createWrapper(
-          {
-            currentCanvasId: 1,
-            infoResponses: {
-              foo: {
-                isFetching: false,
-              },
-              'https://stacks.stanford.edu/image/iiif/fr426cg9537%2FSC1094_s3_b14_f17_Cats_1976_0005': {
-                isFetching: false,
-              },
-            },
-            view: 'book',
-          },
-        );
-        expect(wrapper.instance().currentInfoResponses().length).toEqual(1);
-      });
-      it('error is not present', () => {
-        wrapper = createWrapper(
-          {
-            currentCanvasId: 1,
-            infoResponses: {
-              'https://stacks.stanford.edu/image/iiif/fr426cg9537%2FSC1094_s3_b14_f17_Cats_1976_0005': {
-                isFetching: false,
-              },
-              'https://stacks.stanford.edu/image/iiif/rz176rt6531%2FPC0170_s3_Tree_Calendar_20081101_152516_0410': {
-                error: 'yikes!',
-                isFetching: false,
-              },
-            },
-            view: 'book',
-          },
-        );
-        expect(wrapper.instance().currentInfoResponses().length).toEqual(1);
-      });
-    });
-  });
-
-  describe('componentDidMount', () => {
-    it('does not call fetchInfoResponse for a canvas that has no images', () => {
-      const mockFnCanvas0 = jest.fn();
-      const mockFnCanvas2 = jest.fn();
-      const canvases = Utils.parseManifest(emptyCanvasFixture).getSequences()[0].getCanvases();
-
-      currentCanvases = [canvases[0]];
-
-      wrapper = createWrapper(
-        {
-          currentCanvases,
-          currentCanvasId: 0,
-          fetchInfoResponse: mockFnCanvas0,
-          view: 'single',
-        },
-      );
-      expect(mockFnCanvas0).toHaveBeenCalledTimes(1);
-
-      currentCanvases = [canvases[2]];
-      wrapper = createWrapper(
-        {
-          currentCanvases,
-          currentCanvasId: 2,
-          fetchInfoResponse: mockFnCanvas2,
-          view: 'single',
-        },
-      );
-      expect(mockFnCanvas2).toHaveBeenCalledTimes(0);
-    });
-    it('calls fetchAnnotation when otherContent is present', () => {
-      const mockFnAnno = jest.fn();
-      const canvases = Utils.parseManifest(otherContentFixture).getSequences()[0].getCanvases();
-      currentCanvases = [canvases[0]];
-
-      wrapper = createWrapper(
-        { currentCanvases, fetchAnnotation: mockFnAnno },
-      );
-      expect(mockFnAnno).toHaveBeenCalledTimes(1);
-    });
-  });
-
-  describe('componentDidUpdate', () => {
-    it('does not call fetchInfoResponse for a canvas that has no images', () => {
-      const mockFn = jest.fn();
-      const canvases = Utils.parseManifest(emptyCanvasFixture).getSequences()[0].getCanvases();
-      currentCanvases = [canvases[2]];
-      wrapper = createWrapper(
-        {
-          currentCanvases,
-          currentCanvasId: 2,
-          fetchInfoResponse: mockFn,
-          view: 'single',
-        },
-      );
-
-      wrapper.setProps({ currentCanvases: [canvases[3]], currentCanvasId: 3, view: 'single' });
-
-      expect(mockFn).toHaveBeenCalledTimes(0);
-    });
-  });
 });
diff --git a/__tests__/src/lib/MiradorCanvas.test.js b/__tests__/src/lib/MiradorCanvas.test.js
index d503b9a1d2c8dbe2e3ee9a16e71a73a3c2a9a41f..e501add183df05b52033abeca3a6e3017daa4e4a 100644
--- a/__tests__/src/lib/MiradorCanvas.test.js
+++ b/__tests__/src/lib/MiradorCanvas.test.js
@@ -49,27 +49,7 @@ describe('MiradorCanvas', () => {
       });
     });
   });
-  describe('processAnnotations', () => {
-    describe('v2', () => {
-      it('fetches annotations for each annotationList', () => {
-        const otherContentInstance = new MiradorCanvas(
-          Utils.parseManifest(otherContentFixture).getSequences()[0].getCanvases()[0],
-        );
-        const fetchMock = jest.fn();
-        otherContentInstance.processAnnotations(fetchMock);
-        expect(fetchMock).toHaveBeenCalledTimes(1);
-      });
-    });
-    describe('v3', () => {
-      it('fetches annotations for external items and receives annotations for items that are embedded', () => {
-        const receiveMock = jest.fn();
-        const fetchMock = jest.fn();
-        v3Instance.processAnnotations(fetchMock, receiveMock);
-        expect(receiveMock).toHaveBeenCalledTimes(1);
-        expect(fetchMock).toHaveBeenCalledTimes(2);
-      });
-    });
-  });
+
   describe('aspectRatio', () => {
     it('calculates a width / height aspectRatio', () => {
       expect(instance.aspectRatio).toBeCloseTo(0.667);
diff --git a/__tests__/src/lib/MiradorViewer.test.js b/__tests__/src/lib/MiradorViewer.test.js
index aa207f7bde1fe0c5ba8b9ff7fb34f2f0b0db0746..87aa5094315b8482e3155f3f690f7bf7aa449c4b 100644
--- a/__tests__/src/lib/MiradorViewer.test.js
+++ b/__tests__/src/lib/MiradorViewer.test.js
@@ -5,17 +5,6 @@ jest.unmock('react-i18next');
 jest.mock('react-dom');
 jest.mock('isomorphic-unfetch', () => jest.fn(() => Promise.resolve({ json: () => ({}) })));
 
-jest.mock('../../../src/state/selectors', () => ({
-  getCanvasGrouping: () => [],
-  getCompanionWindowIdsForPosition: () => ['cwid'],
-  getManifestoInstance: () => {},
-  getManifests: () => (
-    { 'https://iiif.harvardartmuseums.org/manifests/object/299843': { isFetching: true } }
-  ),
-  getManifestSearchService: () => ({ id: 'http://example.com/search' }),
-  getSearchForWindow: () => {},
-}));
-
 describe('MiradorViewer', () => {
   let instance;
   beforeAll(() => {
@@ -61,7 +50,7 @@ describe('MiradorViewer', () => {
       expect(windows[windowIds[0]].layoutOrder).toBe(0);
       expect(windows[windowIds[1]].layoutOrder).toBe(1);
       expect(windows[windowIds[0]].thumbnailNavigationPosition).toBe('far-bottom');
-      expect(windows[windowIds[1]].thumbnailNavigationPosition).toBe('off');
+      expect(windows[windowIds[1]].thumbnailNavigationPosition).toBe(undefined);
       expect(windows[windowIds[0]].view).toBe(undefined);
       expect(windows[windowIds[1]].view).toBe('book');
 
diff --git a/__tests__/src/sagas/annotations.test.js b/__tests__/src/sagas/annotations.test.js
new file mode 100644
index 0000000000000000000000000000000000000000..d99249da96145fb42eb5d3fbaaa1c7973312261a
--- /dev/null
+++ b/__tests__/src/sagas/annotations.test.js
@@ -0,0 +1,74 @@
+import { select } from 'redux-saga/effects';
+import { expectSaga } from 'redux-saga-test-plan';
+
+import { fetchAnnotations } from '../../../src/state/sagas/annotations';
+import { getAnnotations, getCanvases } from '../../../src/state/selectors';
+
+describe('annotation sagas', () => {
+  describe('fetchAnnotations', () => {
+    it('requests IIIF v2-style annotations for each visible canvas', () => {
+      const action = {
+        visibleCanvases: ['a', 'b'],
+        windowId: 'foo',
+      };
+
+      return expectSaga(fetchAnnotations, action)
+        .provide([
+          [select(getCanvases, { windowId: 'foo' }), [
+            { __jsonld: { otherContent: 'annoId' }, id: 'a' },
+            { __jsonld: { otherContent: ['alreadyFetched'] }, id: 'b' },
+          ]],
+          [select(getAnnotations), { a: {}, b: { alreadyFetched: {} } }],
+        ])
+        .put({
+          annotationId: 'annoId',
+          targetId: 'a',
+          type: 'mirador/REQUEST_ANNOTATION',
+        })
+        .run();
+    });
+    it('requests IIIF v3-style annotations for each visible canvas', () => {
+      const action = {
+        visibleCanvases: ['a', 'b'],
+        windowId: 'foo',
+      };
+
+      return expectSaga(fetchAnnotations, action)
+        .provide([
+          [select(getCanvases, { windowId: 'foo' }), [
+            { __jsonld: { annotations: { id: 'annoId', type: 'AnnotationPage' } }, id: 'a' },
+          ]],
+          [select(getAnnotations), { a: {} }],
+        ])
+        .put({
+          annotationId: 'annoId',
+          targetId: 'a',
+          type: 'mirador/REQUEST_ANNOTATION',
+        })
+        .run();
+    });
+    it('handles embedded IIIF v3-style annotations on each visible canvas', () => {
+      const action = {
+        visibleCanvases: ['a', 'b'],
+        windowId: 'foo',
+      };
+
+      const annotations = { id: 'annoId', items: [], type: 'AnnotationPage' };
+
+      return expectSaga(fetchAnnotations, action)
+        .provide([
+          [select(getCanvases, { windowId: 'foo' }), [
+            { __jsonld: { annotations }, id: 'a' },
+          ]],
+          [select(getAnnotations), { a: {} }],
+        ])
+        .put({
+          annotationId: 'annoId',
+          annotationJson: annotations,
+          targetId: 'a',
+          type: 'mirador/RECEIVE_ANNOTATION',
+        })
+        .run();
+    });
+  });
+});
diff --git a/__tests__/src/sagas/app.test.js b/__tests__/src/sagas/app.test.js
index 6ce748e340ad3932d03d6049f5a87d496946007e..807e401cea53e8e28651c4b580b79613b93ea760 100644
--- a/__tests__/src/sagas/app.test.js
+++ b/__tests__/src/sagas/app.test.js
@@ -1,9 +1,10 @@
 import { call } from 'redux-saga/effects';
-import { testSaga } from 'redux-saga-test-plan';
+import { expectSaga, testSaga } from 'redux-saga-test-plan';
 
-import { importState } from '../../../src/state/sagas/app';
+import { importConfig, importState } from '../../../src/state/sagas/app';
 import { fetchManifest } from '../../../src/state/sagas/iiif';
 import { fetchWindowManifest } from '../../../src/state/sagas/windows';
+import { addWindow } from '../../../src/state/actions';
 
 describe('app-level sagas', () => {
   describe('importState', () => {
@@ -52,4 +53,31 @@ describe('app-level sagas', () => {
         .all([]);
     });
   });
+
+  describe('importConfig', () => {
+    it('adds windows from the provided config', () => {
+      const action = {
+        config: {
+          thumbnailNavigation: {},
+          windows: [
+            { id: 'x', manifestId: 'a' },
+            { id: 'y', manifestId: 'b' },
+          ],
+        },
+      };
+
+      return expectSaga(importConfig, action)
+        .provide([
+          [call(addWindow, {
+            id: 'x', layoutOrder: 0, manifestId: 'a', thumbnailNavigationPosition: undefined,
+          }), { type: 'thunk1' }],
+          [call(addWindow, {
+            id: 'y', layoutOrder: 1, manifestId: 'b', thumbnailNavigationPosition: undefined,
+          }), { type: 'thunk2' }],
+        ])
+        .put({ type: 'thunk1' })
+        .put({ type: 'thunk2' })
+        .run();
+    });
+  });
 });
diff --git a/__tests__/src/sagas/windows.test.js b/__tests__/src/sagas/windows.test.js
index b557fda944ebf815500d18394bbfdd6fcb2bb84a..772051fa976ff57e3b80b9ecf104fd4f461ddc3f 100644
--- a/__tests__/src/sagas/windows.test.js
+++ b/__tests__/src/sagas/windows.test.js
@@ -13,6 +13,7 @@ import {
   getSelectedContentSearchAnnotationIds,
   getSortedSearchAnnotationsForCompanionWindow,
   getVisibleCanvasIds, getCanvasForAnnotation,
+  getCanvases, selectInfoResponses,
 } from '../../../src/state/selectors';
 import { fetchManifest } from '../../../src/state/sagas/iiif';
 import {
@@ -25,6 +26,7 @@ import {
   setCanvasforSelectedAnnotation,
   panToFocusedWindow,
   setCurrentAnnotationsOnCurrentCanvas,
+  fetchInfoResponses,
 } from '../../../src/state/sagas/windows';
 import fixture from '../../fixtures/version-2/019.json';
 
@@ -398,4 +400,50 @@ describe('window-level sagas', () => {
         .run().then(({ allEffects }) => allEffects.length === 0);
     });
   });
+
+  describe('fetchInfoResponses', () => {
+    it('requests info responses for each visible canvas', () => {
+      const action = {
+        visibleCanvases: ['http://iiif.io/api/presentation/2.0/example/fixtures/canvas/24/c1.json'],
+        windowId: 'foo',
+      };
+
+      const manifest = Utils.parseManifest(fixture);
+
+      return expectSaga(fetchInfoResponses, action)
+        .provide([
+          [select(getCanvases, { windowId: 'foo' }), manifest.getSequences()[0].getCanvases()],
+          [select(selectInfoResponses), {}],
+        ])
+        .put.like({
+          action: {
+            infoId: 'https://stacks.stanford.edu/image/iiif/hg676jb4964%2F0380_796-44',
+            type: 'mirador/REQUEST_INFO_RESPONSE',
+          },
+        })
+        .run();
+    });
+
+    it('requests nothing if the response is  already in the store', () => {
+      const action = {
+        visibleCanvases: ['http://iiif.io/api/presentation/2.0/example/fixtures/canvas/24/c1.json'],
+        windowId: 'foo',
+      };
+
+      const manifest = Utils.parseManifest(fixture);
+
+      return expectSaga(fetchInfoResponses, action)
+        .provide([
+          [select(getCanvases, { windowId: 'foo' }), manifest.getSequences()[0].getCanvases()],
+          [select(selectInfoResponses), { 'https://stacks.stanford.edu/image/iiif/hg676jb4964%2F0380_796-44': {} }],
+        ])
+        .not.put.like({
+          action: {
+            infoId: 'https://stacks.stanford.edu/image/iiif/hg676jb4964%2F0380_796-44',
+            type: 'mirador/REQUEST_INFO_RESPONSE',
+          },
+        })
+        .run();
+    });
+  });
 });
diff --git a/package.json b/package.json
index 5f71f471357475b32d43928315953927e58cae48..12e3b216deb6037f552f0c8358b108d65caf5205 100644
--- a/package.json
+++ b/package.json
@@ -73,7 +73,6 @@
     "redux": "4.0.1",
     "redux-devtools-extension": "^2.13.2",
     "redux-saga": "^1.1.3",
-    "redux-saga-test-plan": "^4.0.0-rc.3",
     "redux-thunk": "^2.3.0",
     "reselect": "^4.0.0",
     "uuid": "^8.1.0"
@@ -120,6 +119,7 @@
     "react-dev-utils": "^9.0.1",
     "react-dom": "^16.8.6",
     "redux-mock-store": "^1.5.1",
+    "redux-saga-test-plan": "^4.0.0-rc.3",
     "style-loader": "^0.23.1",
     "supertest": "^4.0.2",
     "terser-webpack-plugin": "^1.3.0",
diff --git a/src/components/WindowViewer.js b/src/components/WindowViewer.js
index 995e5f1fe86f0010bbaa93e1b321c4650d2e45dd..7a7d1acc697ea00338b6ce34db34dad0c2316cb9 100644
--- a/src/components/WindowViewer.js
+++ b/src/components/WindowViewer.js
@@ -1,10 +1,7 @@
 import React, { Component } from 'react';
 import PropTypes from 'prop-types';
-import difference from 'lodash/difference';
-import flatten from 'lodash/flatten';
 import OSDViewer from '../containers/OpenSeadragonViewer';
 import WindowCanvasNavigationControls from '../containers/WindowCanvasNavigationControls';
-import MiradorCanvas from '../lib/MiradorCanvas';
 
 /**
  * Represents a WindowViewer in the mirador workspace. Responsible for mounting
@@ -23,93 +20,6 @@ export class WindowViewer extends Component {
     return { hasError: true };
   }
 
-  /**
-   * componentDidMount - React lifecycle method
-   * Request the initial canvas on mount
-   */
-  componentDidMount() {
-    const {
-      currentCanvases, fetchInfoResponse, fetchAnnotation, receiveAnnotation,
-    } = this.props;
-
-    if (!this.infoResponseIsInStore()) {
-      currentCanvases.forEach((canvas) => {
-        const miradorCanvas = new MiradorCanvas(canvas);
-        miradorCanvas.iiifImageResources.forEach((imageResource) => {
-          fetchInfoResponse({ imageResource });
-        });
-        miradorCanvas.processAnnotations(fetchAnnotation, receiveAnnotation);
-      });
-    }
-  }
-
-  /**
-   * componentDidUpdate - React lifecycle method
-   * Request a new canvas if it is needed
-   */
-  componentDidUpdate(prevProps) {
-    const {
-      currentCanvases, fetchInfoResponse, fetchAnnotation, receiveAnnotation,
-    } = this.props;
-
-    if (difference(currentCanvases, prevProps.currentCanvases).length > 0
-    && !this.infoResponseIsInStore()) {
-      currentCanvases.forEach((canvas) => {
-        const miradorCanvas = new MiradorCanvas(canvas);
-        miradorCanvas.iiifImageResources.forEach((imageResource) => {
-          fetchInfoResponse({ imageResource });
-        });
-        miradorCanvas.processAnnotations(fetchAnnotation, receiveAnnotation);
-      });
-    }
-  }
-
-  /**
-   * infoResponseIsInStore - checks whether or not an info response is already
-   * in the store. No need to request it again.
-   * @return [Boolean]
-   */
-  infoResponseIsInStore() {
-    const responses = this.currentInfoResponses();
-    if (responses.length === this.imageServiceIds().length) {
-      return true;
-    }
-    return false;
-  }
-
-  /** */
-  imageServiceIds() {
-    const { currentCanvases } = this.props;
-
-    return flatten(currentCanvases.map(canvas => new MiradorCanvas(canvas).imageServiceIds));
-  }
-
-  /**
-   * currentInfoResponses - Selects infoResponses that are relevent to existing
-   * canvases to be displayed.
-   */
-  currentInfoResponses() {
-    const { infoResponses } = this.props;
-
-    return this.imageServiceIds().map(imageId => (
-      infoResponses[imageId]
-    )).filter(infoResponse => (infoResponse !== undefined
-      && infoResponse.isFetching === false
-      && infoResponse.error === undefined));
-  }
-
-  /**
-   * Return an image information response from the store for the correct image
-   */
-  infoResponsesFetchedFromStore() {
-    const responses = this.currentInfoResponses();
-    // Only return actual tileSources when all current canvases have completed.
-    if (responses.length === this.imageServiceIds().length) {
-      return responses;
-    }
-    return [];
-  }
-
   /**
    * Renders things
    */
@@ -124,20 +34,14 @@ export class WindowViewer extends Component {
 
     return (
       <OSDViewer
-        infoResponses={this.infoResponsesFetchedFromStore()}
         windowId={windowId}
       >
-        <WindowCanvasNavigationControls key="canvas_nav" windowId={windowId} />
+        <WindowCanvasNavigationControls windowId={windowId} />
       </OSDViewer>
     );
   }
 }
 
 WindowViewer.propTypes = {
-  currentCanvases: PropTypes.array.isRequired, // eslint-disable-line react/forbid-prop-types
-  fetchAnnotation: PropTypes.func.isRequired,
-  fetchInfoResponse: PropTypes.func.isRequired,
-  infoResponses: PropTypes.object.isRequired, // eslint-disable-line react/forbid-prop-types
-  receiveAnnotation: PropTypes.func.isRequired,
   windowId: PropTypes.string.isRequired,
 };
diff --git a/src/containers/OpenSeadragonViewer.js b/src/containers/OpenSeadragonViewer.js
index 96faa482966383f25357f297faa3ed8742b7e7b7..c733eb33bce2be58a22f31805de13239a1ca870c 100644
--- a/src/containers/OpenSeadragonViewer.js
+++ b/src/containers/OpenSeadragonViewer.js
@@ -2,6 +2,7 @@ import { compose } from 'redux';
 import { connect } from 'react-redux';
 import { withTranslation } from 'react-i18next';
 import { withStyles } from '@material-ui/core/styles';
+import flatten from 'lodash/flatten';
 import { withPlugins } from '../extend/withPlugins';
 import { OpenSeadragonViewer } from '../components/OpenSeadragonViewer';
 import * as actions from '../state/actions';
@@ -14,6 +15,7 @@ import {
   getViewer,
   getConfig,
   getCompanionWindowsForContent,
+  selectInfoResponses,
 } from '../state/selectors';
 
 /**
@@ -21,19 +23,30 @@ import {
  * @memberof Window
  * @private
  */
-const mapStateToProps = (state, { windowId }) => ({
-  canvasWorld: new CanvasWorld(
+const mapStateToProps = (state, { windowId }) => {
+  const canvasWorld = new CanvasWorld(
     getVisibleCanvases(state, { windowId }),
     getLayersForVisibleCanvases(state, { windowId }),
     getSequenceViewingDirection(state, { windowId }),
-  ),
-  drawAnnotations: getConfig(state).window.forceDrawAnnotations
-    || getCompanionWindowsForContent(state, { content: 'annotations', windowId }).length > 0
-    || getCompanionWindowsForContent(state, { content: 'search', windowId }).length > 0,
-  nonTiledImages: getVisibleCanvasNonTiledResources(state, { windowId }),
-  osdConfig: state.config.osdConfig,
-  viewerConfig: getViewer(state, { windowId }),
-});
+  );
+
+  const infoResponses = selectInfoResponses(state);
+  const imageServiceIds = flatten(canvasWorld.canvases.map(c => c.imageServiceIds));
+
+  return {
+    canvasWorld,
+    drawAnnotations: getConfig(state).window.forceDrawAnnotations
+      || getCompanionWindowsForContent(state, { content: 'annotations', windowId }).length > 0
+      || getCompanionWindowsForContent(state, { content: 'search', windowId }).length > 0,
+    infoResponses: imageServiceIds.map(id => infoResponses[id])
+      .filter(infoResponse => (infoResponse !== undefined
+        && infoResponse.isFetching === false
+        && infoResponse.error === undefined)),
+    nonTiledImages: getVisibleCanvasNonTiledResources(state, { windowId }),
+    osdConfig: state.config.osdConfig,
+    viewerConfig: getViewer(state, { windowId }),
+  };
+};
 
 /**
  * mapDispatchToProps - used to hook up connect to action creators
diff --git a/src/containers/WindowViewer.js b/src/containers/WindowViewer.js
index 1a687a388ab22f2532ee074ccdda52bd47893a58..d61c69c21f37a6179a505e954e7607aef4fb909a 100644
--- a/src/containers/WindowViewer.js
+++ b/src/containers/WindowViewer.js
@@ -3,19 +3,13 @@ import { connect } from 'react-redux';
 import { withPlugins } from '../extend/withPlugins';
 import * as actions from '../state/actions';
 import { WindowViewer } from '../components/WindowViewer';
-import { getVisibleCanvases } from '../state/selectors';
 
 /**
  * mapStateToProps - to hook up connect
  * @memberof WindowViewer
  * @private
  */
-const mapStateToProps = (state, { windowId }) => (
-  {
-    currentCanvases: getVisibleCanvases(state, { windowId }) || [],
-    infoResponses: state.infoResponses,
-  }
-);
+const mapStateToProps = (state, { windowId }) => ({});
 
 /**
  * mapDispatchToProps - used to hook up connect to action creators
diff --git a/src/lib/MiradorCanvas.js b/src/lib/MiradorCanvas.js
index 9515320efe92c59fec484ee6e4122915a9aa318f..0c69710307c16e841abfcbcea260bd389e4219a1 100644
--- a/src/lib/MiradorCanvas.js
+++ b/src/lib/MiradorCanvas.js
@@ -57,24 +57,6 @@ export default class MiradorCanvas {
       .filter(annotations => annotations && annotations.type === 'AnnotationPage');
   }
 
-  /** */
-  processAnnotations(fetchAnnotation, receiveAnnotation) {
-    // IIIF v2
-    this.annotationListUris.forEach((uri) => {
-      fetchAnnotation(this.canvas.id, uri);
-    });
-    // IIIF v3
-    this.canvasAnnotationPages.forEach((annotation) => {
-      // If there are no items, try to retrieve the referenced resource.
-      // otherwise the resource should be embedded and just add to the store.
-      if (!annotation.items) {
-        fetchAnnotation(this.canvas.id, annotation.id);
-      } else {
-        receiveAnnotation(this.canvas.id, annotation.id, annotation);
-      }
-    });
-  }
-
   /**
    * Will negotiate a v2 or v3 type of resource
    */
diff --git a/src/lib/MiradorViewer.js b/src/lib/MiradorViewer.js
index cf54c62b47ccac3d5a7d67f542a20f39748bc6d3..81346abbab0e717046c45899fa426ed8f6570630 100644
--- a/src/lib/MiradorViewer.js
+++ b/src/lib/MiradorViewer.js
@@ -1,7 +1,6 @@
 import React from 'react';
 import ReactDOM from 'react-dom';
 import { Provider } from 'react-redux';
-import { v4 as uuid } from 'uuid';
 import HotApp from '../components/App';
 import createStore from '../state/createStore';
 import * as actions from '../state/actions';
@@ -46,22 +45,6 @@ class MiradorViewer {
     /** merge type for arrays */
     const action = actions.importConfig(this.config);
     this.store.dispatch(action);
-    const { config: storedConfig } = this.store.getState();
-
-    storedConfig.windows.forEach((miradorWindow, layoutOrder) => {
-      const windowId = `window-${uuid()}`;
-      const manifestId = miradorWindow.manifestId || miradorWindow.loadedManifest;
-
-      this.store.dispatch(actions.addWindow({
-        // these are default values ...
-        id: windowId,
-        layoutOrder,
-        manifestId,
-        thumbnailNavigationPosition: storedConfig.thumbnailNavigation.defaultPosition,
-        // ... overridden by values from the window configuration ...
-        ...miradorWindow,
-      }));
-    });
   }
 }
 
diff --git a/src/state/actions/annotation.js b/src/state/actions/annotation.js
index dd61cb0f325e16c9e426a2594b7aa99505f20487..1091d7e4a9b20236d4460bc19520b41296404838 100644
--- a/src/state/actions/annotation.js
+++ b/src/state/actions/annotation.js
@@ -49,16 +49,6 @@ export function receiveAnnotationFailure(targetId, annotationId, error) {
   };
 }
 
-/**
- * fetchAnnotation - action creator
- *
- * @param  {String} annotationId
- * @memberof ActionCreators
- */
-export function fetchAnnotation(targetId, annotationId) {
-  return requestAnnotation(targetId, annotationId);
-}
-
 /**
  * selectAnnotation - action creator
  *
diff --git a/src/state/sagas/annotations.js b/src/state/sagas/annotations.js
new file mode 100644
index 0000000000000000000000000000000000000000..885c19ead77f0ec627a4f9d87fb2ff5d3145891e
--- /dev/null
+++ b/src/state/sagas/annotations.js
@@ -0,0 +1,45 @@
+import {
+  all, put, select, takeEvery,
+} from 'redux-saga/effects';
+import { receiveAnnotation, requestAnnotation } from '../actions';
+import { getAnnotations, getCanvases } from '../selectors';
+import ActionTypes from '../actions/action-types';
+import MiradorCanvas from '../../lib/MiradorCanvas';
+
+/** Fetch annotations for the visible canvases */
+export function* fetchAnnotations({ visibleCanvases: visibleCanvasIds, windowId }) {
+  const canvases = yield select(getCanvases, { windowId });
+  const visibleCanvases = (canvases || []).filter(c => visibleCanvasIds.includes(c.id));
+
+  const annotations = yield select(getAnnotations);
+
+  yield all(visibleCanvases.map((canvas) => {
+    const miradorCanvas = new MiradorCanvas(canvas);
+
+    return all([
+      // IIIF v2
+      ...miradorCanvas.annotationListUris
+        .filter(uri => !(annotations[canvas.id] && annotations[canvas.id][uri]))
+        .map(uri => put(requestAnnotation(canvas.id, uri))),
+      // IIIF v3
+      ...miradorCanvas.canvasAnnotationPages
+        .filter(annotation => !(annotations[canvas.id] && annotations[canvas.id][annotation.id]))
+        .map((annotation) => {
+          // If there are no items, try to retrieve the referenced resource.
+          // otherwise the resource should be embedded and just add to the store.
+          if (!annotation.items) {
+            return put(requestAnnotation(canvas.id, annotation.id));
+          }
+
+          return put(receiveAnnotation(canvas.id, annotation.id, annotation));
+        }),
+    ]);
+  }));
+}
+
+/** */
+export default function* appSaga() {
+  yield all([
+    takeEvery(ActionTypes.SET_CANVAS, fetchAnnotations),
+  ]);
+}
diff --git a/src/state/sagas/app.js b/src/state/sagas/app.js
index 08de29310c748eec72c9f80ac506159460a918b1..de44905423376b3cc3b5a212f2d502fe34e2a981 100644
--- a/src/state/sagas/app.js
+++ b/src/state/sagas/app.js
@@ -1,8 +1,10 @@
 import {
-  all, call, takeEvery,
+  all, call, put, takeEvery,
 } from 'redux-saga/effects';
+import { v4 as uuid } from 'uuid';
 import { fetchManifest } from './iiif';
 import { fetchWindowManifest } from './windows';
+import { addWindow } from '../actions';
 import ActionTypes from '../actions/action-types';
 
 /** */
@@ -16,9 +18,34 @@ export function* importState(action) {
   ]);
 }
 
+/** Add windows from the imported config */
+export function* importConfig({ config: { thumbnailNavigation, windows } }) {
+  if (!windows || windows.length === 0) return;
+
+  const thunks = yield all(
+    windows.map((miradorWindow, layoutOrder) => {
+      const windowId = `window-${uuid()}`;
+      const manifestId = miradorWindow.manifestId || miradorWindow.loadedManifest;
+
+      return call(addWindow, {
+        // these are default values ...
+        id: windowId,
+        layoutOrder,
+        manifestId,
+        thumbnailNavigationPosition: thumbnailNavigation && thumbnailNavigation.defaultPosition,
+        // ... overridden by values from the window configuration ...
+        ...miradorWindow,
+      });
+    }),
+  );
+
+  yield all(thunks.map(thunk => put(thunk)));
+}
+
 /** */
 export default function* appSaga() {
   yield all([
     takeEvery(ActionTypes.IMPORT_MIRADOR_STATE, importState),
+    takeEvery(ActionTypes.IMPORT_CONFIG, importConfig),
   ]);
 }
diff --git a/src/state/sagas/index.js b/src/state/sagas/index.js
index fb13462ab9f96cfbff96ee4119e06510901bd936..63ff1cac0bbe1811ed27ed1c2ae34cc16ed2a52a 100644
--- a/src/state/sagas/index.js
+++ b/src/state/sagas/index.js
@@ -5,6 +5,7 @@ import {
 import appSaga from './app';
 import iiifSaga from './iiif';
 import windowSaga from './windows';
+import annotations from './annotations';
 
 /** */
 function* launchSaga(saga) {
@@ -22,6 +23,7 @@ function* launchSaga(saga) {
 function getRootSaga(pluginSagas) {
   return function* rootSaga() {
     const sagas = [
+      annotations,
       appSaga,
       iiifSaga,
       windowSaga,
diff --git a/src/state/sagas/windows.js b/src/state/sagas/windows.js
index f25da8822aff67334f43643f6d2f9fc47d1eb7a0..656a9f0947511fbb7a1dc3e69615180e1b57451c 100644
--- a/src/state/sagas/windows.js
+++ b/src/state/sagas/windows.js
@@ -3,6 +3,7 @@ import {
 } from 'redux-saga/effects';
 import ActionTypes from '../actions/action-types';
 import MiradorManifest from '../../lib/MiradorManifest';
+import MiradorCanvas from '../../lib/MiradorCanvas';
 import {
   setContentSearchCurrentAnnotation,
   selectAnnotation,
@@ -11,6 +12,7 @@ import {
   setCanvas,
   fetchSearch,
   receiveManifest,
+  fetchInfoResponse,
 } from '../actions';
 import {
   getSearchForWindow, getSearchAnnotationsForCompanionWindow,
@@ -22,6 +24,8 @@ import {
   getVisibleCanvasIds,
   getWorkspace,
   getElasticLayout,
+  getCanvases,
+  selectInfoResponses,
 } from '../selectors';
 import { fetchManifest } from './iiif';
 
@@ -183,12 +187,28 @@ export function* setCanvasforSelectedAnnotation({ annotationId, windowId }) {
   yield put(thunk);
 }
 
+/** Fetch info responses for the visible canvases */
+export function* fetchInfoResponses({ visibleCanvases: visibleCanvasIds, windowId }) {
+  const canvases = yield select(getCanvases, { windowId });
+  const infoResponses = yield select(selectInfoResponses);
+  const visibleCanvases = (canvases || []).filter(c => visibleCanvasIds.includes(c.id));
+
+  yield all(visibleCanvases.map((canvas) => {
+    const miradorCanvas = new MiradorCanvas(canvas);
+    return all(miradorCanvas.iiifImageResources.map(imageResource => (
+      !infoResponses[imageResource.getServices()[0].id]
+        && put(fetchInfoResponse({ imageResource }))
+    )).filter(Boolean));
+  }));
+}
+
 /** */
 export default function* windowsSaga() {
   yield all([
     takeEvery(ActionTypes.ADD_WINDOW, fetchWindowManifest),
     takeEvery(ActionTypes.UPDATE_WINDOW, fetchWindowManifest),
     takeEvery(ActionTypes.SET_CANVAS, setCurrentAnnotationsOnCurrentCanvas),
+    takeEvery(ActionTypes.SET_CANVAS, fetchInfoResponses),
     takeEvery(ActionTypes.SET_WINDOW_VIEW_TYPE, updateVisibleCanvases),
     takeEvery(ActionTypes.RECEIVE_SEARCH, setCanvasOfFirstSearchResult),
     takeEvery(ActionTypes.SELECT_ANNOTATION, setCanvasforSelectedAnnotation),
diff --git a/src/state/selectors/annotations.js b/src/state/selectors/annotations.js
index c80ba04fc5e06ee7b87d98aeb769124978715481..7fec25ef117cbd398c271ee803481b627ddd619f 100644
--- a/src/state/selectors/annotations.js
+++ b/src/state/selectors/annotations.js
@@ -4,10 +4,13 @@ import flatten from 'lodash/flatten';
 import AnnotationFactory from '../../lib/AnnotationFactory';
 import { getCanvas, getVisibleCanvasIds } from './canvases';
 
+/** */
+export const getAnnotations = state => state.annotations;
+
 const getAnnotationsOnCanvas = createSelector(
   [
     getCanvas,
-    state => state.annotations,
+    getAnnotations,
   ],
   (canvas, annotations) => {
     if (!annotations || !canvas) return [];