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..8414de7fc16593d5e4df5590747add73f5dc5442 100644
--- a/__tests__/src/components/WindowViewer.test.js
+++ b/__tests__/src/components/WindowViewer.test.js
@@ -6,7 +6,6 @@ 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();
 
@@ -18,7 +17,6 @@ function createWrapper(props) {
       canvasLabel="label"
       infoResponses={{}}
       fetchInfoResponse={() => {}}
-      fetchAnnotation={() => {}}
       currentCanvases={[currentCanvases[1]]}
       view="single"
       windowId="xyz"
@@ -123,16 +121,6 @@ describe('WindowViewer', () => {
       );
       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', () => {
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/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/src/components/WindowViewer.js b/src/components/WindowViewer.js
index 995e5f1fe86f0010bbaa93e1b321c4650d2e45dd..beeba092c5197419ef70a1961de71761133bd95c 100644
--- a/src/components/WindowViewer.js
+++ b/src/components/WindowViewer.js
@@ -29,7 +29,7 @@ export class WindowViewer extends Component {
    */
   componentDidMount() {
     const {
-      currentCanvases, fetchInfoResponse, fetchAnnotation, receiveAnnotation,
+      currentCanvases, fetchInfoResponse,
     } = this.props;
 
     if (!this.infoResponseIsInStore()) {
@@ -38,7 +38,6 @@ export class WindowViewer extends Component {
         miradorCanvas.iiifImageResources.forEach((imageResource) => {
           fetchInfoResponse({ imageResource });
         });
-        miradorCanvas.processAnnotations(fetchAnnotation, receiveAnnotation);
       });
     }
   }
@@ -49,7 +48,7 @@ export class WindowViewer extends Component {
    */
   componentDidUpdate(prevProps) {
     const {
-      currentCanvases, fetchInfoResponse, fetchAnnotation, receiveAnnotation,
+      currentCanvases, fetchInfoResponse,
     } = this.props;
 
     if (difference(currentCanvases, prevProps.currentCanvases).length > 0
@@ -59,7 +58,6 @@ export class WindowViewer extends Component {
         miradorCanvas.iiifImageResources.forEach((imageResource) => {
           fetchInfoResponse({ imageResource });
         });
-        miradorCanvas.processAnnotations(fetchAnnotation, receiveAnnotation);
       });
     }
   }
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/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/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/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 [];