M3 Keep store connected components in separate files - ProjectMirador/mirador GitHub Wiki

Motivation

Mirador 3 is an application based on React and Redux. We use the react-redux library to bind this two frameworks together. In particular we usually use the connect() function of react-redux to wrap our components into higher order components that wire those components to the Redux store. I.e. from a technical perspective we are dealing with two different kinds of components. Those that have access to the data of the Redux store and those that do not.

There are reasons to treat these two classes of components differently and keep them in separate files. Most important is testing. It is easier when we can simply import an unconnected component in our unit tests and mock all needed properties rather than import the connected version and keep track of which properties are already injected by the higher order component and which are not. In turn, when we unit test the connected components we only need to test that the right properties where passed to the unconnected (wrapped) version. Also, when a test fails it's a little easier to trace the bug as we can better localize the problem.

In addition, when we keep these two kinds of components in separate files, we increase readability and reusability, we and follow the principle of separation of concerns.

Example

Our pure, unconnected components are placed in the src/component/ folder:

src/component/ManifestList.js

class ManifestList extends React.Component {
  render() {
    return (
      <h1>{ this.props.someInfo }
      <ul>
        { this.props.manifests.map(manifest) => <li>{ manifest.id }</li> }
      </ul>
    );
  }
}

ManifestList.propTypes = {
  // will be passed via 'connect()', see below
  manifests: PropTypes.object.isRequired,
  // will be passed via parent component, see below
  someInfo: PropTypes.string.isRequired,
};

export default ManifestList;

The connected components are placed in the src/containers/ folder. The files are exactly named as the unconnected components they wrap.

src/containers/ManifestList.js

import { connect } from 'react-redux';
import ManifestList from '../components/ManifestList';

const mapStateToProps = state => {
  return {
    manifests: state.manifests,
  }
}

export default connect(mapStateToProps)(ManifestList);

In other files we can now choose to import the connected or the unconnected version. In the application code we usually want the connected one, so we import it from the src/containers folder.

src/components/Overview.js

import ManifestList from '../containers/ManifestList';

class Overview extends React.Component {
  render() {
    return (
        <div>
          // ...
          <ManifestList someInfo="That's a very impressive list!" />
        </div>
    );
  }
}

// ...

The same applies to unit tests: If we want to test the unconnected component we import from src/component/ otherwise from src/containers/.

Problems

  • The drawbacks of this approach is that we usually have to handle two files when working on a component. Also we will often have to scroll to the top of a file to figure out where the component was imported from.

  • Beside the connect() function there are other cases where we wrap a component in a higher order component, e.g. withStyle() from Material UI or withPlugins() from Mirador's plugin system. So far it's not clear how the use of this functions fits to the described approach. The recommendation for now is to think about the reason for the respective higher order component. If it changes how things look (e.g. withStyle()) than use it within the src/component/ files, if it changes how things work (e.g. withPlugins()) than use it within the src/containers/ files.

⚠️ **GitHub.com Fallback** ⚠️