diff --git a/__tests__/src/actions/index.test.js b/__tests__/src/actions/index.test.js
index 17dfa2caebf95e7eebe76a752ef6fce4e806c9a7..1dd4c73127380b22a29b4a904e46290115d45955 100644
--- a/__tests__/src/actions/index.test.js
+++ b/__tests__/src/actions/index.test.js
@@ -133,4 +133,77 @@ describe('actions', () => {
       expect(actions.removeManifest('foo')).toEqual(expectedAction);
     });
   });
+  describe('requestInfoResponse', () => {
+    it('requests an infoResponse from given a url', () => {
+      const id = 'abc123';
+      const expectedAction = {
+        type: ActionTypes.REQUEST_INFO_RESPONSE,
+        infoId: id,
+      };
+      expect(actions.requestInfoResponse(id)).toEqual(expectedAction);
+    });
+  });
+  describe('receiveInfoResponse', () => {
+    it('recieves an infoResponse', () => {
+      const id = 'abc123';
+      const json = {
+        id,
+        content: 'image information request',
+      };
+      const expectedAction = {
+        type: ActionTypes.RECEIVE_INFO_RESPONSE,
+        infoId: id,
+        infoJson: json,
+      };
+      expect(actions.receiveInfoResponse(id, json)).toEqual(expectedAction);
+    });
+  });
+  describe('fetchInfoResponse', () => {
+    let store = null;
+    beforeEach(() => {
+      store = mockStore({});
+    });
+    describe('success response', () => {
+      beforeEach(() => {
+        fetch.mockResponseOnce(JSON.stringify({ data: '12345' })); // eslint-disable-line no-undef
+      });
+      it('dispatches the REQUEST_MANIFEST action', () => {
+        store.dispatch(actions.fetchInfoResponse('https://stacks.stanford.edu/image/iiif/sn904cj3429%2F12027000/info.json'));
+        expect(store.getActions()).toEqual([
+          { infoId: 'https://stacks.stanford.edu/image/iiif/sn904cj3429%2F12027000/info.json', type: 'REQUEST_INFO_RESPONSE' },
+        ]);
+      });
+      it('dispatches the REQUEST_MANIFEST and then RECEIVE_MANIFEST', () => {
+        store.dispatch(actions.fetchInfoResponse('https://stacks.stanford.edu/image/iiif/sn904cj3429%2F12027000/info.json'))
+          .then(() => {
+            const expectedActions = store.getActions();
+            expect(expectedActions).toEqual([
+              { infoId: 'https://stacks.stanford.edu/image/iiif/sn904cj3429%2F12027000/info.json', type: 'REQUEST_INFO_RESPONSE' },
+              { infoId: 'https://stacks.stanford.edu/image/iiif/sn904cj3429%2F12027000/info.json', infoJson: { data: '12345' }, type: 'RECEIVE_INFO_RESPONSE' },
+            ]);
+          });
+      });
+    });
+    describe('error response', () => {
+      it('dispatches the REQUEST_INFO_RESPONSE and then RECEIVE_INFO_RESPONSE', () => {
+        store.dispatch(actions.fetchInfoResponse('https://stacks.stanford.edu/image/iiif/sn904cj3429%2F12027000/info.json'))
+          .then(() => {
+            const expectedActions = store.getActions();
+            expect(expectedActions).toEqual([
+              { infoId: 'https://stacks.stanford.edu/image/iiif/sn904cj3429%2F12027000/info.json', type: 'REQUEST_INFO_RESPONSE' },
+              { infoId: 'https://stacks.stanford.edu/image/iiif/sn904cj3429%2F12027000/info.json', error: new Error('invalid json response body at undefined reason: Unexpected end of JSON input'), type: 'RECEIVE_INFO_RESPONSE_FAILURE' },
+            ]);
+          });
+      });
+    });
+  });
+  describe('removeInfoResponse', () => {
+    it('removes an existing infoResponse', () => {
+      const expectedAction = {
+        type: ActionTypes.REMOVE_INFO_RESPONSE,
+        infoId: 'foo',
+      };
+      expect(actions.removeInfoResponse('foo')).toEqual(expectedAction);
+    });
+  });
 });
diff --git a/__tests__/src/reducers/infoResponse.test.js b/__tests__/src/reducers/infoResponse.test.js
new file mode 100644
index 0000000000000000000000000000000000000000..7841eb8b5e882383bbc88822d720f92afc38558c
--- /dev/null
+++ b/__tests__/src/reducers/infoResponse.test.js
@@ -0,0 +1,70 @@
+import reducer from '../../../src/reducers/infoResponses';
+import ActionTypes from '../../../src/action-types';
+
+describe('manifests reducer', () => {
+  it('should handle REQUEST_INFO_RESPONSE', () => {
+    expect(reducer({}, {
+      type: ActionTypes.REQUEST_INFO_RESPONSE,
+      infoId: 'abc123',
+    })).toEqual({
+      abc123: {
+        isFetching: true,
+      },
+    });
+  });
+  it('should handle RECEIVE_INFO_RESPONSE', () => {
+    expect(reducer(
+      {
+        abc123: {
+          isFetching: true,
+        },
+      },
+      {
+        type: ActionTypes.RECEIVE_INFO_RESPONSE,
+        infoId: 'abc123',
+        infoJson: {
+          id: 'abc123',
+          '@type': 'sc:Manifest',
+          content: 'lots of canvases and metadata and such',
+        },
+      },
+    )).toMatchObject({
+      abc123: {
+        isFetching: false,
+        json: {},
+      },
+    });
+  });
+  it('should handle RECEIVE_INFO_RESPONSE_FAILURE', () => {
+    expect(reducer(
+      {
+        abc123: {
+          isFetching: true,
+        },
+      },
+      {
+        type: ActionTypes.RECEIVE_INFO_RESPONSE_FAILURE,
+        infoId: 'abc123',
+        error: "This institution didn't enable CORS.",
+      },
+    )).toEqual({
+      abc123: {
+        isFetching: false,
+        error: "This institution didn't enable CORS.",
+      },
+    });
+  });
+  it('should handle REMOVE_INFO_RESPONSE', () => {
+    expect(reducer(
+      {
+        abc123: {
+          stuff: 'foo',
+        },
+      },
+      {
+        type: ActionTypes.REMOVE_INFO_RESPONSE,
+        infoId: 'abc123',
+      },
+    )).toEqual({});
+  });
+});
diff --git a/src/action-types.js b/src/action-types.js
index 70d859bca9806ece145f2f584eec180b109f1daa..9783cb967f276c8c5974510e9448cb22646dc649 100644
--- a/src/action-types.js
+++ b/src/action-types.js
@@ -12,6 +12,10 @@ const ActionTypes = {
   SET_CONFIG: 'SET_CONFIG',
   UPDATE_CONFIG: 'UPDATE_CONFIG',
   REMOVE_MANIFEST: 'REMOVE_MANIFEST',
+  REQUEST_INFO_RESPONSE: 'REQUEST_INFO_RESPONSE',
+  RECEIVE_INFO_RESPONSE: 'RECEIVE_INFO_RESPONSE',
+  RECEIVE_INFO_RESPONSE_FAILURE: 'RECEIVE_INFO_RESPONSE_FAILURE',
+  REMOVE_INFO_RESPONSE: 'REMOVE_INFO_RESPONSE',
 };
 
 export default ActionTypes;
diff --git a/src/actions/index.js b/src/actions/index.js
index 5ecbb27a30b7cf62b700fcb37fd356a5fb8923fb..ec56ae64a9757709b08108efe0f7ad38a45055b9 100644
--- a/src/actions/index.js
+++ b/src/actions/index.js
@@ -156,3 +156,72 @@ export function fetchManifest(manifestId) {
 export function removeManifest(manifestId) {
   return { type: ActionTypes.REMOVE_MANIFEST, manifestId };
 }
+
+/**
+ * requestInfoResponse - action creator
+ *
+ * @param  {String} infoId
+ * @memberof ActionCreators
+ */
+export function requestInfoResponse(infoId) {
+  return {
+    type: ActionTypes.REQUEST_INFO_RESPONSE,
+    infoId,
+  };
+}
+
+/**
+ * receiveInfoResponse - action creator
+ *
+ * @param  {String} infoId
+ * @param  {Object} manifestJson
+ * @memberof ActionCreators
+ */
+export function receiveInfoResponse(infoId, infoJson) {
+  return {
+    type: ActionTypes.RECEIVE_INFO_RESPONSE,
+    infoId,
+    infoJson,
+  };
+}
+
+/**
+ * receiveInfoResponseFailure - action creator
+ *
+ * @param  {String} infoId
+ * @param  {String} error
+ * @memberof ActionCreators
+ */
+export function receiveInfoResponseFailure(infoId, error) {
+  return {
+    type: ActionTypes.RECEIVE_INFO_RESPONSE_FAILURE,
+    infoId,
+    error,
+  };
+}
+
+/**
+ * fetchInfoResponse - action creator
+ *
+ * @param  {String} infoId
+ * @memberof ActionCreators
+ */
+export function fetchInfoResponse(infoId) {
+  return ((dispatch) => {
+    dispatch(requestInfoResponse(infoId));
+    return fetch(infoId)
+      .then(response => response.json())
+      .then(json => dispatch(receiveInfoResponse(infoId, json)))
+      .catch(error => dispatch(receiveInfoResponseFailure(infoId, error)));
+  });
+}
+
+/**
+ * removeInfoResponse - action creator
+ *
+ * @param  {String} infoId
+ * @memberof ActionCreators
+ */
+export function removeInfoResponse(infoId) {
+  return { type: ActionTypes.REMOVE_INFO_RESPONSE, infoId };
+}
diff --git a/src/components/WindowViewer.js b/src/components/WindowViewer.js
index f0624e5f25f26e4333c8d8dabe98984909eb93d7..77f77b7fd45e794a444861f6f1f7816f42e2f4b8 100644
--- a/src/components/WindowViewer.js
+++ b/src/components/WindowViewer.js
@@ -1,6 +1,7 @@
 import React, { Component, Fragment } from 'react';
 import PropTypes from 'prop-types';
-import fetch from 'node-fetch';
+import { connect } from 'react-redux';
+import { actions } from '../store';
 import miradorWithPlugins from '../lib/miradorWithPlugins';
 import OpenSeadragonViewer from './OpenSeadragonViewer';
 import ViewerNavigation from './ViewerNavigation';
@@ -18,9 +19,6 @@ class WindowViewer extends Component {
 
     const { manifest } = this.props;
     this.canvases = manifest.manifestation.getSequences()[0].getCanvases();
-    this.state = {
-      tileSources: [],
-    };
   }
 
   /**
@@ -28,7 +26,8 @@ class WindowViewer extends Component {
    * Request the initial canvas on mount
    */
   componentDidMount() {
-    this.requestAndUpdateTileSources();
+    const { fetchInfoResponse } = this.props;
+    fetchInfoResponse(this.imageInformationUri());
   }
 
   /**
@@ -36,23 +35,43 @@ class WindowViewer extends Component {
    * Request a new canvas if it is needed
    */
   componentDidUpdate(prevProps) {
-    const { window } = this.props;
-    if (prevProps.window.canvasIndex !== window.canvasIndex) {
-      this.requestAndUpdateTileSources();
+    const { window, fetchInfoResponse } = this.props;
+    if (prevProps.window.canvasIndex !== window.canvasIndex && !this.infoResponseIsInStore()) {
+      fetchInfoResponse(this.imageInformationUri());
     }
   }
 
   /**
+   * infoResponseIsInStore - checks whether or not an info response is already
+   * in the store. No need to request it again.
+   * @return [Boolean]
+   */
+  infoResponseIsInStore() {
+    const { infoResponses } = this.props;
+    const currentInfoResponse = infoResponses[this.imageInformationUri()];
+    return (currentInfoResponse !== undefined
+      && currentInfoResponse.isFetching === false
+      && currentInfoResponse.json !== undefined);
+  }
+
+  /**
+   * Constructs an image information URI to request from a canvas
    */
-  requestAndUpdateTileSources() {
+  imageInformationUri() {
     const { window } = this.props;
-    fetch(`${this.canvases[window.canvasIndex].getImages()[0].getResource().getServices()[0].id}/info.json`)
-      .then(response => response.json())
-      .then((json) => {
-        this.setState({
-          tileSources: [json],
-        });
-      });
+    return `${this.canvases[window.canvasIndex].getImages()[0].getResource().getServices()[0].id}/info.json`;
+  }
+
+  /**
+   * Return an image information response from the store for the correct image
+   */
+  tileInfoFetchedFromStore() {
+    const { infoResponses } = this.props;
+    return [infoResponses[this.imageInformationUri()]]
+      .filter(infoResponse => (infoResponse !== undefined
+        && infoResponse.isFetching === false
+        && infoResponse.error === undefined))
+      .map(infoResponse => infoResponse.json);
   }
 
   /**
@@ -60,19 +79,43 @@ class WindowViewer extends Component {
    */
   render() {
     const { window } = this.props;
-    const { tileSources } = this.state;
     return (
       <Fragment>
-        <OpenSeadragonViewer tileSources={tileSources} window={window} />
+        <OpenSeadragonViewer
+          tileSources={this.tileInfoFetchedFromStore()}
+          window={window}
+        />
         <ViewerNavigation window={window} canvases={this.canvases} />
       </Fragment>
     );
   }
 }
 
+/**
+ * mapStateToProps - to hook up connect
+ * @memberof WindowViewer
+ * @private
+ */
+const mapStateToProps = state => (
+  {
+    infoResponses: state.infoResponses,
+  }
+);
+
+/**
+ * mapDispatchToProps - used to hook up connect to action creators
+ * @memberof WindowViewer
+ * @private
+ */
+const mapDispatchToProps = dispatch => ({
+  fetchInfoResponse: infoId => dispatch(actions.fetchInfoResponse(infoId)),
+});
+
 WindowViewer.propTypes = {
+  infoResponses: PropTypes.object.isRequired, // eslint-disable-line react/forbid-prop-types
+  fetchInfoResponse: PropTypes.func.isRequired,
   manifest: PropTypes.object.isRequired, // eslint-disable-line react/forbid-prop-types
   window: PropTypes.object.isRequired, // eslint-disable-line react/forbid-prop-types
 };
 
-export default miradorWithPlugins(WindowViewer);
+export default connect(mapStateToProps, mapDispatchToProps)(miradorWithPlugins(WindowViewer));
diff --git a/src/reducers/index.js b/src/reducers/index.js
index e494f7e3834f334b24ade689c0c384231b610564..a52f1c82bd3259b2a501689a603aad622bbe5c2d 100644
--- a/src/reducers/index.js
+++ b/src/reducers/index.js
@@ -3,6 +3,7 @@ import workspaceReducer from './workspace';
 import windowsReducer from './windows';
 import manifestsReducer from './manifests';
 import configReducer from './config';
+import infoResponsesReducer from './infoResponses';
 
 /**
  * Action Creators for Mirador
@@ -14,6 +15,7 @@ const rootReducer = combineReducers({
   windows: windowsReducer,
   manifests: manifestsReducer,
   config: configReducer,
+  infoResponses: infoResponsesReducer,
 });
 
 export default rootReducer;
diff --git a/src/reducers/infoResponses.js b/src/reducers/infoResponses.js
new file mode 100644
index 0000000000000000000000000000000000000000..9954dcb9b4c7ad1480fe09062a0c1ef98cd0591f
--- /dev/null
+++ b/src/reducers/infoResponses.js
@@ -0,0 +1,39 @@
+import ActionTypes from '../action-types';
+
+/**
+ * infoResponsesReducer
+ */
+const infoResponsesReducer = (state = {}, action) => {
+  switch (action.type) {
+    case ActionTypes.REQUEST_INFO_RESPONSE:
+      return Object.assign({}, state, {
+        [action.infoId]: {
+          isFetching: true,
+        },
+      });
+    case ActionTypes.RECEIVE_INFO_RESPONSE:
+      return Object.assign({}, state, {
+        [action.infoId]: {
+          json: action.infoJson,
+          isFetching: false,
+        },
+      });
+    case ActionTypes.RECEIVE_INFO_RESPONSE_FAILURE:
+      return Object.assign({}, state, {
+        [action.infoId]: {
+          error: action.error,
+          isFetching: false,
+        },
+      });
+    case ActionTypes.REMOVE_INFO_RESPONSE:
+      return Object.keys(state).reduce((object, key) => {
+        if (key !== action.infoId) {
+          object[key] = state[key]; // eslint-disable-line no-param-reassign
+        }
+        return object;
+      }, {});
+    default: return state;
+  }
+};
+
+export default infoResponsesReducer;