From f78eb4405b0ebb945e50d90f7c44294298d97739 Mon Sep 17 00:00:00 2001
From: Tamlyn Rhodes <tamlyn@tamlyn.org>
Date: Mon, 27 Nov 2017 22:42:17 +0000
Subject: [PATCH] Convert to GraphQL with Apollo Client

Remove redundant actions and reducers
Add withLoader helper to handle component loading state
---
 config/default.js                         |   5 -
 config/test.js                            |   5 +-
 package.json                              |   5 +
 src/actions/collections.js                | 244 ----------------------
 src/actions/currentUser.js                |  33 ---
 src/actions/fileUpload.js                 |  48 -----
 src/actions/fragments.js                  | 211 -------------------
 src/actions/index.js                      |  36 +---
 src/actions/teams.js                      | 159 --------------
 src/actions/users.js                      | 112 ----------
 src/components/AuthenticatedComponent.jsx |  49 ++---
 src/components/Root.js                    |  28 ++-
 src/components/UpdateSubscriber.jsx       |   2 +-
 src/helpers/Authorize.jsx                 |  27 +--
 src/helpers/Utils.js                      |   5 -
 src/helpers/api.js                        |  14 --
 src/helpers/endpoint.js                   |   3 -
 src/helpers/token.js                      |   7 -
 src/helpers/withAuthsome.js               |  63 +++---
 src/helpers/withLoader.js                 |  21 ++
 src/index.js                              |   1 -
 src/reducers/collections.js               | 130 ------------
 src/reducers/currentUser.js               |  48 -----
 src/reducers/error.js                     |   9 -
 src/reducers/fileUpload.js                |  24 ---
 src/reducers/fragments.js                 |  80 -------
 src/reducers/index.js                     |  17 --
 src/reducers/teams.js                     |  44 ----
 src/reducers/users.js                     |  72 -------
 src/store/configureStore.js               |  14 --
 src/validations.js                        |   4 +-
 test/actions/collections.test.js          | 120 -----------
 test/actions/currentUser.test.js          |  20 --
 test/actions/fileUpload.test.js           |  32 ---
 test/actions/fragments.test.js            | 105 ----------
 test/actions/index.test.js                |  14 --
 test/actions/teams.test.js                |  85 --------
 test/actions/users.test.js                |  54 -----
 test/helpers/api.test.js                  | 118 -----------
 test/helpers/describeAction.js            | 128 ------------
 test/reducers/collections.test.js         | 116 ----------
 test/reducers/currentUser.test.js         |  73 -------
 test/reducers/error.test.js               |  22 --
 test/reducers/fileUpload.test.js          |  36 ----
 test/reducers/fragments.test.js           |  97 ---------
 test/reducers/teams.test.js               |  70 -------
 test/reducers/users.test.js               | 109 ----------
 yarn.lock                                 | 122 ++++++++++-
 48 files changed, 248 insertions(+), 2593 deletions(-)
 delete mode 100644 config/default.js
 delete mode 100644 src/actions/collections.js
 delete mode 100644 src/actions/currentUser.js
 delete mode 100644 src/actions/fileUpload.js
 delete mode 100644 src/actions/fragments.js
 delete mode 100644 src/actions/teams.js
 delete mode 100644 src/actions/users.js
 delete mode 100644 src/helpers/Utils.js
 delete mode 100644 src/helpers/endpoint.js
 delete mode 100644 src/helpers/token.js
 create mode 100644 src/helpers/withLoader.js
 delete mode 100644 src/reducers/collections.js
 delete mode 100644 src/reducers/currentUser.js
 delete mode 100644 src/reducers/error.js
 delete mode 100644 src/reducers/fileUpload.js
 delete mode 100644 src/reducers/fragments.js
 delete mode 100644 src/reducers/index.js
 delete mode 100644 src/reducers/teams.js
 delete mode 100644 src/reducers/users.js
 delete mode 100644 test/actions/collections.test.js
 delete mode 100644 test/actions/currentUser.test.js
 delete mode 100644 test/actions/fileUpload.test.js
 delete mode 100644 test/actions/fragments.test.js
 delete mode 100644 test/actions/index.test.js
 delete mode 100644 test/actions/teams.test.js
 delete mode 100644 test/actions/users.test.js
 delete mode 100644 test/helpers/api.test.js
 delete mode 100644 test/helpers/describeAction.js
 delete mode 100644 test/reducers/collections.test.js
 delete mode 100644 test/reducers/currentUser.test.js
 delete mode 100644 test/reducers/error.test.js
 delete mode 100644 test/reducers/fileUpload.test.js
 delete mode 100644 test/reducers/fragments.test.js
 delete mode 100644 test/reducers/teams.test.js
 delete mode 100644 test/reducers/users.test.js

diff --git a/config/default.js b/config/default.js
deleted file mode 100644
index 3e1cd0b..0000000
--- a/config/default.js
+++ /dev/null
@@ -1,5 +0,0 @@
-module.exports = {
-  'pubsweet-client': {
-    API_ENDPOINT: 'http://localhost:3000/api',
-  },
-}
diff --git a/config/test.js b/config/test.js
index bf905ac..b25a093 100644
--- a/config/test.js
+++ b/config/test.js
@@ -1,11 +1,12 @@
 module.exports = {
   'pubsweet-client': {
-    API_ENDPOINT: 'http://example.com',
+    // this should be a relative URL but tests use node-fetch which requires absolute URL
+    API_ENDPOINT: '/graphql',
     'update-subscriber': {
       visible: true,
     },
   },
   authsome: {
-    mode: 'fake-mode'
+    mode: 'fake-mode',
   },
 }
diff --git a/package.json b/package.json
index bdcff00..fa3b17c 100644
--- a/package.json
+++ b/package.json
@@ -16,11 +16,15 @@
   "author": "Collaborative Knowledge Foundation",
   "license": "MIT",
   "dependencies": {
+    "apollo-client-preset": "^1.0.3",
+    "apollo-link": "^1.0.3",
     "authsome": "0.0.9",
     "config": "^1.21.0",
     "eslint-config-prettier": "^2.6.0",
     "event-source-polyfill": "^0.0.10",
     "global": "^4.3.1",
+    "graphql": "^0.11.7",
+    "graphql-tag": "^2.5.0",
     "husky": "^0.14.3",
     "isomorphic-fetch": "^2.1.1",
     "lint-staged": "^4.2.3",
@@ -29,6 +33,7 @@
     "prop-types": "^15.5.8",
     "pubsweet-component-login": "^0.5.2",
     "react": "^15.4.4",
+    "react-apollo": "^2.0.1",
     "react-css-themr": "^2.1.2",
     "react-redux": "^5.0.2",
     "react-router-dom": "^4.2.2",
diff --git a/src/actions/collections.js b/src/actions/collections.js
deleted file mode 100644
index 2a06e40..0000000
--- a/src/actions/collections.js
+++ /dev/null
@@ -1,244 +0,0 @@
-import * as api from '../helpers/api'
-import * as T from './types'
-
-const collectionUrl = (collection, suffix) => {
-  let url = '/collections'
-
-  if (collection) url += `/${collection.id}`
-
-  if (suffix) url += `/${suffix}`
-
-  return url
-}
-
-function getCollectionsRequest() {
-  return {
-    type: T.GET_COLLECTIONS_REQUEST,
-  }
-}
-
-function getCollectionsFailure(error) {
-  return {
-    type: T.GET_COLLECTIONS_FAILURE,
-    error: error,
-  }
-}
-
-function getCollectionsSuccess(collections) {
-  return {
-    type: T.GET_COLLECTIONS_SUCCESS,
-    collections: collections,
-    receivedAt: Date.now(),
-  }
-}
-
-export function getCollections(options) {
-  return dispatch => {
-    dispatch(getCollectionsRequest())
-
-    let url = collectionUrl()
-
-    if (options && options.fields) {
-      url += '?fields=' + encodeURIComponent(options.fields.join(','))
-    }
-
-    return api
-      .get(url)
-      .then(
-        collections => dispatch(getCollectionsSuccess(collections)),
-        err => dispatch(getCollectionsFailure(err)),
-      )
-  }
-}
-
-function getCollectionTeamsRequest() {
-  return {
-    type: T.GET_COLLECTION_TEAMS_REQUEST,
-  }
-}
-
-function getCollectionTeamsFailure(error) {
-  return {
-    type: T.GET_COLLECTION_TEAMS_FAILURE,
-    error: error,
-  }
-}
-
-function getCollectionTeamsSuccess(teams) {
-  return {
-    type: T.GET_COLLECTION_TEAMS_SUCCESS,
-    teams,
-    receivedAt: Date.now(),
-  }
-}
-
-export function getCollectionTeams(collection) {
-  return dispatch => {
-    dispatch(getCollectionTeamsRequest())
-
-    let url = collectionUrl(collection, 'teams')
-
-    return api
-      .get(url)
-      .then(
-        teams => dispatch(getCollectionTeamsSuccess(teams)),
-        err => dispatch(getCollectionTeamsFailure(err)),
-      )
-  }
-}
-
-function createCollectionRequest(collection) {
-  return {
-    type: T.CREATE_COLLECTION_REQUEST,
-    collection: collection,
-  }
-}
-
-function createCollectionSuccess(collection) {
-  return {
-    type: T.CREATE_COLLECTION_SUCCESS,
-    collection: collection,
-  }
-}
-
-function createCollectionFailure(collection, error) {
-  return {
-    type: T.CREATE_COLLECTION_FAILURE,
-    isFetching: false,
-    collection: collection,
-    error: error,
-  }
-}
-
-export function createCollection(collection) {
-  return dispatch => {
-    dispatch(createCollectionRequest(collection))
-
-    const url = collectionUrl()
-
-    return api
-      .create(url, collection)
-      .then(
-        collection => dispatch(createCollectionSuccess(collection)),
-        err => dispatch(createCollectionFailure(collection, err)),
-      )
-  }
-}
-
-function getCollectionRequest(collection) {
-  return {
-    type: T.GET_COLLECTION_REQUEST,
-    collection: collection,
-  }
-}
-
-function getCollectionSuccess(collection) {
-  return {
-    type: T.GET_COLLECTION_SUCCESS,
-    collection: collection,
-    receivedAt: Date.now(),
-  }
-}
-
-function getCollectionFailure(collection, error) {
-  return {
-    type: T.GET_COLLECTION_FAILURE,
-    isFetching: false,
-    collection: collection,
-    error: error,
-  }
-}
-
-export function getCollection(collection) {
-  return dispatch => {
-    dispatch(getCollectionRequest(collection))
-
-    const url = collectionUrl(collection)
-
-    return api
-      .get(url)
-      .then(
-        collection => dispatch(getCollectionSuccess(collection)),
-        err => dispatch(getCollectionFailure(collection, err)),
-      )
-  }
-}
-
-function updateCollectionRequest(collection) {
-  return {
-    type: T.UPDATE_COLLECTION_REQUEST,
-    collection: collection,
-  }
-}
-
-function updateCollectionSuccess(collection, update) {
-  return {
-    type: T.UPDATE_COLLECTION_SUCCESS,
-    collection: collection,
-    update: update,
-    receivedAt: Date.now(),
-  }
-}
-
-function updateCollectionFailure(collection, error) {
-  return {
-    type: T.UPDATE_COLLECTION_FAILURE,
-    isFetching: false,
-    collection: collection,
-    error: error,
-  }
-}
-
-export function updateCollection(collection) {
-  return dispatch => {
-    dispatch(updateCollectionRequest(collection))
-
-    const url = collectionUrl(collection)
-
-    return api
-      .update(url, collection)
-      .then(
-        update => dispatch(updateCollectionSuccess(collection, update)),
-        err => dispatch(updateCollectionFailure(collection, err)),
-      )
-  }
-}
-
-function deleteCollectionRequest(collection) {
-  return {
-    type: T.DELETE_COLLECTION_REQUEST,
-    collection: collection,
-    update: { deleted: true },
-  }
-}
-
-function deleteCollectionSuccess(collection) {
-  return {
-    type: T.DELETE_COLLECTION_SUCCESS,
-    collection: collection,
-  }
-}
-
-function deleteCollectionFailure(collection, error) {
-  return {
-    type: T.DELETE_COLLECTION_FAILURE,
-    collection: collection,
-    update: { deleted: undefined },
-    error: error,
-  }
-}
-
-export function deleteCollection(collection) {
-  return dispatch => {
-    dispatch(deleteCollectionRequest(collection))
-
-    const url = collectionUrl(collection)
-
-    return api
-      .remove(url)
-      .then(
-        () => dispatch(deleteCollectionSuccess(collection)),
-        err => dispatch(deleteCollectionFailure(collection, err)),
-      )
-  }
-}
diff --git a/src/actions/currentUser.js b/src/actions/currentUser.js
deleted file mode 100644
index a3a08b0..0000000
--- a/src/actions/currentUser.js
+++ /dev/null
@@ -1,33 +0,0 @@
-import * as api from '../helpers/api'
-import * as T from './types'
-
-function getCurrentUserRequest() {
-  return {
-    type: T.GET_CURRENT_USER_REQUEST,
-  }
-}
-
-function getCurrentUserSuccess(user) {
-  return {
-    type: T.GET_CURRENT_USER_SUCCESS,
-    user,
-  }
-}
-
-function getCurrentUserFailure(error) {
-  return {
-    type: T.GET_CURRENT_USER_FAILURE,
-    error,
-  }
-}
-
-export function getCurrentUser() {
-  return dispatch => {
-    dispatch(getCurrentUserRequest())
-
-    return api
-      .get('/users/authenticate')
-      .then(user => dispatch(getCurrentUserSuccess(user)))
-      .catch(err => dispatch(getCurrentUserFailure(err)))
-  }
-}
diff --git a/src/actions/fileUpload.js b/src/actions/fileUpload.js
deleted file mode 100644
index ee67814..0000000
--- a/src/actions/fileUpload.js
+++ /dev/null
@@ -1,48 +0,0 @@
-import request from '../helpers/api'
-import * as T from './types'
-
-function fileUploadRequest() {
-  return {
-    type: T.FILE_UPLOAD_REQUEST,
-    isFetching: true,
-  }
-}
-
-function fileUploadSuccess(file) {
-  return {
-    type: T.FILE_UPLOAD_SUCCESS,
-    isFetching: false,
-    file: file,
-  }
-}
-
-function fileUploadFailure(message) {
-  return {
-    type: T.FILE_UPLOAD_FAILURE,
-    isFetching: false,
-    error: message,
-  }
-}
-
-export function fileUpload(file) {
-  return dispatch => {
-    dispatch(fileUploadRequest())
-
-    const data = new FormData()
-    data.append('file', file)
-
-    let opts = {
-      method: 'POST',
-      headers: {
-        Accept: 'text/plain', // the response is a URL
-        // TODO: set the Location header of the response instead
-      },
-      body: data,
-    }
-
-    return request('/upload', opts).then(
-      file => dispatch(fileUploadSuccess(file)),
-      err => dispatch(fileUploadFailure(err)),
-    )
-  }
-}
diff --git a/src/actions/fragments.js b/src/actions/fragments.js
deleted file mode 100644
index 0f9197b..0000000
--- a/src/actions/fragments.js
+++ /dev/null
@@ -1,211 +0,0 @@
-import * as api from '../helpers/api'
-import * as T from './types'
-
-export const fragmentUrl = (collection, fragment) => {
-  let url = ''
-  if (collection) url += `/collections/${collection.id}`
-  url += '/fragments'
-  if (fragment && fragment.id) url += `/${fragment.id}`
-
-  return url
-}
-
-function getFragmentsRequest(collection) {
-  return {
-    type: T.GET_FRAGMENTS_REQUEST,
-    collection: collection,
-  }
-}
-
-function getFragmentsSuccess(collection, fragments) {
-  return {
-    type: T.GET_FRAGMENTS_SUCCESS,
-    collection: collection,
-    fragments: fragments,
-    receivedAt: Date.now(),
-  }
-}
-
-function getFragmentsFailure(error) {
-  return {
-    type: T.GET_FRAGMENTS_FAILURE,
-    error: error,
-  }
-}
-
-export function getFragments(collection, options) {
-  return dispatch => {
-    dispatch(getFragmentsRequest(collection))
-
-    let url = fragmentUrl(collection)
-
-    if (options && options.fields) {
-      url += '?fields=' + encodeURIComponent(options.fields.join(','))
-    }
-
-    return api
-      .get(url)
-      .then(
-        fragments => dispatch(getFragmentsSuccess(collection, fragments)),
-        err => dispatch(getFragmentsFailure(err)),
-      )
-  }
-}
-
-function createFragmentRequest(fragment) {
-  return {
-    type: T.CREATE_FRAGMENT_REQUEST,
-    fragment: fragment,
-  }
-}
-
-function createFragmentSuccess(collection, fragment) {
-  return {
-    type: T.CREATE_FRAGMENT_SUCCESS,
-    collection: collection,
-    fragment: fragment,
-  }
-}
-
-function createFragmentFailure(fragment, error) {
-  return {
-    type: T.CREATE_FRAGMENT_FAILURE,
-    isFetching: false,
-    fragment: fragment,
-    error: error,
-  }
-}
-
-export function createFragment(collection, fragment) {
-  return dispatch => {
-    dispatch(createFragmentRequest(fragment))
-
-    const url = fragmentUrl(collection, fragment)
-
-    return api
-      .create(url, fragment)
-      .then(
-        fragment => dispatch(createFragmentSuccess(collection, fragment)),
-        err => dispatch(createFragmentFailure(fragment, err)),
-      )
-  }
-}
-
-function getFragmentRequest(fragment) {
-  return {
-    type: T.GET_FRAGMENT_REQUEST,
-    fragment: fragment,
-  }
-}
-
-function getFragmentSuccess(fragment) {
-  return {
-    type: T.GET_FRAGMENT_SUCCESS,
-    fragment: fragment,
-    receivedAt: Date.now(),
-  }
-}
-
-function getFragmentFailure(fragment, error) {
-  return {
-    type: T.GET_FRAGMENT_FAILURE,
-    isFetching: false,
-    fragment: fragment,
-    error: error,
-  }
-}
-
-export function getFragment(collection, fragment) {
-  return dispatch => {
-    dispatch(getFragmentRequest(fragment))
-
-    const url = fragmentUrl(collection, fragment)
-
-    return api
-      .get(url)
-      .then(
-        fragment => dispatch(getFragmentSuccess(fragment)),
-        err => dispatch(getFragmentFailure(fragment, err)),
-      )
-  }
-}
-
-function updateFragmentRequest(fragment) {
-  return {
-    type: T.UPDATE_FRAGMENT_REQUEST,
-    fragment: fragment,
-  }
-}
-
-function updateFragmentSuccess(fragment, update) {
-  return {
-    type: T.UPDATE_FRAGMENT_SUCCESS,
-    fragment: fragment,
-    update: update,
-    receivedAt: Date.now(),
-  }
-}
-
-function updateFragmentFailure(fragment, error) {
-  return {
-    type: T.UPDATE_FRAGMENT_FAILURE,
-    isFetching: false,
-    fragment: fragment,
-    error: error,
-  }
-}
-
-export function updateFragment(collection, fragment) {
-  return dispatch => {
-    dispatch(updateFragmentRequest(fragment))
-
-    const url = fragmentUrl(collection, fragment)
-
-    return api
-      .update(url, fragment)
-      .then(
-        update => dispatch(updateFragmentSuccess(fragment, update)),
-        err => dispatch(updateFragmentFailure(fragment, err)),
-      )
-  }
-}
-
-function deleteFragmentRequest(fragment) {
-  return {
-    type: T.DELETE_FRAGMENT_REQUEST,
-    fragment: fragment,
-    update: { deleted: true },
-  }
-}
-
-function deleteFragmentSuccess(collection, fragment) {
-  return {
-    type: T.DELETE_FRAGMENT_SUCCESS,
-    collection: collection,
-    fragment: fragment,
-  }
-}
-
-function deleteFragmentFailure(fragment, error) {
-  return {
-    type: T.DELETE_FRAGMENT_FAILURE,
-    fragment: fragment,
-    update: { deleted: undefined },
-    error: error,
-  }
-}
-
-export function deleteFragment(collection, fragment) {
-  return dispatch => {
-    dispatch(deleteFragmentRequest(fragment))
-
-    const url = fragmentUrl(collection, fragment)
-
-    return api
-      .remove(url)
-      .then(
-        json => dispatch(deleteFragmentSuccess(collection, fragment)),
-        err => dispatch(deleteFragmentFailure(fragment, err)),
-      )
-  }
-}
diff --git a/src/actions/index.js b/src/actions/index.js
index 0ad2f8c..b1c6ea4 100644
--- a/src/actions/index.js
+++ b/src/actions/index.js
@@ -1,35 +1 @@
-import { RESET_ERROR_MESSAGE } from './types'
-
-import * as collections from './collections'
-import * as currentUser from './currentUser'
-import * as fileUpload from './fileUpload'
-import * as fragments from './fragments'
-import * as teams from './teams'
-import * as users from './users'
-
-import componentActions from '../components/actions'
-
-// Resets the currently visible error message.
-const resetErrorMessage = () => ({
-  type: RESET_ERROR_MESSAGE,
-})
-
-// Hydrate hydrates the store from a persistent store, the backend.
-// It gets collections, fragments and user data (via token).
-const hydrate = () => dispatch =>
-  Promise.all([
-    dispatch(currentUser.getCurrentUser()),
-    dispatch(collections.getCollections()),
-  ])
-
-export default {
-  ...collections,
-  ...currentUser,
-  ...fileUpload,
-  ...fragments,
-  ...teams,
-  ...users,
-  ...componentActions,
-  hydrate,
-  resetErrorMessage,
-}
+export default {}
diff --git a/src/actions/teams.js b/src/actions/teams.js
deleted file mode 100644
index 741bf17..0000000
--- a/src/actions/teams.js
+++ /dev/null
@@ -1,159 +0,0 @@
-import * as api from '../helpers/api'
-import * as T from './types'
-
-const teamUrl = team => {
-  let url = '/teams'
-
-  if (team) url += `/${team.id}`
-
-  return url
-}
-
-function getTeamsRequest() {
-  return {
-    type: T.GET_TEAMS_REQUEST,
-    isFetching: true,
-  }
-}
-
-function getTeamsSuccess(teams) {
-  return {
-    type: T.GET_TEAMS_SUCCESS,
-    isFetching: false,
-    teams: teams,
-  }
-}
-
-function getTeamsFailure(message) {
-  return {
-    type: T.GET_TEAMS_FAILURE,
-    isFetching: false,
-    message,
-  }
-}
-
-export function getTeams() {
-  return dispatch => {
-    dispatch(getTeamsRequest())
-
-    return api
-      .get(teamUrl())
-      .then(
-        teams => dispatch(getTeamsSuccess(teams)),
-        err => dispatch(getTeamsFailure(err)),
-      )
-  }
-}
-
-function createTeamRequest(team) {
-  return {
-    type: T.CREATE_TEAM_REQUEST,
-    team: team,
-  }
-}
-
-function createTeamSuccess(team) {
-  return {
-    type: T.CREATE_TEAM_SUCCESS,
-    team: team,
-  }
-}
-
-function createTeamFailure(team, error) {
-  return {
-    type: T.CREATE_TEAM_FAILURE,
-    isFetching: false,
-    team: team,
-    error: error,
-  }
-}
-
-export function createTeam(team) {
-  return dispatch => {
-    dispatch(createTeamRequest(team))
-
-    const url = teamUrl()
-
-    return api
-      .create(url, team)
-      .then(
-        team => dispatch(createTeamSuccess(team)),
-        err => dispatch(createTeamFailure(team, err)),
-      )
-  }
-}
-
-function updateTeamRequest(team) {
-  return {
-    type: T.UPDATE_TEAM_REQUEST,
-    team: team,
-  }
-}
-
-function updateTeamSuccess(team) {
-  return {
-    type: T.UPDATE_TEAM_SUCCESS,
-    team: team,
-  }
-}
-
-function updateTeamFailure(team, error) {
-  return {
-    type: T.UPDATE_TEAM_FAILURE,
-    isFetching: false,
-    team: team,
-    error: error,
-  }
-}
-
-export function updateTeam(team) {
-  return dispatch => {
-    dispatch(updateTeamRequest(team))
-    const url = teamUrl(team)
-
-    return api
-      .update(url, team)
-      .then(
-        team => dispatch(updateTeamSuccess(team)),
-        err => dispatch(updateTeamFailure(team, err)),
-      )
-  }
-}
-
-function deleteTeamRequest(team) {
-  return {
-    type: T.DELETE_TEAM_REQUEST,
-    team: team,
-  }
-}
-
-function deleteTeamSuccess(team) {
-  return {
-    type: T.DELETE_TEAM_SUCCESS,
-    team: team,
-  }
-}
-
-function deleteTeamFailure(team, error) {
-  return {
-    type: T.DELETE_TEAM_FAILURE,
-    isFetching: false,
-    team: team,
-    error: error,
-  }
-}
-
-export function deleteTeam(team) {
-  return dispatch => {
-    dispatch(deleteTeamRequest(team))
-
-    const url = teamUrl(team)
-
-    return api
-      .remove(url)
-      .then(
-        team => dispatch(deleteTeamSuccess(team)),
-        err => dispatch(deleteTeamFailure(team, err)),
-      )
-  }
-}
diff --git a/src/actions/users.js b/src/actions/users.js
deleted file mode 100644
index 1c76c04..0000000
--- a/src/actions/users.js
+++ /dev/null
@@ -1,112 +0,0 @@
-import * as api from '../helpers/api'
-import * as T from './types'
-
-function getUsersRequest() {
-  return {
-    type: T.GET_USERS_REQUEST,
-    isFetching: true,
-  }
-}
-
-function getUsersSuccess(users) {
-  return {
-    type: T.GET_USERS_SUCCESS,
-    isFetching: false,
-    users: users,
-  }
-}
-
-function getUsersFailure(message) {
-  return {
-    type: T.GET_USERS_FAILURE,
-    isFetching: false,
-    message,
-  }
-}
-
-export function getUsers() {
-  return dispatch => {
-    dispatch(getUsersRequest())
-
-    return api
-      .get('/users')
-      .then(
-        users => dispatch(getUsersSuccess(users.users)),
-        err => dispatch(getUsersFailure(err)),
-      )
-  }
-}
-
-function getUserRequest(user) {
-  return {
-    type: T.GET_USER_REQUEST,
-    user,
-  }
-}
-
-function getUserSuccess(user) {
-  return {
-    type: T.GET_USER_SUCCESS,
-    user,
-  }
-}
-
-function getUserFailure(user, error) {
-  return {
-    type: T.GET_USER_FAILURE,
-    user,
-    error,
-  }
-}
-
-export function getUser(user) {
-  return dispatch => {
-    dispatch(getUserRequest(user))
-
-    return api
-      .get('/users/' + user.id)
-      .then(
-        user => dispatch(getUserSuccess(user)),
-        err => dispatch(getUserFailure(err)),
-      )
-  }
-}
-
-function updateUserRequest(user) {
-  return {
-    type: T.UPDATE_USER_REQUEST,
-    user: user,
-    isFetching: true,
-  }
-}
-
-function updateUserSuccess(users) {
-  return {
-    type: T.UPDATE_USER_SUCCESS,
-    isFetching: false,
-    users: users,
-  }
-}
-
-function updateUserFailure(message) {
-  return {
-    type: T.UPDATE_USER_FAILURE,
-    isFetching: false,
-    error: message,
-  }
-}
-
-export function updateUser(user) {
-  return dispatch => {
-    dispatch(updateUserRequest(user))
-
-    const url = '/users/' + user.id
-
-    return api
-      .update(url, user)
-      .then(
-        user => dispatch(updateUserSuccess(user)),
-        err => dispatch(updateUserFailure(err)),
-      )
-  }
-}
diff --git a/src/components/AuthenticatedComponent.jsx b/src/components/AuthenticatedComponent.jsx
index 9960140..507e31e 100644
--- a/src/components/AuthenticatedComponent.jsx
+++ b/src/components/AuthenticatedComponent.jsx
@@ -1,53 +1,46 @@
 import React from 'react'
-import { connect } from 'react-redux'
 import { withRouter } from 'react-router'
-import { push } from 'react-router-redux'
 import PropTypes from 'prop-types'
+import { graphql, compose } from 'react-apollo'
 
-import actions from '../actions'
+import gql from 'graphql-tag'
 
-export class AuthenticatedComponent extends React.Component {
-  componentWillMount() {
-    this.props.getCurrentUser().then(() => this.checkAuth(this.props))
-  }
+const query = gql`
+    query CurrentUser {
+        currentUser: loggedInUser {
+            id
+            username
+            email
+            admin
+        }
+    }`
 
+
+export class AuthenticatedComponent extends React.Component {
   componentWillReceiveProps(nextProps) {
     this.checkAuth(nextProps)
   }
 
-  checkAuth({ isFetching, isAuthenticated }) {
-    if (!isFetching && !isAuthenticated) {
+  checkAuth({ data: {loading, currentUser} }) {
+    if (!loading && !currentUser) {
       const redirectAfterLogin = this.props.location.pathname
       this.props.pushState(`/login?next=${redirectAfterLogin}`)
     }
   }
 
   render() {
-    return this.props.isAuthenticated ? this.props.children : null
+    return this.props.data.currentUser ? this.props.children : null
   }
 }
 
 AuthenticatedComponent.propTypes = {
+  data: PropTypes.shape({
+    loading: PropTypes.bool,
+    currentUser: PropTypes.object,
+  }),
   children: PropTypes.node,
   location: PropTypes.object,
-  getCurrentUser: PropTypes.func.isRequired,
-  isFetching: PropTypes.bool,
-  isAuthenticated: PropTypes.bool,
   pushState: PropTypes.func.isRequired,
 }
 
-function mapState(state) {
-  return {
-    isFetching: state.currentUser.isFetching,
-    isAuthenticated: state.currentUser.isAuthenticated,
-  }
-}
-
-const ConnectedAuthenticatedComponent = withRouter(
-  connect(mapState, {
-    getCurrentUser: actions.getCurrentUser,
-    pushState: push,
-  })(AuthenticatedComponent),
-)
-
-export default ConnectedAuthenticatedComponent
+export default compose(withRouter, graphql(query))(AuthenticatedComponent)
diff --git a/src/components/Root.js b/src/components/Root.js
index f40aafc..bba6750 100644
--- a/src/components/Root.js
+++ b/src/components/Root.js
@@ -3,12 +3,34 @@ import { ConnectedRouter } from 'react-router-redux'
 import { Provider } from 'react-redux'
 import PropTypes from 'prop-types'
 import { ThemeProvider } from 'react-css-themr'
+import { ApolloProvider } from 'react-apollo'
+import { ApolloClient } from 'apollo-client'
+import { HttpLink } from 'apollo-link-http'
+import { ApolloLink } from 'apollo-link'
+import { InMemoryCache } from 'apollo-cache-inmemory'
+import config from 'config'
+
+const httpLink = new HttpLink({ uri: config['pubsweet-client'].API_ENDPOINT })
+const authLink = new ApolloLink((operation, forward) => {
+  operation.setContext({
+    headers: {
+      authorization: `Bearer ${localStorage.getItem('graphcoolToken')}`,
+    },
+  })
+  return forward(operation)
+})
+const client = new ApolloClient({
+  link: authLink.concat(httpLink),
+  cache: new InMemoryCache(),
+})
 
 const Root = ({ store, history, routes, theme }) => (
   <Provider store={store}>
-    <ConnectedRouter history={history}>
-      <ThemeProvider theme={theme}>{routes}</ThemeProvider>
-    </ConnectedRouter>
+    <ApolloProvider client={client}>
+      <ConnectedRouter history={history}>
+        <ThemeProvider theme={theme}>{routes}</ThemeProvider>
+      </ConnectedRouter>
+    </ApolloProvider>
   </Provider>
 )
 
diff --git a/src/components/UpdateSubscriber.jsx b/src/components/UpdateSubscriber.jsx
index 9f483e2..da8586a 100644
--- a/src/components/UpdateSubscriber.jsx
+++ b/src/components/UpdateSubscriber.jsx
@@ -6,7 +6,7 @@ import _ from 'lodash/fp'
 
 import * as T from '../actions/types'
 import 'event-source-polyfill'
-import token from '../helpers/token'
+const token = 'TODO'
 
 const actionMap = {
   'collection:create': T.CREATE_COLLECTION_SUCCESS,
diff --git a/src/helpers/Authorize.jsx b/src/helpers/Authorize.jsx
index 2d23985..4774a74 100644
--- a/src/helpers/Authorize.jsx
+++ b/src/helpers/Authorize.jsx
@@ -2,10 +2,10 @@
 
 import React from 'react'
 import PropTypes from 'prop-types'
-import { connect } from 'react-redux'
-import { compose } from 'redux'
 
 import withAuthsome from './withAuthsome'
+import { graphql, compose } from 'react-apollo'
+import gql from 'graphql-tag'
 
 export class Authorize extends React.Component {
   constructor(props) {
@@ -24,7 +24,7 @@ export class Authorize extends React.Component {
     this.checkAuth(nextProps)
   }
 
-  async checkAuth({ authsome, currentUser, operation, object }) {
+  async checkAuth({ authsome, data: {currentUser}, operation, object }) {
     try {
       const authorized = await authsome.can(
         currentUser && currentUser.id,
@@ -47,7 +47,7 @@ export class Authorize extends React.Component {
 }
 
 Authorize.propTypes = {
-  currentUser: PropTypes.object,
+  data: PropTypes.shape({currentUser: PropTypes.object}),
   operation: PropTypes.string,
   object: PropTypes.object,
   children: PropTypes.element,
@@ -55,13 +55,14 @@ Authorize.propTypes = {
   authsome: PropTypes.object.isRequired,
 }
 
-function mapState(state) {
-  return {
-    teams: state.teams,
-    collections: state.collections,
-    fragments: state.fragments,
-    currentUser: state.currentUser.user,
-  }
-}
+const query = gql`
+    query CurrentUser {
+        currentUser: loggedInUser {
+            id
+            username
+            email
+            admin
+        }
+    }`
 
-export default compose(withAuthsome(), connect(mapState))(Authorize)
+export default compose(withAuthsome(), graphql(query))(Authorize)
diff --git a/src/helpers/Utils.js b/src/helpers/Utils.js
deleted file mode 100644
index bb9a20f..0000000
--- a/src/helpers/Utils.js
+++ /dev/null
@@ -1,5 +0,0 @@
-// NOTE: deprecated - only retained for backwards compatibility
-
-import { deprecatedFetch } from './api'
-
-export const fetch = deprecatedFetch
diff --git a/src/helpers/api.js b/src/helpers/api.js
index b3e29c4..0e8c879 100644
--- a/src/helpers/api.js
+++ b/src/helpers/api.js
@@ -1,8 +1,4 @@
 import fetch from 'isomorphic-fetch'
-import endpoint from './endpoint'
-
-// read the authentication token from LocalStorage
-import getToken from './token'
 
 const parse = response => {
   if (response.headers.get('content-type').includes('application/json')) {
@@ -16,16 +12,6 @@ const request = (url, options = {}) => {
   options.headers = options.headers || {}
   options.headers['Accept'] = options.headers['Accept'] || 'application/json'
 
-  const token = getToken()
-
-  if (token) {
-    options.headers['Authorization'] = 'Bearer ' + token
-  }
-
-  if (!url.match(/^https?:/)) {
-    url = endpoint + url
-  }
-
   return fetch(url, options).then(response => {
     if (!response.ok) {
       return response.text().then(errorText => {
diff --git a/src/helpers/endpoint.js b/src/helpers/endpoint.js
deleted file mode 100644
index 84a7b26..0000000
--- a/src/helpers/endpoint.js
+++ /dev/null
@@ -1,3 +0,0 @@
-import config from 'config'
-
-module.exports = config['pubsweet-client'].API_ENDPOINT
diff --git a/src/helpers/token.js b/src/helpers/token.js
deleted file mode 100644
index 0fb7d80..0000000
--- a/src/helpers/token.js
+++ /dev/null
@@ -1,7 +0,0 @@
-module.exports = () => {
-  const localStorage = window.localStorage || global.window.localStorage
-
-  if (!localStorage) throw new Error('localstorage is not available')
-
-  return localStorage.getItem('token')
-}
diff --git a/src/helpers/withAuthsome.js b/src/helpers/withAuthsome.js
index 01ab39e..e997de2 100644
--- a/src/helpers/withAuthsome.js
+++ b/src/helpers/withAuthsome.js
@@ -1,36 +1,47 @@
+import React from 'react'
 import Authsome from 'authsome'
-import { connect } from 'react-redux'
+import { withApollo } from 'react-apollo'
 import config from 'config'
+import { gql } from 'apollo-client-preset'
 const mode = require(config.authsome.mode)
 
+const fragment = gql`
+  fragment theUser on User {
+    id
+    username
+    email
+    admin
+  }
+`
+
 // higher order component to inject authsome into a component
-export default function withAuthsome() {
-  const authsome = new Authsome({...config.authsome, mode}, {})
+function withAuthsome() {
+  const authsome = new Authsome({ ...config.authsome, mode }, {})
 
-  function mapState(state) {
-    authsome.context = {
-      // fetch entities from store instead of database
-      models: {
-        Collection: {
-          find: id =>
-            state.collections.find(collection => collection.id === id),
-        },
-        Fragment: {
-          find: id => state.fragments[id],
-        },
-        Team: {
-          find: id => state.teams.find(team => team.id === id),
-        },
-        User: {
-          find: id => {
-            return state.users.users.find(user => user.id === id)
+  return Component =>
+    withApollo(({ client, ...props }) => {
+      authsome.context = {
+        // fetch entities from store instead of database
+        models: {
+          Collection: {
+            find: id =>
+              client.readFragment({ id: `Collection:${id}`, fragment }),
+          },
+          Fragment: {
+            find: id => client.readFragment({ id: `Fragment:${id}`, fragment }),
+          },
+          Team: {
+            find: id => client.readFragment({ id: `Team:${id}`, fragment }),
+          },
+          User: {
+            find: id =>
+              client.readFragment({ id: `UserPayload:${id}`, fragment }),
           },
         },
-      },
-    }
+      }
 
-    return { authsome }
-  }
-
-  return connect(mapState)
+      return <Component authsome={authsome} {...props} />
+    })
 }
+
+export default withAuthsome
diff --git a/src/helpers/withLoader.js b/src/helpers/withLoader.js
new file mode 100644
index 0000000..cb9dbfb
--- /dev/null
+++ b/src/helpers/withLoader.js
@@ -0,0 +1,21 @@
+import React from 'react'
+import PropTypes from 'prop-types'
+
+export default () => {
+  return WrappedComponent => {
+    const Wrapper = ({
+      data: { loading, error, ...apolloProps },
+      ...parentProps
+    }) => {
+      if (loading) return <div>Loading...</div>
+      if (error) return <div>{error.message}</div>
+      return <WrappedComponent {...parentProps} {...apolloProps} />
+    }
+
+    Wrapper.propTypes = {
+      data: PropTypes.object,
+    }
+
+    return Wrapper
+  }
+}
diff --git a/src/index.js b/src/index.js
index 0296627..b7e85b8 100644
--- a/src/index.js
+++ b/src/index.js
@@ -2,5 +2,4 @@ export { default as configureStore } from './store/configureStore'
 export { default as Root } from './components/Root'
 export { requireAuthentication } from './components/AuthenticatedComponent'
 export { default as actions } from './actions'
-export { default as reducers } from './reducers'
 export { default as validations } from './validations'
diff --git a/src/reducers/collections.js b/src/reducers/collections.js
deleted file mode 100644
index 25d4fbb..0000000
--- a/src/reducers/collections.js
+++ /dev/null
@@ -1,130 +0,0 @@
-import {
-  GET_COLLECTIONS_SUCCESS,
-  GET_COLLECTIONS_FAILURE,
-  CREATE_COLLECTION_SUCCESS,
-  GET_COLLECTION_REQUEST,
-  GET_COLLECTION_SUCCESS,
-  UPDATE_COLLECTION_SUCCESS,
-  PATCH_COLLECTION_SUCCESS,
-  DELETE_COLLECTION_SUCCESS,
-  GET_FRAGMENTS_SUCCESS,
-  CREATE_FRAGMENT_SUCCESS,
-  DELETE_FRAGMENT_SUCCESS,
-  LOGOUT_SUCCESS,
-} from '../actions/types'
-
-import find from 'lodash/find'
-import union from 'lodash/union'
-import difference from 'lodash/difference'
-import clone from 'lodash/clone'
-import findIndex from 'lodash/findIndex'
-import without from 'lodash/without'
-
-export default function(state = [], action) {
-  const collections = clone(state)
-
-  // TODO: store entities as an object or immutable Map, with the id as the key
-  function getCollection() {
-    return find(collections, { id: action.collection.id })
-  }
-
-  function getCollectionIndex() {
-    return findIndex(collections, { id: action.collection.id })
-  }
-
-  function addCollection() {
-    // only add the collection if it hasn't already been added
-    if (!getCollection()) {
-      collections.push(action.collection)
-    }
-
-    return collections
-  }
-
-  function setCollection() {
-    const index = getCollectionIndex()
-
-    // NOTE: this is necessary because the collections state is an array
-    if (index === -1) {
-      collections.push(action.collection)
-    } else {
-      collections[index] = action.collection
-    }
-
-    return collections
-  }
-
-  function updateCollection() {
-    const index = getCollectionIndex()
-
-    collections[index] = { ...collections[index], ...action.update }
-
-    return collections
-  }
-
-  function deleteCollection() {
-    const collection = getCollection()
-
-    return without(collections, collection)
-  }
-
-  function addFragments() {
-    const collection = getCollection()
-
-    if (collection) {
-      collection.fragments = union(
-        collection.fragments,
-        (action.fragments || [action.fragment]).map(fragment => fragment.id),
-      )
-    }
-
-    return collections
-  }
-
-  function removeFragments() {
-    const collection = getCollection()
-
-    if (collection) {
-      collection.fragments = difference(
-        collection.fragments,
-        (action.fragments || [action.fragment]).map(fragment => fragment.id),
-      )
-    }
-
-    return collections
-  }
-
-  switch (action.type) {
-    case GET_COLLECTIONS_SUCCESS:
-      return clone(action.collections)
-
-    case GET_COLLECTIONS_FAILURE:
-      return []
-
-    case CREATE_COLLECTION_SUCCESS:
-      return addCollection()
-
-    case GET_COLLECTION_SUCCESS:
-      return setCollection()
-
-    case UPDATE_COLLECTION_SUCCESS:
-    case PATCH_COLLECTION_SUCCESS:
-      return updateCollection()
-
-    case GET_COLLECTION_REQUEST:
-    case DELETE_COLLECTION_SUCCESS:
-      return deleteCollection()
-
-    case DELETE_FRAGMENT_SUCCESS:
-      return removeFragments()
-
-    case GET_FRAGMENTS_SUCCESS:
-    case CREATE_FRAGMENT_SUCCESS:
-      return addFragments()
-
-    case LOGOUT_SUCCESS:
-      return []
-  }
-
-  return state
-}
diff --git a/src/reducers/currentUser.js b/src/reducers/currentUser.js
deleted file mode 100644
index a14eb84..0000000
--- a/src/reducers/currentUser.js
+++ /dev/null
@@ -1,48 +0,0 @@
-import {
-  GET_CURRENT_USER_REQUEST,
-  GET_CURRENT_USER_SUCCESS,
-  GET_CURRENT_USER_FAILURE,
-  LOGOUT_SUCCESS,
-} from '../actions/types'
-
-export default function(
-  state = {
-    isFetching: false,
-    isAuthenticated: false,
-  },
-  action,
-) {
-  switch (action.type) {
-    case GET_CURRENT_USER_REQUEST:
-      return {
-        ...state,
-        isFetching: true,
-        isAuthenticated: false,
-      }
-
-    case GET_CURRENT_USER_SUCCESS:
-      return {
-        ...state,
-        isFetching: false,
-        isAuthenticated: true,
-        user: action.user,
-      }
-
-    case GET_CURRENT_USER_FAILURE:
-      return {
-        ...state,
-        isFetching: false,
-        isAuthenticated: false,
-      }
-
-    case LOGOUT_SUCCESS:
-      return {
-        isFetching: false,
-        isAuthenticated: false,
-        user: null,
-      }
-
-    default:
-      return state
-  }
-}
diff --git a/src/reducers/error.js b/src/reducers/error.js
deleted file mode 100644
index df299d1..0000000
--- a/src/reducers/error.js
+++ /dev/null
@@ -1,9 +0,0 @@
-// Updates error message to notify about the failed fetches.
-export default function(state = null, { error }) {
-  if (error && error.message) {
-    console.error(error)
-    return error.message
-  }
-
-  return null
-}
diff --git a/src/reducers/fileUpload.js b/src/reducers/fileUpload.js
deleted file mode 100644
index d5db71d..0000000
--- a/src/reducers/fileUpload.js
+++ /dev/null
@@ -1,24 +0,0 @@
-import { FILE_UPLOAD_REQUEST, FILE_UPLOAD_SUCCESS } from '../actions/types'
-
-export default function(
-  state = {
-    isFetching: false,
-  },
-  action,
-) {
-  switch (action.type) {
-    case FILE_UPLOAD_SUCCESS:
-      return {
-        ...state,
-        isFetching: false,
-        file: action.file,
-      }
-    case FILE_UPLOAD_REQUEST:
-      return {
-        ...state,
-        isFetching: true,
-      }
-    default:
-      return state
-  }
-}
diff --git a/src/reducers/fragments.js b/src/reducers/fragments.js
deleted file mode 100644
index 4d5cafe..0000000
--- a/src/reducers/fragments.js
+++ /dev/null
@@ -1,80 +0,0 @@
-import {
-  GET_FRAGMENTS_SUCCESS,
-  CREATE_FRAGMENT_REQUEST,
-  CREATE_FRAGMENT_SUCCESS,
-  CREATE_FRAGMENT_FAILURE,
-  GET_FRAGMENT_REQUEST,
-  GET_FRAGMENT_SUCCESS,
-  UPDATE_FRAGMENT_REQUEST,
-  UPDATE_FRAGMENT_SUCCESS,
-  // UPDATE_FRAGMENT_FAILURE,
-  DELETE_FRAGMENT_REQUEST,
-  DELETE_FRAGMENT_FAILURE,
-  DELETE_FRAGMENT_SUCCESS,
-  LOGOUT_SUCCESS,
-} from '../actions/types'
-
-import clone from 'lodash/clone'
-import unset from 'lodash/unset'
-
-export default function(state = {}, action) {
-  const fragments = clone(state)
-
-  function replaceAll() {
-    unset(fragments, action.collection.fragments)
-    action.fragments.forEach(fragment => {
-      fragments[fragment.id] = fragment
-    })
-    return fragments
-  }
-
-  function setOne() {
-    fragments[action.fragment.id] = action.fragment
-
-    return fragments
-  }
-
-  function updateOne() {
-    const oldfragment = fragments[action.fragment.id] || {}
-    const update = action.update || action.fragment
-
-    fragments[action.fragment.id] = { ...oldfragment, ...update }
-
-    return fragments
-  }
-
-  function removeOne() {
-    unset(fragments, action.fragment.id)
-    return fragments
-  }
-
-  // choose the sword, and you will join me
-  // choose the ball, and you join your mother... in death
-  // you don't understand my words, but you must choose
-  // 拝 一刀 | Ogami Ittō
-  switch (action.type) {
-    case UPDATE_FRAGMENT_REQUEST:
-    case UPDATE_FRAGMENT_SUCCESS:
-    case DELETE_FRAGMENT_REQUEST:
-    case DELETE_FRAGMENT_FAILURE:
-    case CREATE_FRAGMENT_SUCCESS:
-    case CREATE_FRAGMENT_REQUEST:
-      return updateOne()
-
-    case GET_FRAGMENT_SUCCESS:
-      return setOne()
-
-    case GET_FRAGMENT_REQUEST:
-    case CREATE_FRAGMENT_FAILURE:
-    case DELETE_FRAGMENT_SUCCESS:
-      return removeOne()
-
-    case GET_FRAGMENTS_SUCCESS:
-      return replaceAll()
-
-    case LOGOUT_SUCCESS:
-      return {}
-  }
-
-  return state
-}
diff --git a/src/reducers/index.js b/src/reducers/index.js
deleted file mode 100644
index 25a2890..0000000
--- a/src/reducers/index.js
+++ /dev/null
@@ -1,17 +0,0 @@
-import collections from './collections'
-import currentUser from './currentUser'
-import error from './error'
-import fileUpload from './fileUpload'
-import fragments from './fragments'
-import users from './users'
-import teams from './teams'
-
-export default {
-  collections,
-  currentUser,
-  error,
-  fileUpload,
-  fragments,
-  teams,
-  users,
-}
diff --git a/src/reducers/teams.js b/src/reducers/teams.js
deleted file mode 100644
index 4055822..0000000
--- a/src/reducers/teams.js
+++ /dev/null
@@ -1,44 +0,0 @@
-import {
-  GET_TEAMS_SUCCESS,
-  CREATE_TEAM_SUCCESS,
-  UPDATE_TEAM_SUCCESS,
-  DELETE_TEAM_SUCCESS,
-  GET_COLLECTION_TEAMS_SUCCESS,
-  LOGOUT_SUCCESS,
-} from '../actions/types'
-
-import clone from 'lodash/clone'
-import findIndex from 'lodash/findIndex'
-import differenceBy from 'lodash/differenceBy'
-import unionBy from 'lodash/unionBy'
-
-export default function(state = [], action) {
-  const teams = clone(state)
-
-  function updateOne() {
-    const index = findIndex(teams, { id: action.team.id })
-    if (index !== -1) {
-      teams[index] = { ...teams[index], ...action.team }
-    } else {
-      teams.push(action.team)
-    }
-
-    return teams
-  }
-
-  switch (action.type) {
-    case CREATE_TEAM_SUCCESS:
-    case UPDATE_TEAM_SUCCESS:
-      return updateOne()
-    case GET_TEAMS_SUCCESS:
-      return clone(action.teams)
-    case DELETE_TEAM_SUCCESS:
-      return differenceBy(state, [action.team], 'id')
-    case LOGOUT_SUCCESS:
-      return []
-    case GET_COLLECTION_TEAMS_SUCCESS:
-      return unionBy(state, action.teams, 'id')
-  }
-
-  return state
-}
diff --git a/src/reducers/users.js b/src/reducers/users.js
deleted file mode 100644
index 80d00fb..0000000
--- a/src/reducers/users.js
+++ /dev/null
@@ -1,72 +0,0 @@
-import { unionBy } from 'lodash'
-
-import {
-  GET_USERS_REQUEST,
-  GET_USERS_SUCCESS,
-  GET_USER_SUCCESS,
-  UPDATE_USER_REQUEST,
-  UPDATE_USER_SUCCESS,
-  GET_CURRENT_USER_SUCCESS,
-  LOGOUT_SUCCESS,
-} from '../actions/types'
-
-// TODO: store users as an object/map instead of an array
-
-// The users reducer.
-export default (
-  state = {
-    isFetching: false,
-    users: [],
-  },
-  action,
-) => {
-  switch (action.type) {
-    case GET_USERS_REQUEST:
-      return {
-        ...state,
-        isFetching: true,
-      }
-
-    case GET_USERS_SUCCESS:
-      return {
-        ...state,
-        isFetching: false,
-        users: action.users,
-      }
-
-    case GET_USER_SUCCESS:
-      return {
-        ...state,
-        isFetching: false,
-        users: unionBy([action.user], state.users, 'id'),
-      }
-
-    case UPDATE_USER_REQUEST:
-      return {
-        ...state,
-        isFetching: true,
-      }
-
-    case UPDATE_USER_SUCCESS:
-      return {
-        ...state,
-        isFetching: false,
-        users: unionBy([action.user], state.users, 'id'),
-      }
-
-    case LOGOUT_SUCCESS:
-      return {
-        isFetching: false,
-        users: [],
-      }
-
-    case GET_CURRENT_USER_SUCCESS:
-      return {
-        ...state,
-        users: unionBy([action.user], state.users, 'id'),
-      }
-
-    default:
-      return state
-  }
-}
diff --git a/src/store/configureStore.js b/src/store/configureStore.js
index 9e65bef..e1314e4 100644
--- a/src/store/configureStore.js
+++ b/src/store/configureStore.js
@@ -5,11 +5,6 @@ import thunk from 'redux-thunk'
 import { createLogger } from 'redux-logger'
 import { reducer as formReducer } from 'redux-form'
 import config from 'config'
-import reducers from '../reducers'
-
-require('../components/reducers').forEach(componentReducers =>
-  Object.assign(reducers, componentReducers),
-)
 
 function createConfigureStore(env) {
   return function configureStore(
@@ -19,7 +14,6 @@ function createConfigureStore(env) {
     customMiddlewares = [],
   ) {
     const reducer = combineReducers({
-      ...reducers,
       form: formReducer,
       routing: routerReducer,
       ...customReducers,
@@ -47,14 +41,6 @@ function createConfigureStore(env) {
       composeEnhancers(...middleware),
     )
 
-    if (module.hot) {
-      // Enable Webpack hot module replacement for reducers
-      module.hot.accept('../reducers', () => {
-        const nextRootReducer = require('../reducers')
-        store.replaceReducer(nextRootReducer)
-      })
-    }
-
     return store
   }
 }
diff --git a/src/validations.js b/src/validations.js
index 3aaca1f..4ba52ba 100644
--- a/src/validations.js
+++ b/src/validations.js
@@ -1,3 +1 @@
-import config from 'config'
-
-module.exports = require('pubsweet-server/src/models/validations')(config)
+module.exports = {}
diff --git a/test/actions/collections.test.js b/test/actions/collections.test.js
deleted file mode 100644
index 65ed309..0000000
--- a/test/actions/collections.test.js
+++ /dev/null
@@ -1,120 +0,0 @@
-global.PUBSWEET_COMPONENTS = []
-
-const actions = require('../../src/actions/collections')
-const describeAction = require('../helpers/describeAction')(actions)
-const T = require('../../src/actions/types')
-
-describe('Collection actions', () => {
-  describeAction('getCollections', {
-    types: {
-      request: T.GET_COLLECTIONS_REQUEST,
-      success: T.GET_COLLECTIONS_SUCCESS,
-      failure: T.GET_COLLECTIONS_FAILURE,
-    },
-    properties: {
-      request: ['type'],
-      success: ['type', 'collections', 'receivedAt'],
-      failure: ['type', 'error'],
-    },
-  })
-
-  describeAction('getCollectionTeams', {
-    firstarg: { id: 123 },
-    types: {
-      request: T.GET_COLLECTION_TEAMS_REQUEST,
-      success: T.GET_COLLECTION_TEAMS_SUCCESS,
-      failure: T.GET_COLLECTION_TEAMS_FAILURE,
-    },
-    properties: {
-      request: ['type'],
-      success: ['type', 'teams', 'receivedAt'],
-      failure: ['type', 'error'],
-    },
-  })
-
-  describeAction('createCollection', {
-    firstarg: {
-      type: 'testing',
-      title: 'this is a test collection',
-    },
-    types: {
-      request: T.CREATE_COLLECTION_REQUEST,
-      success: T.CREATE_COLLECTION_SUCCESS,
-      failure: T.CREATE_COLLECTION_FAILURE,
-    },
-    properties: {
-      request: ['type', 'collection'],
-      success: ['type', 'collection'],
-      failure: ['type', 'isFetching', 'collection', 'error'],
-    },
-  })
-
-  describeAction('getCollection', {
-    firstarg: { id: 123 },
-    types: {
-      request: T.GET_COLLECTION_REQUEST,
-      success: T.GET_COLLECTION_SUCCESS,
-      failure: T.GET_COLLECTION_FAILURE,
-    },
-    properties: {
-      request: ['type', 'collection'],
-      success: ['type', 'collection', 'receivedAt'],
-      failure: ['type', 'isFetching', 'collection', 'error'],
-    },
-  })
-
-  describeAction('updateCollection', {
-    firstarg: { id: 123 },
-    secondarg: {
-      type: 'testing',
-      title: 'this is an updated collection',
-    },
-    types: {
-      request: T.UPDATE_COLLECTION_REQUEST,
-      success: T.UPDATE_COLLECTION_SUCCESS,
-      failure: T.UPDATE_COLLECTION_FAILURE,
-    },
-    properties: {
-      request: ['type', 'collection'],
-      success: ['type', 'collection'],
-      failure: ['type', 'isFetching', 'collection', 'error'],
-    },
-  })
-
-  // NOTE: enable this once PATCH method is implemented on the server
-  // describeAction('patchCollection', {
-  //   firstarg: newcol,
-  //   secondarg: {
-  //     type: 'testing',
-  //     title: 'this is a patched collection'
-  //   },
-  //   types: {
-  //     request: T.PATCH_COLLECTION_REQUEST,
-  //     success: T.PATCH_COLLECTION_SUCCESS,
-  //     failure: T.PATCH_COLLECTION_FAILURE
-  //   },
-  //   properties: {
-  //     request: ['type', 'collection'],
-  //     success: ['type', 'collection'],
-  //     failure: ['type', 'isFetching', 'collection', 'error']
-  //   }
-  // }, (action, data) => {
-  //   expect(
-  //     data.PATCH_COLLECTION_SUCCESS.collection.title
-  //   ).toBe('this is a patched collection')
-  // })
-
-  describeAction('deleteCollection', {
-    firstarg: { id: 123 },
-    types: {
-      request: T.DELETE_COLLECTION_REQUEST,
-      success: T.DELETE_COLLECTION_SUCCESS,
-      failure: T.DELETE_COLLECTION_FAILURE,
-    },
-    properties: {
-      request: ['type', 'collection', 'update'],
-      success: ['type', 'collection'],
-      failure: ['type', 'collection', 'update', 'error'],
-    },
-  })
-})
diff --git a/test/actions/currentUser.test.js b/test/actions/currentUser.test.js
deleted file mode 100644
index 4bfd727..0000000
--- a/test/actions/currentUser.test.js
+++ /dev/null
@@ -1,20 +0,0 @@
-global.PUBSWEET_COMPONENTS = []
-
-const actions = require('../../src/actions/currentUser')
-const describeAction = require('../helpers/describeAction')(actions)
-const T = require('../../src/actions/types')
-
-describe('currentUser actions', () => {
-  describeAction('getCurrentUser', {
-    types: {
-      request: T.GET_CURRENT_USER_REQUEST,
-      success: T.GET_CURRENT_USER_SUCCESS,
-      failure: T.GET_CURRENT_USER_FAILURE,
-    },
-    properties: {
-      request: [],
-      success: ['user'],
-      failure: ['error'],
-    },
-  })
-})
diff --git a/test/actions/fileUpload.test.js b/test/actions/fileUpload.test.js
deleted file mode 100644
index 33b3eaa..0000000
--- a/test/actions/fileUpload.test.js
+++ /dev/null
@@ -1,32 +0,0 @@
-global.PUBSWEET_COMPONENTS = []
-
-require('isomorphic-form-data')
-global.FormData.prototype.oldAppend = global.FormData.prototype.append
-global.FormData.prototype.append = function(field, value, options) {
-  // File upload testing hack
-  if (typeof value === 'string') {
-    value = fs.createReadStream(value)
-  }
-  this.oldAppend(field, value, options)
-}
-
-const fs = require('fs')
-const actions = require('../../src/actions/fileUpload')
-const describeAction = require('../helpers/describeAction')(actions)
-const T = require('../../src/actions/types')
-
-describe('fileUpload actions', () => {
-  describeAction('fileUpload', {
-    firstarg: './test/helpers/mockapp.js',
-    types: {
-      request: T.FILE_UPLOAD_REQUEST,
-      success: T.FILE_UPLOAD_SUCCESS,
-      failure: T.FILE_UPLOAD_FAILURE,
-    },
-    properties: {
-      request: ['isFetching'],
-      success: ['isFetching', 'file'],
-      failure: ['isFetching', 'error'],
-    },
-  })
-})
diff --git a/test/actions/fragments.test.js b/test/actions/fragments.test.js
deleted file mode 100644
index 97c50fb..0000000
--- a/test/actions/fragments.test.js
+++ /dev/null
@@ -1,105 +0,0 @@
-global.PUBSWEET_COMPONENTS = []
-
-const actions = require('../../src/actions/fragments')
-const describeAction = require('../helpers/describeAction')(actions)
-const T = require('../../src/actions/types')
-
-describe('fragments actions', () => {
-  const mockcol = { id: '123' }
-  const mockfragment = { id: '1234' }
-
-  describeAction('getFragments', {
-    firstarg: mockcol,
-    types: {
-      request: T.GET_FRAGMENTS_REQUEST,
-      success: T.GET_FRAGMENTS_SUCCESS,
-    },
-    properties: {
-      success: ['fragments'],
-    },
-  })
-
-  // get a list of collections, with the specified fields
-  describeAction('getFragments', {
-    firstarg: mockcol,
-    secondarg: {
-      fields: ['type', 'presentation'],
-    },
-    types: {
-      request: T.GET_FRAGMENTS_REQUEST,
-      success: T.GET_FRAGMENTS_SUCCESS,
-      failure: T.GET_FRAGMENTS_FAILURE,
-    },
-    properties: {
-      success: ['fragments'],
-    },
-  })
-
-  describeAction('createFragment', {
-    // no collection routes to top level fragment endpoint
-    firstarg: null,
-    secondarg: {
-      title: 'mock fragment',
-      type: 'some_fragment',
-      owners: [],
-    },
-    types: {
-      request: T.CREATE_FRAGMENT_REQUEST,
-      success: T.CREATE_FRAGMENT_SUCCESS,
-      failure: T.CREATE_FRAGMENT_FAILURE,
-    },
-    properties: {
-      success: ['collection', 'fragment'],
-      failure: ['fragment', 'error'],
-    },
-  })
-
-  describeAction('getFragment', {
-    firstarg: mockcol,
-    secondarg: mockfragment,
-    types: {
-      request: T.GET_FRAGMENT_REQUEST,
-      success: T.GET_FRAGMENT_SUCCESS,
-      failure: T.GET_FRAGMENT_FAILURE,
-    },
-    properties: {
-      request: ['fragment'],
-      success: ['fragment', 'receivedAt'],
-      failure: ['isFetching', 'fragment', 'error'],
-    },
-  })
-
-  describeAction('updateFragment', {
-    firstarg: mockcol,
-    secondarg: {
-      id: '1234',
-      title: 'modded fragment',
-      type: 'some_fragment',
-      owners: [],
-    },
-    types: {
-      request: T.UPDATE_FRAGMENT_REQUEST,
-      success: T.UPDATE_FRAGMENT_SUCCESS,
-      failure: T.UPDATE_FRAGMENT_FAILURE,
-    },
-    properties: {
-      success: ['fragment', 'receivedAt'],
-      failure: ['fragment', 'error'],
-    },
-  })
-
-  describeAction('deleteFragment', {
-    firstarg: mockcol,
-    secondarg: mockfragment,
-    types: {
-      request: T.DELETE_FRAGMENT_REQUEST,
-      success: T.DELETE_FRAGMENT_SUCCESS,
-      failure: T.DELETE_FRAGMENT_FAILURE,
-    },
-    properties: {
-      request: ['fragment', 'update'],
-      success: ['fragment', 'collection'],
-      failure: ['fragment', 'error', 'update'],
-    },
-  })
-})
diff --git a/test/actions/index.test.js b/test/actions/index.test.js
deleted file mode 100644
index 7f00128..0000000
--- a/test/actions/index.test.js
+++ /dev/null
@@ -1,14 +0,0 @@
-global.PUBSWEET_COMPONENTS = []
-
-const { hydrate } = require('../../src/actions').default
-
-describe('actions index', () => {
-  describe('hydrate', () => {
-    it('dispatches two actions', async () => {
-      const mockDispatch = jest.fn()
-      await hydrate()(mockDispatch)
-
-      expect(mockDispatch.mock.calls).toHaveLength(2)
-    })
-  })
-})
diff --git a/test/actions/teams.test.js b/test/actions/teams.test.js
deleted file mode 100644
index e0206a2..0000000
--- a/test/actions/teams.test.js
+++ /dev/null
@@ -1,85 +0,0 @@
-global.PUBSWEET_COMPONENTS = []
-
-const actions = require('../../src/actions/teams')
-const describeAction = require('../helpers/describeAction')(actions)
-const T = require('../../src/actions/types')
-
-describe('teams actions', () => {
-  describeAction('getTeams', {
-    types: {
-      request: T.GET_TEAMS_REQUEST,
-      success: T.GET_TEAMS_SUCCESS,
-      failure: T.GET_TEAMS_FAILURE,
-    },
-    properties: {
-      request: ['isFetching'],
-      success: ['isFetching', 'teams'],
-      failure: ['isFetching', 'message'],
-    },
-  })
-
-  describeAction('createTeam', {
-    firstarg: {
-      name: 'My readers',
-      teamType: {
-        name: 'Readers',
-        permissions: 'read',
-      },
-      object: {
-        kind: 'blogpost',
-        source: '<blog></blog>',
-        presentation: '<p></p>',
-      },
-    },
-    types: {
-      request: T.CREATE_TEAM_REQUEST,
-      success: T.CREATE_TEAM_SUCCESS,
-      failure: T.CREATE_TEAM_FAILURE,
-    },
-    properties: {
-      request: ['team'],
-      success: ['team'],
-      failure: ['isFetching', 'team', 'error'],
-    },
-  })
-
-  describeAction('updateTeam', {
-    firstarg: { id: 234 },
-    secondard: {
-      name: 'My readers',
-      teamType: {
-        name: 'Readers',
-        permissions: 'read',
-      },
-      object: {
-        kind: 'blogpost',
-        source: '<blog></blog>',
-        presentation: '<p></p>',
-      },
-    },
-    types: {
-      request: T.UPDATE_TEAM_REQUEST,
-      success: T.UPDATE_TEAM_SUCCESS,
-      failure: T.UPDATE_TEAM_FAILURE,
-    },
-    properties: {
-      request: ['team'],
-      success: ['team'],
-      failure: ['isFetching', 'team', 'error'],
-    },
-  })
-
-  describeAction('deleteTeam', {
-    firstarg: { id: 234 },
-    types: {
-      request: T.DELETE_TEAM_REQUEST,
-      success: T.DELETE_TEAM_SUCCESS,
-      failure: T.DELETE_TEAM_FAILURE,
-    },
-    properties: {
-      request: ['team'],
-      success: ['team'],
-      failure: ['isFetching', 'team', 'error'],
-    },
-  })
-})
diff --git a/test/actions/users.test.js b/test/actions/users.test.js
deleted file mode 100644
index e65136f..0000000
--- a/test/actions/users.test.js
+++ /dev/null
@@ -1,54 +0,0 @@
-global.PUBSWEET_COMPONENTS = []
-
-const actions = require('../../src/actions/users')
-const describeAction = require('../helpers/describeAction')(actions)
-const T = require('../../src/actions/types')
-
-describe('users actions', () => {
-  const user = {
-    username: 'fakeymcfake',
-    password: 'correct battery horse staple',
-    email: 'fakey_mcfake@pseudonymous.com',
-    id: '57d0fc8e-ece9-47bf-87d3-7935326b0128',
-  }
-
-  describeAction('getUsers', {
-    types: {
-      request: T.GET_USERS_REQUEST,
-      success: T.GET_USERS_SUCCESS,
-      failure: T.GET_USERS_FAILURE,
-    },
-    properties: {
-      success: ['users'],
-      failure: ['isFetching', 'message'],
-    },
-  })
-
-  describeAction('getUser', {
-    firstarg: { id: user.id },
-    types: {
-      request: T.GET_USER_REQUEST,
-      success: T.GET_USER_SUCCESS,
-      failure: T.GET_USER_FAILURE,
-    },
-    properties: {
-      request: ['user'],
-      success: ['user'],
-      failure: ['user', 'error'],
-    },
-  })
-
-  describeAction('updateUser', {
-    firstarg: user,
-    secondarg: user,
-    types: {
-      request: T.UPDATE_USER_REQUEST,
-      success: T.UPDATE_USER_SUCCESS,
-      failure: T.UPDATE_USER_FAILURE,
-    },
-    properties: {
-      success: ['users'],
-      failure: ['isFetching', 'error'],
-    },
-  })
-})
diff --git a/test/helpers/api.test.js b/test/helpers/api.test.js
deleted file mode 100644
index dd1d4ed..0000000
--- a/test/helpers/api.test.js
+++ /dev/null
@@ -1,118 +0,0 @@
-import nock from 'nock'
-import endpoint from '../../src/helpers/endpoint'
-
-// must be require, not import, due to mocking global above
-const api = require('../../src/helpers/api')
-
-describe('API helper', () => {
-  beforeAll(() => {
-    global.window.localStorage = {
-      getItem: jest.fn(() => 'tok123'),
-    }
-  })
-
-  it('makes a GET request', async () => {
-    nock(endpoint)
-      .get('/thing')
-      .reply(200, 'A thing', { 'content-type': 'text/plain' })
-
-    const actual = await api.get('/thing')
-    expect(actual).toBe('A thing')
-  })
-
-  it('makes a POST request', async () => {
-    nock(endpoint)
-      .post('/thing', {
-        some: 'data',
-      })
-      .reply(200, 'A new thing', { 'content-type': 'text/plain' })
-
-    const actual = await api.create('/thing', { some: 'data' })
-    expect(actual).toBe('A new thing')
-  })
-
-  it('makes a PATCH request', async () => {
-    nock(endpoint)
-      .patch('/thing/1', {
-        some: 'data',
-      })
-      .reply(200, 'A partially updated thing', { 'content-type': 'text/plain' })
-
-    const actual = await api.update('/thing/1', { some: 'data' })
-    expect(actual).toBe('A partially updated thing')
-  })
-
-  it('makes a PUT request', async () => {
-    nock(endpoint)
-      .put('/thing/1', {
-        some: 'data',
-      })
-      .reply(200, 'An updated thing', { 'content-type': 'text/plain' })
-
-    const actual = await api.update('/thing/1', { some: 'data' }, true)
-    expect(actual).toBe('An updated thing')
-  })
-
-  it('makes a DELETE request', async () => {
-    nock(endpoint)
-      .delete('/thing/1')
-      .reply(200, 'No thing', { 'content-type': 'text/plain' })
-
-    const actual = await api.remove('/thing/1')
-    expect(actual).toBe('No thing')
-  })
-
-  it('automatically parses JSON response', async () => {
-    const expected = { oh: 'yeah' }
-    nock(endpoint)
-      .get('/thing')
-      .reply(200, expected, { 'content-type': 'application/json' })
-
-    const actual = await api.get('/thing')
-    expect(actual).toEqual(expected)
-  })
-
-  it('includes token in header', async () => {
-    nock(endpoint, {
-      reqheaders: {
-        authorization: 'Bearer tok123',
-      },
-    })
-      .get('/thing')
-      .reply(200, 'OK', { 'content-type': '' })
-
-    const actual = await api.get('/thing')
-    expect(actual).toEqual('OK')
-  })
-
-  it('wraps HTTP errors', async () => {
-    nock(endpoint)
-      .get('/thing')
-      .reply(500, 'Yikes!', { 'content-type': '' })
-
-    try {
-      await api.get('/thing')
-    } catch (e) {
-      expect(e.message).toBe('Internal Server Error')
-      expect(e.response).toBe('Yikes!')
-      expect(e.statusCode).toBe(500)
-    }
-    expect.hasAssertions()
-  })
-
-  it('optionally returns raw response', async () => {
-    nock(endpoint)
-      .get('/thing')
-      .reply(204)
-
-    const response = await api.default('/thing', {
-      method: 'GET',
-      parse: false,
-    })
-
-    expect(response).toMatchObject({
-      ok: true,
-      statusText: 'No Content',
-    })
-  })
-})
diff --git a/test/helpers/describeAction.js b/test/helpers/describeAction.js
deleted file mode 100644
index 4d6222d..0000000
--- a/test/helpers/describeAction.js
+++ /dev/null
@@ -1,128 +0,0 @@
-const allactions = require('../../src/actions').default
-const api = require('../../src/helpers/api')
-
-const mockGetState = () => {
-  return {
-    currentUser: {},
-  }
-}
-
-function mockApi(succeed = true) {
-  const implementation = () =>
-    succeed ? Promise.resolve({}) : Promise.reject(new Error({}))
-  Object.keys(api)
-    .filter(method => typeof api[method] === 'function')
-    .forEach(method =>
-      jest.spyOn(api, method).mockImplementation(implementation),
-    )
-}
-
-// custom Jest matcher
-expect.extend({
-  toHaveProperties(object, expectedKeys) {
-    const actualKeys = Object.keys(object)
-    const pass = expectedKeys.every(key => actualKeys.includes(key))
-    return {
-      message: `Expected object ${pass
-        ? 'not to'
-        : 'to'} have properties: ${expectedKeys.join(', ')}`,
-      pass,
-    }
-  },
-})
-
-const describeAction = actions => (key, opts) => {
-  describe(key, () => {
-    const actionCreator = actions[key]
-
-    beforeEach(mockApi)
-
-    afterEach(() => jest.restoreAllMocks())
-
-    it('is exported from the file', () => {
-      expect(actions).toHaveProperty(key)
-    })
-
-    it('is exported in the all actions object', () => {
-      expect(allactions).toHaveProperty(key)
-    })
-
-    it('returns a fetcher function', () => {
-      const thunk = actionCreator(() => {}, mockGetState)
-      expect(typeof thunk).toBe('function')
-    })
-
-    it('returns a promise from the fetcher function', () => {
-      const thunk = actionCreator(opts.firstarg, opts.secondarg)
-      const returned = thunk(() => {}, mockGetState)
-      expect(typeof returned.then).toBe('function')
-    })
-
-    it('dispatches an action object with a type property', () => {
-      const actions = []
-      const thunk = actionCreator(opts.firstarg, opts.secondarg)
-      thunk(action => actions.push(action), mockGetState)
-      expect(actions).toHaveLength(1)
-      expect(actions[0]).toHaveProperty('type')
-    })
-
-    if (opts.types.request) {
-      const properties = opts.properties.request
-      const propmsg = properties ? `with [${properties.join(', ')}] ` : ''
-
-      it(`dispatches ${key}Request ${propmsg}immediately`, () => {
-        const actions = []
-        const thunk = actionCreator(opts.firstarg, opts.secondarg)
-        thunk(action => actions.push(action), mockGetState)
-
-        const firstAction = actions[0]
-        expect(firstAction).toBeTruthy()
-        expect(firstAction.type).toBe(opts.types.request)
-        if (properties) {
-          expect(firstAction).toHaveProperties(properties)
-        }
-      })
-    }
-
-    if (opts.types.success) {
-      const properties = opts.properties.success
-      const propmsg = properties ? `with [${properties.join(', ')}] ` : ''
-
-      it(`dispatches ${key}Success ${propmsg}on successful response`, async () => {
-        const actions = []
-        const thunk = actionCreator(opts.firstarg, opts.secondarg)
-        await thunk(action => actions.push(action), mockGetState)
-
-        const secondAction = actions[1]
-        expect(secondAction).toBeTruthy()
-        expect(secondAction.type).toBe(opts.types.success)
-        if (properties) {
-          expect(secondAction).toHaveProperties(properties)
-        }
-      })
-    }
-
-    if (opts.types.failure) {
-      const properties = opts.properties.failure
-      const propmsg = properties ? `with [${properties.join(', ')}] ` : ''
-
-      it(`dispatches ${key}Failure ${propmsg}on failed response`, async () => {
-        // make API reject every request
-        mockApi(false)
-
-        const actions = []
-        const thunk = actionCreator(opts.firstarg, opts.secondarg)
-        await thunk(action => actions.push(action), mockGetState)
-
-        const secondAction = actions[1]
-        expect(secondAction).toBeTruthy()
-        expect(secondAction.type).toBe(opts.types.failure)
-        if (properties) {
-          expect(secondAction).toHaveProperties(properties)
-        }
-      })
-    }
-  })
-}
-
-module.exports = describeAction
diff --git a/test/reducers/collections.test.js b/test/reducers/collections.test.js
deleted file mode 100644
index 0ff025c..0000000
--- a/test/reducers/collections.test.js
+++ /dev/null
@@ -1,116 +0,0 @@
-const allReducers = require('../../src/reducers').default
-const reducer = require('../../src/reducers/collections').default
-
-const T = require('../../src/actions/types')
-
-describe('collections reducers', () => {
-  it('is exported in the all reducers object', () => {
-    expect(allReducers.collections).toBe(reducer)
-  })
-
-  const mockCollection = { id: '123' }
-  const mockFragment = { name: 'mock fragment', id: '1234' }
-  const mockCollectionWithFragment = {
-    ...mockCollection,
-    fragments: [mockFragment.id],
-  }
-
-  it('getCollections success', () => {
-    const actual = reducer([mockCollection], {
-      type: T.GET_COLLECTIONS_SUCCESS,
-      collections: [mockCollection],
-    })
-    expect(actual).toEqual([mockCollection])
-  })
-
-  it('getCollections failure', () => {
-    const actual = reducer(undefined, {
-      type: T.GET_COLLECTIONS_FAILURE,
-    })
-    expect(actual).toEqual([])
-  })
-
-  it('getCollection request', () => {
-    const actual = reducer([mockCollection], {
-      type: T.GET_COLLECTION_REQUEST,
-      collection: mockCollection,
-    })
-    expect(actual).toEqual([])
-  })
-
-  it('getCollection success adds collection to store', () => {
-    const actual = reducer([], {
-      type: T.GET_COLLECTION_SUCCESS,
-      collection: mockCollection,
-    })
-    expect(actual).toEqual([mockCollection])
-  })
-
-  it('getCollection success updates collection in store', () => {
-    const actual = reducer([mockCollection], {
-      type: T.GET_COLLECTION_SUCCESS,
-      collection: mockCollectionWithFragment,
-    })
-    expect(actual).toEqual([mockCollectionWithFragment])
-  })
-
-  it('createCollection success', () => {
-    const actual = reducer(['dummy'], {
-      type: T.CREATE_COLLECTION_SUCCESS,
-      collection: mockCollection,
-    })
-    expect(actual).toEqual(['dummy', mockCollection])
-  })
-
-  it('createCollection success ignores duplicate', () => {
-    const actual = reducer([mockCollection], {
-      type: T.CREATE_COLLECTION_SUCCESS,
-      collection: { ...mockCollection, same: 'but different' },
-    })
-    expect(actual).toEqual([mockCollection])
-  })
-
-  it('updateCollection success', () => {
-    const actual = reducer(['dummy', mockCollection], {
-      type: T.UPDATE_COLLECTION_SUCCESS,
-      collection: mockCollection,
-      update: {
-        some: 'value',
-      },
-    })
-    expect(actual).toEqual(['dummy', { ...mockCollection, some: 'value' }])
-  })
-
-  it('addFragments success', () => {
-    const actual = reducer([mockCollection], {
-      type: T.CREATE_FRAGMENT_SUCCESS,
-      collection: mockCollection,
-      fragment: mockFragment,
-    })
-    expect(actual).toEqual([mockCollectionWithFragment])
-  })
-
-  it('removeFragments success', () => {
-    const actual = reducer([mockCollectionWithFragment], {
-      type: T.DELETE_FRAGMENT_SUCCESS,
-      collection: mockCollectionWithFragment,
-      fragment: mockFragment,
-    })
-    expect(actual).toEqual([mockCollectionWithFragment])
-  })
-
-  it('logout success', () => {
-    const actual = reducer([mockCollectionWithFragment], {
-      type: T.LOGOUT_SUCCESS,
-    })
-    expect(actual).toEqual([])
-  })
-
-  it('returns same state for unrecognised action', () => {
-    const state = []
-    const actual = reducer(state, {
-      type: 'something else',
-    })
-    expect(actual).toBe(state)
-  })
-})
diff --git a/test/reducers/currentUser.test.js b/test/reducers/currentUser.test.js
deleted file mode 100644
index de55c02..0000000
--- a/test/reducers/currentUser.test.js
+++ /dev/null
@@ -1,73 +0,0 @@
-const allReducers = require('../../src/reducers').default
-const reducer = require('../../src/reducers/currentUser').default
-
-const T = require('../../src/actions/types')
-
-describe('currentUser reducers', () => {
-  it('is exported in the all reducers object', () => {
-    expect(allReducers.currentUser).toBe(reducer)
-  })
-
-  const mockuser = {
-    name: 'jo johnson',
-  }
-
-  it('currentUser success', () => {
-    const actual = reducer(
-      {},
-      {
-        type: T.GET_CURRENT_USER_SUCCESS,
-        user: mockuser,
-      },
-    )
-    expect(actual).toEqual({
-      isFetching: false,
-      isAuthenticated: true,
-      user: mockuser,
-    })
-  })
-
-  it('currentUser failure', () => {
-    const actual = reducer(
-      {},
-      {
-        type: T.GET_CURRENT_USER_FAILURE,
-      },
-    )
-    expect(actual).toEqual({ isFetching: false, isAuthenticated: false })
-  })
-
-  it('currentUser request', () => {
-    const actual = reducer(
-      {},
-      {
-        type: T.GET_CURRENT_USER_REQUEST,
-      },
-    )
-    expect(actual).toEqual({ isFetching: true, isAuthenticated: false })
-  })
-
-  it('logout success', () => {
-    const actual = reducer(
-      {
-        user: mockuser,
-      },
-      {
-        type: T.LOGOUT_SUCCESS,
-      },
-    )
-    expect(actual).toEqual({
-      isFetching: false,
-      isAuthenticated: false,
-      user: null,
-    })
-  })
-
-  it('returns same state for unrecognised action', () => {
-    const state = {}
-    const actual = reducer(state, {
-      type: 'something else',
-    })
-    expect(actual).toBe(state)
-  })
-})
diff --git a/test/reducers/error.test.js b/test/reducers/error.test.js
deleted file mode 100644
index ad2fc75..0000000
--- a/test/reducers/error.test.js
+++ /dev/null
@@ -1,22 +0,0 @@
-const allReducers = require('../../src/reducers').default
-const reducer = require('../../src/reducers/error').default
-
-describe('error reducers', () => {
-  it('is exported in the all reducers object', () => {
-    expect(allReducers.error).toBe(reducer)
-  })
-
-  describe('reducer error handler', () => {
-    it("doesn't do anything if there's no error", () => {
-      expect(reducer(null, { error: null })).toBeNull()
-    })
-
-    it("returns the error message if there's an error", () => {
-      jest.spyOn(console, 'error').mockImplementation(jest.fn())
-      const error = new Error('this is a fake error')
-      const action = { error }
-      expect(reducer(null, action)).toBe(error.message)
-      expect(console.error.mock.calls[0][0]).toBe(error)
-    })
-  })
-})
diff --git a/test/reducers/fileUpload.test.js b/test/reducers/fileUpload.test.js
deleted file mode 100644
index 4e0a014..0000000
--- a/test/reducers/fileUpload.test.js
+++ /dev/null
@@ -1,36 +0,0 @@
-const allReducers = require('../../src/reducers').default
-const reducer = require('../../src/reducers/fileUpload').default
-
-const T = require('../../src/actions/types')
-
-describe('fileUpload reducers', () => {
-  it('is exported in the all reducers object', () => {
-    expect(allReducers.fileUpload).toBe(reducer)
-  })
-
-  it('fileUpload success', () => {
-    const actual = reducer(undefined, {
-      type: T.FILE_UPLOAD_SUCCESS,
-      file: 'somefile',
-    })
-    expect(actual).toEqual({
-      isFetching: false,
-      file: 'somefile',
-    })
-  })
-
-  it('fileUpload request', () => {
-    const actual = reducer(undefined, {
-      type: T.FILE_UPLOAD_REQUEST,
-    })
-    expect(actual).toEqual({ isFetching: true })
-  })
-
-  it('returns same state for unrecognised action', () => {
-    const state = {}
-    const actual = reducer(state, {
-      type: 'something else',
-    })
-    expect(actual).toBe(state)
-  })
-})
diff --git a/test/reducers/fragments.test.js b/test/reducers/fragments.test.js
deleted file mode 100644
index 6a51433..0000000
--- a/test/reducers/fragments.test.js
+++ /dev/null
@@ -1,97 +0,0 @@
-const allReducers = require('../../src/reducers').default
-const reducer = require('../../src/reducers/fragments').default
-
-const T = require('../../src/actions/types')
-
-const clone = require('lodash/clone')
-
-describe('fragments reducers', () => {
-  it('is exported in the all reducers object', () => {
-    expect(allReducers.fragments).toBe(reducer)
-  })
-
-  const mockcol = { id: '123' }
-  mockcol.fragments = []
-
-  const mockfrag = {
-    title: 'mock fragment',
-    type: 'some_fragment',
-    owners: [],
-  }
-  const mockstate = {}
-  mockstate[mockfrag.id] = mockfrag
-
-  const mockfragmod = {
-    title: 'modded fragment',
-    type: 'some_fragment',
-    owners: [],
-  }
-  const mockstatemod = {}
-  mockstatemod[mockfrag.id] = mockfragmod
-
-  const colwithfrag = clone(mockcol)
-  colwithfrag.fragments = [mockfrag]
-
-  it('getOne request', () => {
-    const actual = reducer(mockstate, {
-      type: T.GET_FRAGMENT_REQUEST,
-      collection: colwithfrag,
-      fragment: mockfragmod,
-    })
-    expect(actual).toEqual({})
-  })
-
-  it('getOne success', () => {
-    const actual = reducer(
-      {},
-      {
-        type: T.GET_FRAGMENT_SUCCESS,
-        collection: colwithfrag,
-        fragment: mockfragmod,
-      },
-    )
-    expect(actual).toEqual(mockstatemod)
-  })
-
-  it('updateOne success', () => {
-    const actual = reducer(mockstate, {
-      type: T.UPDATE_FRAGMENT_SUCCESS,
-      collection: colwithfrag,
-      fragment: mockfragmod,
-    })
-    expect(actual).toEqual(mockstatemod)
-  })
-
-  it('removeOne success', () => {
-    const actual = reducer(mockstate, {
-      type: T.DELETE_FRAGMENT_SUCCESS,
-      collection: colwithfrag,
-      fragment: mockfrag,
-    })
-    expect(actual).toEqual({})
-  })
-
-  it('replaceAll success', () => {
-    const actual = reducer(mockstate, {
-      type: T.GET_FRAGMENTS_SUCCESS,
-      collection: colwithfrag,
-      fragments: [mockfragmod],
-    })
-    expect(actual).toEqual(mockstatemod)
-  })
-
-  it('logout success', () => {
-    const actual = reducer(mockstate, {
-      type: T.LOGOUT_SUCCESS,
-    })
-    expect(actual).toEqual({})
-  })
-
-  it('returns same state for unrecognised action', () => {
-    const state = {}
-    const actual = reducer(state, {
-      type: 'something else',
-    })
-    expect(actual).toBe(state)
-  })
-})
diff --git a/test/reducers/teams.test.js b/test/reducers/teams.test.js
deleted file mode 100644
index 49160d6..0000000
--- a/test/reducers/teams.test.js
+++ /dev/null
@@ -1,70 +0,0 @@
-const allReducers = require('../../src/reducers').default
-const reducer = require('../../src/reducers/teams').default
-
-const T = require('../../src/actions/types')
-
-describe('teams reducers', () => {
-  it('is exported in the all reducers object', () => {
-    expect(allReducers.teams).toBe(reducer)
-  })
-
-  const mockteam = { name: 'someteam', id: '1234' }
-  const mockstate = [mockteam]
-
-  it('createTeam success', () => {
-    const actual = reducer([], {
-      type: T.CREATE_TEAM_SUCCESS,
-      team: mockteam,
-    })
-    expect(actual).toEqual(mockstate)
-  })
-
-  it('updateTeam success', () => {
-    const updatedTeam = { ...mockteam, foo: 'bar' }
-    const actual = reducer(mockstate, {
-      type: T.CREATE_TEAM_SUCCESS,
-      team: updatedTeam,
-    })
-    expect(actual).toEqual([updatedTeam])
-  })
-
-  it('getTeams success', () => {
-    const actual = reducer(mockstate, {
-      type: T.GET_TEAMS_SUCCESS,
-      teams: mockstate,
-    })
-    expect(actual).toEqual(mockstate)
-  })
-
-  it('deleteTeam success', () => {
-    const actual = reducer(mockstate, {
-      type: T.DELETE_TEAM_SUCCESS,
-      team: { ...mockteam },
-    })
-    expect(actual).toEqual([])
-  })
-
-  it('logout success', () => {
-    const actual = reducer(mockstate, {
-      type: T.LOGOUT_SUCCESS,
-    })
-    expect(actual).toEqual([])
-  })
-
-  it('getCollectionTeam success', () => {
-    const extraTeam = { id: '4321', name: 'Another team' }
-    const actual = reducer(mockstate, {
-      type: T.GET_COLLECTION_TEAMS_SUCCESS,
-      teams: [extraTeam, mockteam],
-    })
-    expect(actual).toEqual([mockteam, extraTeam])
-  })
-
-  it('returns same state for unrecognised action', () => {
-    const state = []
-    const actual = reducer(state, {
-      type: 'something else',
-    })
-    expect(actual).toBe(state)
-  })
-})
diff --git a/test/reducers/users.test.js b/test/reducers/users.test.js
deleted file mode 100644
index 33e2c17..0000000
--- a/test/reducers/users.test.js
+++ /dev/null
@@ -1,109 +0,0 @@
-import { GET_CURRENT_USER_SUCCESS } from '../../src/actions/types'
-
-const allReducers = require('../../src/reducers').default
-const reducer = require('../../src/reducers/users').default
-
-const T = require('../../src/actions/types')
-
-describe('users reducers', () => {
-  it('is exported in the all reducers object', () => {
-    expect(allReducers.users).toBe(reducer)
-  })
-
-  const user = {
-    username: 'fakeymcfake',
-    password: 'correct battery horse staple',
-    email: 'fakey_mcfake@pseudonymous.com',
-    id: '57d0fc8e-ece9-47bf-87d3-7935326b0128',
-  }
-
-  const usermod = { ...user, email: 'new@email.com' }
-
-  const mockstate = { users: [user] }
-
-  it('getUsers success', () => {
-    const actual = reducer(undefined, {
-      type: T.GET_USERS_SUCCESS,
-      users: [user],
-    })
-    expect(actual).toEqual({
-      isFetching: false,
-      users: [user],
-    })
-  })
-
-  it('getUsers request', () => {
-    const actual = reducer(undefined, {
-      type: T.GET_USERS_REQUEST,
-    })
-    expect(actual).toEqual({
-      isFetching: true,
-      users: [],
-    })
-  })
-
-  it('getUser success', () => {
-    const actual = reducer(
-      { users: [] },
-      {
-        type: T.GET_USER_SUCCESS,
-        user: user,
-      },
-    )
-    expect(actual).toEqual({
-      users: [user],
-      isFetching: false,
-    })
-  })
-
-  it('updateUser request', () => {
-    const actual = reducer(mockstate, {
-      type: T.UPDATE_USER_REQUEST,
-      user: usermod,
-    })
-    expect(actual).toEqual({
-      users: [user],
-      isFetching: true,
-    })
-  })
-
-  it('updateUser success', () => {
-    const actual = reducer(mockstate, {
-      type: T.UPDATE_USER_SUCCESS,
-      user: usermod,
-    })
-    expect(actual).toEqual({
-      users: [usermod],
-      isFetching: false,
-    })
-  })
-
-  it('logout success', () => {
-    const actual = reducer(mockstate, {
-      type: T.LOGOUT_SUCCESS,
-      user: usermod,
-    })
-    expect(actual).toEqual({
-      users: [],
-      isFetching: false,
-    })
-  })
-
-  it('getCurrentUser success adds user to users array', () => {
-    const actual = reducer(mockstate, {
-      type: GET_CURRENT_USER_SUCCESS,
-      user,
-    })
-    expect(actual).toEqual({
-      users: [user],
-    })
-  })
-
-  it('returns same state for unrecognised action', () => {
-    const state = {}
-    const actual = reducer(state, {
-      type: 'something else',
-    })
-    expect(actual).toBe(state)
-  })
-})
diff --git a/yarn.lock b/yarn.lock
index 2860c32..4b38b55 100644
--- a/yarn.lock
+++ b/yarn.lock
@@ -2,6 +2,14 @@
 # yarn lockfile v1
 
 
+"@types/async@2.0.45":
+  version "2.0.45"
+  resolved "https://registry.yarnpkg.com/@types/async/-/async-2.0.45.tgz#0cfe971d7ed5542695740338e0455c91078a0e83"
+
+"@types/zen-observable@0.5.3", "@types/zen-observable@^0.5.3":
+  version "0.5.3"
+  resolved "https://registry.yarnpkg.com/@types/zen-observable/-/zen-observable-0.5.3.tgz#91b728599544efbb7386d8b6633693a3c2e7ade5"
+
 abab@^1.0.3:
   version "1.0.4"
   resolved "https://registry.yarnpkg.com/abab/-/abab-1.0.4.tgz#5faad9c2c07f60dd76770f71cf025b62a63cfd4e"
@@ -99,6 +107,75 @@ anymatch@^1.3.0:
     micromatch "^2.1.5"
     normalize-path "^2.0.0"
 
+apollo-cache-inmemory@^1.1.1:
+  version "1.1.1"
+  resolved "https://registry.yarnpkg.com/apollo-cache-inmemory/-/apollo-cache-inmemory-1.1.1.tgz#1511f00eb845da88504abf867f408c3026a909ba"
+  dependencies:
+    apollo-cache "^1.0.1"
+    apollo-utilities "^1.0.2"
+    graphql-anywhere "^4.0.1"
+
+apollo-cache@^1.0.1:
+  version "1.0.1"
+  resolved "https://registry.yarnpkg.com/apollo-cache/-/apollo-cache-1.0.1.tgz#66c16141173bc752d3ad3dce990310c10dfc4076"
+  dependencies:
+    apollo-utilities "^1.0.2"
+
+apollo-client-preset@^1.0.3:
+  version "1.0.3"
+  resolved "https://registry.yarnpkg.com/apollo-client-preset/-/apollo-client-preset-1.0.3.tgz#e1f5d2d34115806c5021ae17cd09a88f61f623f5"
+  dependencies:
+    apollo-cache-inmemory "^1.1.1"
+    apollo-client "^2.0.3"
+    apollo-link "1.0.0"
+    apollo-link-http "1.1.0"
+    graphql-tag "^2.4.2"
+
+apollo-client@^2.0.3:
+  version "2.0.3"
+  resolved "https://registry.yarnpkg.com/apollo-client/-/apollo-client-2.0.3.tgz#f99f32e2c851bbd52da1e1b113ce8f6a0cf94945"
+  dependencies:
+    "@types/zen-observable" "^0.5.3"
+    apollo-cache "^1.0.1"
+    apollo-link "^1.0.0"
+    apollo-link-dedup "^1.0.0"
+    apollo-utilities "^1.0.2"
+    symbol-observable "^1.0.2"
+    zen-observable "^0.6.0"
+  optionalDependencies:
+    "@types/async" "2.0.45"
+
+apollo-link-dedup@^1.0.0:
+  version "1.0.2"
+  resolved "https://registry.yarnpkg.com/apollo-link-dedup/-/apollo-link-dedup-1.0.2.tgz#bab659dde41f8dd627839142d4dad90e55251110"
+
+apollo-link-http@1.1.0:
+  version "1.1.0"
+  resolved "https://registry.yarnpkg.com/apollo-link-http/-/apollo-link-http-1.1.0.tgz#a85cc43d9a5286bb54ac32213f7a16bef3554ae4"
+
+apollo-link@1.0.0:
+  version "1.0.0"
+  resolved "https://registry.yarnpkg.com/apollo-link/-/apollo-link-1.0.0.tgz#3d334285789c217f95712ebce434d56ce7f3e991"
+  dependencies:
+    apollo-utilities "^0.2.0-beta.0"
+    zen-observable "^0.6.0"
+
+apollo-link@^1.0.0, apollo-link@^1.0.3:
+  version "1.0.3"
+  resolved "https://registry.yarnpkg.com/apollo-link/-/apollo-link-1.0.3.tgz#759c36abeeb99e227eca45f919ee07fb8fee911e"
+  dependencies:
+    "@types/zen-observable" "0.5.3"
+    apollo-utilities "^1.0.0"
+    zen-observable "^0.6.0"
+
+apollo-utilities@^0.2.0-beta.0:
+  version "0.2.0-rc.3"
+  resolved "https://registry.yarnpkg.com/apollo-utilities/-/apollo-utilities-0.2.0-rc.3.tgz#7bd93be0f587f20c5b46e21880272e305759fdc2"
+
+apollo-utilities@^1.0.0, apollo-utilities@^1.0.2:
+  version "1.0.2"
+  resolved "https://registry.yarnpkg.com/apollo-utilities/-/apollo-utilities-1.0.2.tgz#bcf348a7e613e82e2624ddb5be2b9f6bf1259c6d"
+
 app-root-path@^2.0.0:
   version "2.0.1"
   resolved "https://registry.yarnpkg.com/app-root-path/-/app-root-path-2.0.1.tgz#cd62dcf8e4fd5a417efc664d2e5b10653c651b46"
@@ -2004,6 +2081,22 @@ graceful-fs@^4.1.11, graceful-fs@^4.1.2, graceful-fs@^4.1.4:
   version "4.1.11"
   resolved "https://registry.yarnpkg.com/graceful-fs/-/graceful-fs-4.1.11.tgz#0e8bdfe4d1ddb8854d64e04ea7c00e2a026e5658"
 
+graphql-anywhere@^4.0.1:
+  version "4.0.1"
+  resolved "https://registry.yarnpkg.com/graphql-anywhere/-/graphql-anywhere-4.0.1.tgz#eb53ed5c56ef42e21d34dc22951e3da38f88a342"
+  dependencies:
+    apollo-utilities "^1.0.2"
+
+graphql-tag@^2.4.2, graphql-tag@^2.5.0:
+  version "2.5.0"
+  resolved "https://registry.yarnpkg.com/graphql-tag/-/graphql-tag-2.5.0.tgz#b43bfd8b5babcd2c205ad680c03e98b238934e0f"
+
+graphql@^0.11.7:
+  version "0.11.7"
+  resolved "https://registry.yarnpkg.com/graphql/-/graphql-0.11.7.tgz#e5abaa9cb7b7cccb84e9f0836bf4370d268750c6"
+  dependencies:
+    iterall "1.1.3"
+
 growly@^1.3.0:
   version "1.3.0"
   resolved "https://registry.yarnpkg.com/growly/-/growly-1.3.0.tgz#f10748cbe76af964b7c96c93c6bcc28af120c081"
@@ -2104,7 +2197,7 @@ hoist-non-react-statics@^1.2.0:
   version "1.2.0"
   resolved "https://registry.yarnpkg.com/hoist-non-react-statics/-/hoist-non-react-statics-1.2.0.tgz#aa448cf0986d55cc40773b17174b7dd066cb7cfb"
 
-hoist-non-react-statics@^2.2.1, hoist-non-react-statics@^2.3.0, hoist-non-react-statics@^2.3.1:
+hoist-non-react-statics@^2.2.0, hoist-non-react-statics@^2.2.1, hoist-non-react-statics@^2.3.0, hoist-non-react-statics@^2.3.1:
   version "2.3.1"
   resolved "https://registry.yarnpkg.com/hoist-non-react-statics/-/hoist-non-react-statics-2.3.1.tgz#343db84c6018c650778898240135a1420ee22ce0"
 
@@ -2492,6 +2585,10 @@ istanbul-reports@^1.1.2:
   dependencies:
     handlebars "^4.0.3"
 
+iterall@1.1.3:
+  version "1.1.3"
+  resolved "https://registry.yarnpkg.com/iterall/-/iterall-1.1.3.tgz#1cbbff96204056dde6656e2ed2e2226d0e6d72c9"
+
 jest-changed-files@^21.2.0:
   version "21.2.0"
   resolved "https://registry.yarnpkg.com/jest-changed-files/-/jest-changed-files-21.2.0.tgz#5dbeecad42f5d88b482334902ce1cba6d9798d29"
@@ -2970,6 +3067,10 @@ lodash.flatten@^4.2.0:
   version "4.4.0"
   resolved "https://registry.yarnpkg.com/lodash.flatten/-/lodash.flatten-4.4.0.tgz#f31c22225a9632d2bbf8e4addbef240aa765a61f"
 
+lodash.flowright@^3.5.0:
+  version "3.5.0"
+  resolved "https://registry.yarnpkg.com/lodash.flowright/-/lodash.flowright-3.5.0.tgz#2b5fff399716d7e7dc5724fe9349f67065184d67"
+
 lodash.foreach@^4.3.0:
   version "4.5.0"
   resolved "https://registry.yarnpkg.com/lodash.foreach/-/lodash.foreach-4.5.0.tgz#1a6a35eace401280c7f06dddec35165ab27e3e53"
@@ -2982,7 +3083,7 @@ lodash.merge@^4.4.0:
   version "4.6.0"
   resolved "https://registry.yarnpkg.com/lodash.merge/-/lodash.merge-4.6.0.tgz#69884ba144ac33fe699737a6086deffadd0f89c5"
 
-lodash.pick@^4.2.1:
+lodash.pick@^4.2.1, lodash.pick@^4.4.0:
   version "4.4.0"
   resolved "https://registry.yarnpkg.com/lodash.pick/-/lodash.pick-4.4.0.tgz#52f05610fff9ded422611441ed1fc123a03001b3"
 
@@ -3602,6 +3703,17 @@ rc@^1.1.7:
     minimist "^1.2.0"
     strip-json-comments "~2.0.1"
 
+react-apollo@^2.0.1:
+  version "2.0.1"
+  resolved "https://registry.yarnpkg.com/react-apollo/-/react-apollo-2.0.1.tgz#43997db9f294d81bd229eb705944fb36d5164607"
+  dependencies:
+    apollo-link "^1.0.0"
+    hoist-non-react-statics "^2.2.0"
+    invariant "^2.2.1"
+    lodash.flowright "^3.5.0"
+    lodash.pick "^4.4.0"
+    prop-types "^15.5.8"
+
 react-bootstrap@^0.31.3:
   version "0.31.3"
   resolved "https://registry.yarnpkg.com/react-bootstrap/-/react-bootstrap-0.31.3.tgz#db2b7d45b00b5dac1ab8b6de3dd97feb3091b849"
@@ -4234,7 +4346,7 @@ supports-color@^4.0.0:
   dependencies:
     has-flag "^2.0.0"
 
-symbol-observable@^1.0.1, symbol-observable@^1.0.3:
+symbol-observable@^1.0.1, symbol-observable@^1.0.2, symbol-observable@^1.0.3:
   version "1.0.4"
   resolved "https://registry.yarnpkg.com/symbol-observable/-/symbol-observable-1.0.4.tgz#29bf615d4aa7121bdd898b22d4b3f9bc4e2aa03d"
 
@@ -4576,3 +4688,7 @@ yargs@~3.10.0:
     cliui "^2.1.0"
     decamelize "^1.0.0"
     window-size "0.1.0"
+
+zen-observable@^0.6.0:
+  version "0.6.0"
+  resolved "https://registry.yarnpkg.com/zen-observable/-/zen-observable-0.6.0.tgz#8a6157ed15348d185d948cfc4a59d90a2c0f70ee"
-- 
GitLab