From d13df2d9a1b2757e34701978d04d99650f09d8a3 Mon Sep 17 00:00:00 2001
From: Jack Reed <mejackreed@users.noreply.github.com>
Date: Fri, 12 Apr 2019 17:56:38 -0600
Subject: [PATCH] Revert "Connect plugins on start. Closes #2411"

---
 .../src/extend/pluginPreprocessing.test.js    | 146 ------------------
 __tests__/src/extend/pluginStore.test.js      |  77 +++++++++
 __tests__/src/extend/withPlugins.test.js      |  70 ++++-----
 __tests__/src/lib/MiradorViewer.test.js       |  27 ++++
 src/extend/index.js                           |   6 +-
 src/extend/pluginContext.js                   |   3 -
 src/extend/pluginPreprocessing.js             |  71 ---------
 src/extend/pluginProvider.js                  |  43 ------
 src/extend/pluginStore.js                     |  71 +++++++++
 src/extend/withPlugins.js                     |  41 +++--
 src/lib/MiradorViewer.js                      |  16 +-
 11 files changed, 244 insertions(+), 327 deletions(-)
 delete mode 100644 __tests__/src/extend/pluginPreprocessing.test.js
 create mode 100644 __tests__/src/extend/pluginStore.test.js
 delete mode 100644 src/extend/pluginContext.js
 delete mode 100644 src/extend/pluginPreprocessing.js
 delete mode 100644 src/extend/pluginProvider.js
 create mode 100644 src/extend/pluginStore.js

diff --git a/__tests__/src/extend/pluginPreprocessing.test.js b/__tests__/src/extend/pluginPreprocessing.test.js
deleted file mode 100644
index d695c53c9..000000000
--- a/__tests__/src/extend/pluginPreprocessing.test.js
+++ /dev/null
@@ -1,146 +0,0 @@
-import {
-  filterValidPlugins,
-  connectPluginsToStore,
-  addPluginReducersToStore,
-  createTargetToPluginMapping,
-} from '../../../src/extend/pluginPreprocessing';
-
-
-describe('filterValidPlugins', () => {
-  it('returns empty array if plugin array is empty', () => {
-    expect(filterValidPlugins([])).toEqual([]);
-  });
-
-  it('returns only valid plugins', () => {
-    const plugins = [
-      {
-        component: props => null,
-        mode: 'add',
-        name: 'valid plugin 1',
-        target: 'Window',
-      },
-      {
-        component: props => null,
-        mode: 'wrap',
-        name: 'valid plugin 2',
-        target: 'Window',
-      },
-      {
-        name: 'invalid Plugin 1',
-      },
-      {
-        name: 'invalid Plugin 2',
-      },
-    ];
-    const result = filterValidPlugins(plugins);
-    expect(result.length).toBe(2);
-    expect(result[0].name).toBe('valid plugin 1');
-    expect(result[1].name).toBe('valid plugin 2');
-  });
-});
-
-describe('createTargetToPluginMapping', () => {
-  it('returns empty object if plugin array is empty', () => {
-    expect(createTargetToPluginMapping([])).toEqual({});
-  });
-
-  it('should create a mapping from targets to plugins and modes', () => {
-    /** */
-    const component = props => null;
-
-    const plugins = [
-      { component, mode: 'wrap', target: 'Window' },
-      { component, mode: 'wrap', target: 'Window' },
-      { component, mode: 'add', target: 'Window' },
-      { component, mode: 'add', target: 'Window' },
-
-      { component, mode: 'wrap', target: 'TopBar' },
-      { component, mode: 'wrap', target: 'TopBar' },
-      { component, mode: 'add', target: 'TopBar' },
-      { component, mode: 'add', target: 'TopBar' },
-    ];
-
-    expect(createTargetToPluginMapping(plugins)).toEqual({
-      TopBar: {
-        add: [
-          { component, mode: 'add', target: 'TopBar' },
-          { component, mode: 'add', target: 'TopBar' },
-        ],
-        wrap: [
-          { component, mode: 'wrap', target: 'TopBar' },
-          { component, mode: 'wrap', target: 'TopBar' },
-        ],
-      },
-      Window: {
-        add: [
-          { component, mode: 'add', target: 'Window' },
-          { component, mode: 'add', target: 'Window' },
-        ],
-        wrap: [
-          { component, mode: 'wrap', target: 'Window' },
-          { component, mode: 'wrap', target: 'Window' },
-        ],
-      },
-    });
-  });
-});
-
-describe('connectPluginsToStore', () => {
-  it('returns empty array if plugin array is empty', () => {
-    expect(filterValidPlugins([])).toEqual([]);
-  });
-
-  it('returns plugins with components connected to store', () => {
-    /** */
-    const ComponentA = props => null;
-    /** */
-    const ComponentB = props => null;
-
-    const plugins = [
-      { component: ComponentA, mode: 'wrap', target: 'Window' },
-      { component: ComponentB, mode: 'add', target: 'TopBar' },
-    ];
-
-    const result = connectPluginsToStore(plugins);
-    expect(result.length).toBe(2);
-    expect(result[0].component.displayName).toBe('Connect(ComponentA)');
-    expect(result[1].component.displayName).toBe('Connect(ComponentB)');
-  });
-});
-
-describe('addPluginReducersToStore', () => {
-  const store = { replaceReducer: jest.fn() };
-  const createRootReducer = jest.fn(pluginReducers => pluginReducers);
-
-  /** */ const fooReducer = x => x;
-  /** */ const barReducer = x => x;
-  /** */ const bazReducer = x => x;
-
-  const plugins = [
-    {
-      component: props => null,
-      mode: 'add',
-      reducers: {
-        bar: barReducer,
-        foo: fooReducer,
-      },
-      target: 'Window',
-    },
-    {
-      component: props => null,
-      mode: 'add',
-      reducers: {
-        baz: bazReducer,
-      },
-      target: 'Window',
-    },
-  ];
-
-  addPluginReducersToStore(store, createRootReducer, plugins);
-  expect(store.replaceReducer.mock.calls.length).toBe(1);
-  expect(store.replaceReducer.mock.calls[0][0]).toEqual({
-    bar: barReducer,
-    baz: bazReducer,
-    foo: fooReducer,
-  });
-});
diff --git a/__tests__/src/extend/pluginStore.test.js b/__tests__/src/extend/pluginStore.test.js
new file mode 100644
index 000000000..fa34f945c
--- /dev/null
+++ b/__tests__/src/extend/pluginStore.test.js
@@ -0,0 +1,77 @@
+import { pluginStore } from '../../../src/extend';
+
+describe('storePlugins()', () => {
+  it('should run without throw error when Array is passed', () => {
+    expect(() => pluginStore.storePlugins([])).not.toThrow();
+  });
+  it('should run without throw error when nothing is passed', () => {
+    expect(() => pluginStore.storePlugins()).not.toThrow();
+  });
+});
+
+describe('getPlugins', () => {
+  it('returns undefined if no plugin for target exist', () => {
+    pluginStore.storePlugins();
+    expect(pluginStore.getPlugins('target')).not.toBeDefined();
+  });
+
+  it('returns mode->plugins mapping for target', () => {
+    /** */
+    const component = x => x;
+
+    const plugins = [
+      { component, mode: 'wrap', target: 'Window' },
+      { component, mode: 'wrap', target: 'Window' },
+      { component, mode: 'add', target: 'Window' },
+      { component, mode: 'add', target: 'Window' },
+
+      { component, mode: 'wrap', target: 'TopBar' },
+      { component, mode: 'wrap', target: 'TopBar' },
+      { component, mode: 'add', target: 'TopBar' },
+      { component, mode: 'add', target: 'TopBar' },
+    ];
+
+    pluginStore.storePlugins(plugins);
+
+    expect(pluginStore.getPlugins('Window')).toEqual({
+      add: [
+        { component, mode: 'add', target: 'Window' },
+        { component, mode: 'add', target: 'Window' },
+      ],
+      wrap: [
+        { component, mode: 'wrap', target: 'Window' },
+        { component, mode: 'wrap', target: 'Window' },
+      ],
+    });
+
+    expect(pluginStore.getPlugins('TopBar')).toEqual({
+      add: [
+        { component, mode: 'add', target: 'TopBar' },
+        { component, mode: 'add', target: 'TopBar' },
+      ],
+      wrap: [
+        { component, mode: 'wrap', target: 'TopBar' },
+        { component, mode: 'wrap', target: 'TopBar' },
+      ],
+    });
+  });
+
+  // see also pluginValidation.test.js
+  it('filter out invalid plugins', () => {
+    /** */
+    const component = x => x;
+
+    const plugins = [
+      { component, mode: 'add', target: 'Window' },
+      { component, mode: 'LURK', target: 'Window' },
+    ];
+
+    pluginStore.storePlugins(plugins);
+
+    expect(pluginStore.getPlugins('Window')).toEqual({
+      add: [
+        { component, mode: 'add', target: 'Window' },
+      ],
+    });
+  });
+});
diff --git a/__tests__/src/extend/withPlugins.test.js b/__tests__/src/extend/withPlugins.test.js
index 08b33c5eb..1e3372b25 100644
--- a/__tests__/src/extend/withPlugins.test.js
+++ b/__tests__/src/extend/withPlugins.test.js
@@ -1,20 +1,20 @@
 import React from 'react';
-import { mount } from 'enzyme';
-import { withPlugins, PluginContext } from '../../../src/extend';
+import { shallow } from 'enzyme';
+import { withPlugins } from '../../../src/extend';
+import { pluginStore } from '../../../src/extend/pluginStore';
 
 
+jest.mock('../../../src/extend/pluginStore');
+
 /** Mock target component */
 const Target = props => <div>Hello</div>;
 
 /** create wrapper  */
-function createPluginHoc(pluginMap) {
+function createPluginHoc(plugins) {
+  pluginStore.getPlugins = () => plugins;
   const props = { bar: 2, foo: 1 };
   const PluginHoc = withPlugins('Target', Target);
-  return mount(
-    <PluginContext.Provider value={pluginMap}>
-      <PluginHoc {...props} />
-    </PluginContext.Provider>,
-  );
+  return shallow(<PluginHoc {...props} />);
 }
 
 describe('withPlugins', () => {
@@ -34,7 +34,7 @@ describe('withPlugins', () => {
 
 describe('PluginHoc: if no plugin exists for the target', () => {
   it('renders the target component', () => {
-    const hoc = createPluginHoc({});
+    const hoc = createPluginHoc([]);
     expect(hoc.find(Target).length).toBe(1);
     expect(hoc.find(Target).props().foo).toBe(1);
     expect(hoc.find(Target).props().bar).toBe(2);
@@ -45,16 +45,14 @@ describe('PluginHoc: if wrap plugins exist for target', () => {
   it('renders the first wrap plugin and passes the target component and the target props to it', () => {
     /** */ const WrapPluginComponentA = props => <div>look i am a plugin</div>;
     /** */ const WrapPluginComponentB = props => <div>look i am a plugin</div>;
-    const pluginMap = {
-      Target: {
-        wrap: [
-          { component: WrapPluginComponentA, mode: 'wrap', target: 'Target' },
-          { component: WrapPluginComponentB, mode: 'wrap', target: 'Target' },
-        ],
-      },
+    const plugins = {
+      wrap: [
+        { component: WrapPluginComponentA, mode: 'wrap', target: 'Target' },
+        { component: WrapPluginComponentB, mode: 'wrap', target: 'Target' },
+      ],
     };
-    const hoc = createPluginHoc(pluginMap);
-    const selector = 'WrapPluginComponentA';
+    const hoc = createPluginHoc(plugins);
+    const selector = 'Connect(WrapPluginComponentA)';
     expect(hoc.find(selector).length).toBe(1);
     expect(hoc.find(selector).props().TargetComponent).toBe(Target);
     expect(hoc.find(selector).props().targetProps).toEqual({ bar: 2, foo: 1 });
@@ -66,20 +64,20 @@ describe('PluginHoc: if add plugins exist but no wrap plugin', () => {
     /** */ const AddPluginComponentA = props => <div>look i am a plugin</div>;
     /** */ const AddPluginComponentB = props => <div>look i am a plugin</div>;
     const plugins = {
-      Target: {
-        add: [
-          { component: AddPluginComponentA, mode: 'add', target: 'Target' },
-          { component: AddPluginComponentB, mode: 'add', target: 'Target' },
-        ],
-      },
+      add: [
+        { component: AddPluginComponentA, mode: 'add', target: 'Target' },
+        { component: AddPluginComponentB, mode: 'add', target: 'Target' },
+      ],
     };
     const hoc = createPluginHoc(plugins);
     const selector = Target;
     expect(hoc.find(selector).length).toBe(1);
     expect(hoc.find(selector).props().foo).toBe(1);
     expect(hoc.find(selector).props().bar).toBe(2);
-    expect(hoc.find(selector).props().PluginComponents[0]).toBe(AddPluginComponentA);
-    expect(hoc.find(selector).props().PluginComponents[1]).toBe(AddPluginComponentB);
+    expect(hoc.find(selector).props().PluginComponents[0].displayName)
+      .toBe('Connect(AddPluginComponentA)');
+    expect(hoc.find(selector).props().PluginComponents[1].displayName)
+      .toBe('Connect(AddPluginComponentB)');
   });
 });
 
@@ -90,19 +88,17 @@ describe('PluginHoc: if wrap plugins AND add plugins exist for target', () => {
     /** */ const AddPluginComponentA = props => <div>look i am a plugin</div>;
     /** */ const AddPluginComponentB = props => <div>look i am a plugin</div>;
     const plugins = {
-      Target: {
-        add: [
-          { component: AddPluginComponentA, mode: 'add', target: 'Target' },
-          { component: AddPluginComponentB, mode: 'add', target: 'Target' },
-        ],
-        wrap: [
-          { component: WrapPluginComponentA, mode: 'wrap', target: 'Target' },
-          { component: WrapPluginComponentB, mode: 'wrap', target: 'Target' },
-        ],
-      },
+      add: [
+        { component: AddPluginComponentA, mode: 'add', target: 'Target' },
+        { component: AddPluginComponentB, mode: 'add', target: 'Target' },
+      ],
+      wrap: [
+        { component: WrapPluginComponentA, mode: 'wrap', target: 'Target' },
+        { component: WrapPluginComponentB, mode: 'wrap', target: 'Target' },
+      ],
     };
     const hoc = createPluginHoc(plugins);
-    expect(hoc.find(WrapPluginComponentA).length).toBe(1);
+    expect(hoc.find('Connect(WrapPluginComponentA)').length).toBe(1);
     expect(hoc.find(Target).length).toBe(0);
   });
 });
diff --git a/__tests__/src/lib/MiradorViewer.test.js b/__tests__/src/lib/MiradorViewer.test.js
index 0df7c0e0a..b8592e40c 100644
--- a/__tests__/src/lib/MiradorViewer.test.js
+++ b/__tests__/src/lib/MiradorViewer.test.js
@@ -1,7 +1,9 @@
 import ReactDOM from 'react-dom';
+import { pluginStore } from '../../../src/extend/pluginStore';
 import MiradorViewer from '../../../src/lib/MiradorViewer';
 
 jest.unmock('react-i18next');
+jest.mock('../../../src/extend/pluginStore');
 jest.mock('react-dom');
 
 describe('MiradorViewer', () => {
@@ -21,6 +23,31 @@ describe('MiradorViewer', () => {
       expect(ReactDOM.render).toHaveBeenCalled();
     });
   });
+  describe('process plugins', () => {
+    it('should store plugins and set reducers to state', () => {
+      /** */ const fooReducer = (state = 0) => state;
+      /** */ const barReducer = (state = 0) => state;
+      /** */ const bazReducer = (state = 0) => state;
+      /** */ const plugins = [
+        {
+          reducers: {
+            bar: barReducer,
+            foo: fooReducer,
+          },
+        },
+        {
+          reducers: {
+            baz: bazReducer,
+          },
+        },
+      ];
+      instance = new MiradorViewer({}, plugins);
+      expect(pluginStore.storePlugins).toBeCalledWith(plugins);
+      expect(instance.store.getState().foo).toBeDefined();
+      expect(instance.store.getState().bar).toBeDefined();
+      expect(instance.store.getState().baz).toBeDefined();
+    });
+  });
   describe('processConfig', () => {
     it('transforms config values to actions to dispatch to store', () => {
       instance = new MiradorViewer({
diff --git a/src/extend/index.js b/src/extend/index.js
index 18c4065c5..d534dcaff 100644
--- a/src/extend/index.js
+++ b/src/extend/index.js
@@ -1,5 +1,3 @@
-export * from './pluginContext';
-export * from './pluginProvider';
-export * from './pluginPreprocessing';
-export * from './pluginValidation';
+export * from './pluginStore';
 export * from './withPlugins';
+export * from './pluginValidation';
diff --git a/src/extend/pluginContext.js b/src/extend/pluginContext.js
deleted file mode 100644
index 8e4d87459..000000000
--- a/src/extend/pluginContext.js
+++ /dev/null
@@ -1,3 +0,0 @@
-import React from 'react';
-
-export const PluginContext = React.createContext();
diff --git a/src/extend/pluginPreprocessing.js b/src/extend/pluginPreprocessing.js
deleted file mode 100644
index 6bed9602b..000000000
--- a/src/extend/pluginPreprocessing.js
+++ /dev/null
@@ -1,71 +0,0 @@
-import update from 'lodash/update';
-import { connect } from 'react-redux';
-import { validatePlugin } from '.';
-
-/**
- * Returns a mapping from targets to plugins and modes
- *
- * @param {Array} plugins
- * @return {Object} - looks like:
- *
- *  {
- *    'WorkspacePanel': {
- *      wrap:     [plugin3, ...],
- *      add:      [plugin4, ...],
- *    },
- *    ...
- *  }
- */
-export function createTargetToPluginMapping(plugins) {
-  return plugins.reduce((map, plugin) => (
-    update(map, [plugin.target, plugin.mode], x => [...x || [], plugin])
-  ), {});
-}
-
-/** */
-export function filterValidPlugins(plugins) {
-  const { validPlugins, invalidPlugins } = splitPluginsByValidation(plugins);
-  logInvalidPlugins(invalidPlugins);
-  return validPlugins;
-}
-
-/** */
-export function connectPluginsToStore(plugins) {
-  return plugins.map(plugin => (
-    { ...plugin, component: connectPluginComponent(plugin) }
-  ));
-}
-
-/** */
-export function addPluginReducersToStore(store, createRootReducer, plugins) {
-  const pluginReducers = getReducersFromPlugins(plugins);
-  store.replaceReducer(createRootReducer(pluginReducers));
-}
-
-/** */
-function splitPluginsByValidation(plugins) {
-  const splittedPlugins = { invalidPlugins: [], validPlugins: [] };
-  plugins.forEach(plugin => (
-    validatePlugin(plugin)
-      ? splittedPlugins.validPlugins.push(plugin)
-      : splittedPlugins.invalidPlugins.push(plugin)
-  ));
-  return splittedPlugins;
-}
-
-/** */
-function logInvalidPlugins(plugins) {
-  plugins.forEach(plugin => (
-    console.log(`Mirador: Plugin ${plugin.name} is not valid and was rejected.`)
-  ));
-}
-
-/** Connect plugin component to state */
-function connectPluginComponent(plugin) {
-  return connect(plugin.mapStateToProps, plugin.mapDispatchToProps)(plugin.component);
-}
-
-/**  */
-function getReducersFromPlugins(plugins) {
-  return plugins && plugins.reduce((acc, plugin) => ({ ...acc, ...plugin.reducers }), {});
-}
diff --git a/src/extend/pluginProvider.js b/src/extend/pluginProvider.js
deleted file mode 100644
index 73d2739b0..000000000
--- a/src/extend/pluginProvider.js
+++ /dev/null
@@ -1,43 +0,0 @@
-import React, { useContext, useEffect, useState } from 'react';
-import PropTypes from 'prop-types';
-import { ReactReduxContext } from 'react-redux';
-import {
-  PluginContext,
-  filterValidPlugins,
-  addPluginReducersToStore,
-  connectPluginsToStore,
-  createTargetToPluginMapping,
-} from '.';
-
-
-/**  */
-export function PluginProvider(props) {
-  const { store } = useContext(ReactReduxContext);
-  const { plugins, createRootReducer, children } = props;
-  const [pluginMap, setPluginMap] = useState({});
-
-  useEffect(() => {
-    const validPlugins = filterValidPlugins(plugins);
-    const connectedPlugins = connectPluginsToStore(validPlugins);
-    createRootReducer && addPluginReducersToStore(store, createRootReducer, validPlugins);
-    setPluginMap(createTargetToPluginMapping(connectedPlugins));
-  }, []);
-
-  return (
-    <PluginContext.Provider value={pluginMap}>
-      { children }
-    </PluginContext.Provider>
-  );
-}
-
-PluginProvider.propTypes = {
-  children: PropTypes.node,
-  createRootReducer: PropTypes.func,
-  plugins: PropTypes.array, // eslint-disable-line react/forbid-prop-types
-};
-
-PluginProvider.defaultProps = {
-  children: null,
-  createRootReducer: null,
-  plugins: [],
-};
diff --git a/src/extend/pluginStore.js b/src/extend/pluginStore.js
new file mode 100644
index 000000000..0b191b3dd
--- /dev/null
+++ b/src/extend/pluginStore.js
@@ -0,0 +1,71 @@
+import update from 'lodash/update';
+import { validatePlugin } from '.';
+
+export const pluginStore = {
+  /**
+  * Get plugins for target
+  *
+  * @param {String} targetName
+  * @return {Object | undefined } - looks like:
+  *
+  *  {
+  *    wrap:     [plugin1, ...],
+  *    add:      [plugin2, ...],
+  *  }
+  */
+  getPlugins(target) {
+    return this.pluginMap[target];
+  },
+  /**
+   * Store Plugins
+   *
+   * @param {Array} plugins
+   */
+  storePlugins(plugins = []) {
+    const { validPlugins, invalidPlugins } = filterPlugins(plugins);
+    logInvalidPlugins(invalidPlugins);
+    this.pluginMap = mapPlugins(validPlugins);
+  },
+};
+
+/**
+ * Returns a mapping from plugins to targets and modes
+ *
+ * @param {Array} plugins
+ * @return {Object} - looks like:
+ *
+ *
+ *  {
+ *    'WorkspacePanel': {
+ *      wrap:     [plugin3, ...],
+ *      add:      [plugin4, ...],
+ *    },
+ *    'Window': {
+ *      wrap:     [plugin3, ...],
+ *      add:      [plugin4, ...],
+ *    }
+ *  }
+ */
+function mapPlugins(plugins) {
+  return plugins.reduce((map, plugin) => (
+    update(map, [plugin.target, plugin.mode], x => [...x || [], plugin])
+  ), {});
+}
+
+/** */
+function filterPlugins(plugins) {
+  const filteredPlugins = { invalidPlugins: [], validPlugins: [] };
+  plugins.forEach(plugin => (
+    validatePlugin(plugin)
+      ? filteredPlugins.validPlugins.push(plugin)
+      : filteredPlugins.invalidPlugins.push(plugin)
+  ));
+  return filteredPlugins;
+}
+
+/** */
+function logInvalidPlugins(plugins) {
+  plugins.forEach(plugin => (
+    console.log(`Mirador: Plugin ${plugin.name} is not valid and was rejected.`)
+  ));
+}
diff --git a/src/extend/withPlugins.js b/src/extend/withPlugins.js
index 8d78d0ef0..2f7973481 100644
--- a/src/extend/withPlugins.js
+++ b/src/extend/withPlugins.js
@@ -1,28 +1,30 @@
-import React, { useContext } from 'react';
+import React, { Component } from 'react';
 import curry from 'lodash/curry';
 import isEmpty from 'lodash/isEmpty';
-import { PluginContext } from '.';
-
+import { connect } from 'react-redux';
+import { pluginStore } from '.';
 
 /** withPlugins should be the innermost HOC */
 function _withPlugins(targetName, TargetComponent) { // eslint-disable-line no-underscore-dangle
-  /** */
-  function PluginHoc(props) {
-    const pluginMap = useContext(PluginContext);
-    const plugins = pluginMap[targetName];
+  /** plugin wrapper hoc */
+  class PluginHoc extends Component {
+    /** render */
+    render() { // eslint-disable-line consistent-return
+      const plugins = pluginStore.getPlugins(targetName);
 
-    if (isEmpty(plugins)) {
-      return <TargetComponent {...props} />;
-    }
+      if (isEmpty(plugins)) {
+        return <TargetComponent {...this.props} />;
+      }
 
-    if (!isEmpty(plugins.wrap)) {
-      const PluginComponent = plugins.wrap[0].component;
-      return <PluginComponent targetProps={props} TargetComponent={TargetComponent} />;
-    }
+      if (!isEmpty(plugins.wrap)) {
+        const WrapPluginComponent = connectPluginComponent(plugins.wrap[0]);
+        return <WrapPluginComponent targetProps={this.props} TargetComponent={TargetComponent} />;
+      }
 
-    if (!isEmpty(plugins.add)) {
-      const PluginComponents = plugins.add.map(plugin => plugin.component);
-      return <TargetComponent {...props} PluginComponents={PluginComponents} />;
+      if (!isEmpty(plugins.add)) {
+        const AddPluginComponents = plugins.add.map(plugin => connectPluginComponent(plugin));
+        return <TargetComponent {...this.props} PluginComponents={AddPluginComponents} />;
+      }
     }
   }
 
@@ -30,5 +32,10 @@ function _withPlugins(targetName, TargetComponent) { // eslint-disable-line no-u
   return PluginHoc;
 }
 
+/** Connect plugin component to state */
+function connectPluginComponent(plugin) {
+  return connect(plugin.mapStateToProps, plugin.mapDispatchToProps)(plugin.component);
+}
+
 /** withPlugins('MyComponent')(MyComponent) */
 export const withPlugins = curry(_withPlugins);
diff --git a/src/lib/MiradorViewer.js b/src/lib/MiradorViewer.js
index 1cbcb7e04..91c5544a7 100644
--- a/src/lib/MiradorViewer.js
+++ b/src/lib/MiradorViewer.js
@@ -2,10 +2,9 @@ import React from 'react';
 import ReactDOM from 'react-dom';
 import { Provider } from 'react-redux';
 import deepmerge from 'deepmerge';
-import { PluginProvider } from '../extend';
 import App from '../containers/App';
+import { pluginStore } from '../extend';
 import createStore from '../state/createStore';
-import createRootReducer from '../state/reducers/rootReducer';
 import * as actions from '../state/actions';
 import settings from '../config/settings';
 
@@ -16,7 +15,9 @@ class MiradorViewer {
   /**
    */
   constructor(config, plugins) {
-    this.store = createStore();
+    pluginStore.storePlugins(plugins);
+    const pluginReducers = getReducersFromPlugins(plugins);
+    this.store = createStore(pluginReducers);
     this.config = config;
     this.processConfig();
     const viewer = {
@@ -26,9 +27,7 @@ class MiradorViewer {
 
     ReactDOM.render(
       <Provider store={this.store}>
-        <PluginProvider plugins={plugins} createRootReducer={createRootReducer}>
-          <App />
-        </PluginProvider>
+        <App />
       </Provider>,
       document.getElementById(config.id),
     );
@@ -74,4 +73,9 @@ class MiradorViewer {
   }
 }
 
+/** Return reducers from plugins */
+function getReducersFromPlugins(plugins) {
+  return plugins && plugins.reduce((acc, plugin) => ({ ...acc, ...plugin.reducers }), {});
+}
+
 export default MiradorViewer;
-- 
GitLab