From 3400ca041b8d7eec1e9d7d905fc7e9c10a60094e Mon Sep 17 00:00:00 2001
From: Alf Eaton <eaton.alf@gmail.com>
Date: Thu, 19 Oct 2017 10:14:30 +0100
Subject: [PATCH] Upgrade to react-router v4

* Import from react-router-dom
* Use withRouter
* Remove react-router-redux
* Replace AuthenticatedPage with PrivateRoute
---
 packages/component-app/package.json           |  6 +-
 packages/component-app/src/components/App.js  |  2 +-
 .../component-authentication/package.json     |  6 +-
 .../src/components/AuthenticatedPage.js       | 60 -------------------
 .../src/components/Login.js                   |  2 +-
 .../src/components/LoginPage.js               |  6 +-
 .../src/components/PrivateRoute.js            | 40 +++++++++++++
 .../src/components/Signup.js                  |  2 +-
 .../src/components/index.js                   |  2 +-
 .../src/redux/currentUser.js                  |  7 ++-
 .../src/redux/login.js                        |  6 +-
 .../src/redux/logout.js                       |  7 +--
 packages/component-dashboard/package.json     |  2 +-
 .../src/components/DashboardPage.js           | 14 +++--
 .../src/components/ProjectLink.js             |  2 +-
 .../src/redux/conversion.js                   |  5 +-
 packages/component-manuscript/package.json    |  4 +-
 .../src/components/Manuscript.js              |  8 +--
 .../src/components/ManuscriptPage.js          | 12 ++--
 packages/component-review/package.json        |  5 +-
 .../src/components/DecisionPage.js            | 19 +++---
 .../src/components/ReviewPage.js              | 21 +++----
 .../src/components/ReviewersPage.js           | 14 ++---
 packages/component-submit/package.json        |  5 +-
 .../component-submit/src/components/Submit.js |  2 +-
 .../src/components/SubmitPage.js              | 35 ++++++-----
 packages/xpub-collabra/app/Root.js            | 19 ------
 packages/xpub-collabra/app/app.js             | 25 ++++++--
 packages/xpub-collabra/app/routes.js          | 50 +++++++++-------
 packages/xpub-collabra/package.json           | 14 ++---
 packages/xpub-connect/package.json            |  6 +-
 .../src/components/ConnectPage.js             |  6 +-
 packages/xpub-ui/package.json                 |  2 +-
 packages/xpub-ui/src/molecules/AppBar.js      |  2 +-
 packages/xpub-ui/test/AppBar.test.js          |  2 +-
 packages/xpub-upload/package.json             |  6 +-
 packages/xpub-upload/src/upload.js            |  2 -
 37 files changed, 201 insertions(+), 227 deletions(-)
 delete mode 100644 packages/component-authentication/src/components/AuthenticatedPage.js
 create mode 100644 packages/component-authentication/src/components/PrivateRoute.js
 delete mode 100644 packages/xpub-collabra/app/Root.js

diff --git a/packages/component-app/package.json b/packages/component-app/package.json
index bea5558d4..19d61fe3a 100644
--- a/packages/component-app/package.json
+++ b/packages/component-app/package.json
@@ -14,8 +14,7 @@
     "react": "^15.6.1",
     "react-dom": "^15.6.1",
     "react-redux": "^5.0.2",
-    "react-router": "^3.0.5",
-    "react-router-redux": "^4.0.7",
+    "react-router-dom": "^4.2.2",
     "recompose": "^0.25.0",
     "redux": "^3.6.0",
     "xpub-bootstrap": "^0.0.2",
@@ -27,7 +26,6 @@
     "react": "^15.6.1",
     "react-dom": "^15.6.1",
     "react-redux": "^5.0.2",
-    "react-router": "^3.0.5",
-    "react-router-redux": "^4.0.7"
+    "react-router-dom": "^4.2.2"
   }
 }
diff --git a/packages/component-app/src/components/App.js b/packages/component-app/src/components/App.js
index 9d0a249ad..a65975f6d 100644
--- a/packages/component-app/src/components/App.js
+++ b/packages/component-app/src/components/App.js
@@ -24,7 +24,7 @@ const App = ({ children, currentUser, journal }) => (
 export default compose(
   connect(
     state => ({
-      currentUser: state.currentUser.user
+      currentUser: state.currentUser.user,
     })
   ),
   withJournal
diff --git a/packages/component-authentication/package.json b/packages/component-authentication/package.json
index 4f72e541b..343c9fdf5 100644
--- a/packages/component-authentication/package.json
+++ b/packages/component-authentication/package.json
@@ -15,8 +15,7 @@
     "react": "^15.6.1",
     "react-dom": "^15.6.1",
     "react-redux": "^5.0.2",
-    "react-router": "^3.0.5",
-    "react-router-redux": "^4.0.7",
+    "react-router-dom": "^4.2.2",
     "recompose": "^0.25.0",
     "redux": "^3.6.0",
     "redux-form": "^7.0.3",
@@ -45,8 +44,7 @@
     "react": "^15.6.1",
     "react-dom": "^15.6.1",
     "react-redux": "^5.0.2",
-    "react-router": "^3.0.5",
-    "react-router-redux": "^4.0.7"
+    "react-router-dom": "^4.2.2"
   },
   "scripts": {
     "styleguide": "styleguidist server",
diff --git a/packages/component-authentication/src/components/AuthenticatedPage.js b/packages/component-authentication/src/components/AuthenticatedPage.js
deleted file mode 100644
index f2a62da9d..000000000
--- a/packages/component-authentication/src/components/AuthenticatedPage.js
+++ /dev/null
@@ -1,60 +0,0 @@
-import React from 'react'
-import PropTypes from 'prop-types'
-import { compose } from 'recompose'
-import { connect } from 'react-redux'
-import { push } from 'react-router-redux'
-import { withRouter } from 'react-router'
-import { getCurrentUser } from '../redux/currentUser'
-
-class AuthenticatedPage extends React.Component {
-  componentDidMount () {
-    const { isAuthenticated, getCurrentUser } = this.props
-
-    if (!isAuthenticated) {
-      getCurrentUser()
-    }
-  }
-
-  componentWillReceiveProps (nextProps) {
-    const { isAuthenticated, isFetching } = nextProps
-
-    if (!isAuthenticated && !isFetching) {
-      this.login()
-    }
-  }
-
-  login () {
-    const { location, push } = this.props
-
-    push('/login?next=' + encodeURIComponent(location.pathname))
-  }
-
-  render () {
-    const { isAuthenticated, children } = this.props
-
-    return isAuthenticated ? children : null
-  }
-}
-
-AuthenticatedPage.propTypes = {
-  children: PropTypes.node.isRequired,
-  getCurrentUser: PropTypes.func.isRequired,
-  isAuthenticated: PropTypes.bool.isRequired,
-  isFetching: PropTypes.bool.isRequired,
-  location: PropTypes.object.isRequired,
-  push: PropTypes.func.isRequired
-}
-
-export default compose(
-  connect(
-    state => ({
-      isAuthenticated: state.currentUser.isAuthenticated,
-      isFetching: state.currentUser.isFetching
-    }),
-    {
-      getCurrentUser,
-      push
-    }
-  ),
-  withRouter
-)(AuthenticatedPage)
diff --git a/packages/component-authentication/src/components/Login.js b/packages/component-authentication/src/components/Login.js
index fbcd62a73..0113eac6a 100644
--- a/packages/component-authentication/src/components/Login.js
+++ b/packages/component-authentication/src/components/Login.js
@@ -1,6 +1,6 @@
 import React from 'react'
 import { Field } from 'redux-form'
-import { Link } from 'react-router'
+import { Link } from 'react-router-dom'
 import { Button, TextField } from 'xpub-ui'
 import classes from './Form.local.scss'
 
diff --git a/packages/component-authentication/src/components/LoginPage.js b/packages/component-authentication/src/components/LoginPage.js
index efda21f78..840b42c48 100644
--- a/packages/component-authentication/src/components/LoginPage.js
+++ b/packages/component-authentication/src/components/LoginPage.js
@@ -6,8 +6,10 @@ import Login from './Login'
 
 // TODO: const redirect = this.props.location.query.next | CONFIG['pubsweet-client']['login-redirect']
 
-const onSubmit = (values, dispatch) => {
-  dispatch(login(values)).catch(error => {
+const onSubmit = (values, dispatch, { history }) => {
+  dispatch(login(values)).then(() => {
+    history.push('/') // TODO: state
+  }).catch(error => {
     if (error.validationErrors) {
       throw new SubmissionError(error.validationErrors)
     } else {
diff --git a/packages/component-authentication/src/components/PrivateRoute.js b/packages/component-authentication/src/components/PrivateRoute.js
new file mode 100644
index 000000000..4a00b6073
--- /dev/null
+++ b/packages/component-authentication/src/components/PrivateRoute.js
@@ -0,0 +1,40 @@
+import React from 'react'
+import { compose } from 'recompose'
+import { connect } from 'react-redux'
+import { Route, Redirect, withRouter } from 'react-router-dom'
+import { getCurrentUser } from '../redux/currentUser'
+
+const PrivateRoute = ({ currentUser, getCurrentUser, component: Component, ...rest }) => (
+  <Route {...rest} render={props => {
+    if (!currentUser.isFetched) {
+      if (!currentUser.isFetching) {
+        getCurrentUser()
+      }
+
+      return <div>loading…</div>
+    }
+
+    if (!currentUser.isAuthenticated) {
+      return (
+        <Redirect to={{
+          pathname: '/login',
+          state: { from: props.location }
+        }}/>
+      )
+    }
+
+    return <Component {...props}/>
+  }}/>
+)
+
+export default compose(
+  withRouter,
+  connect(
+    state => ({
+      currentUser: state.currentUser,
+    }),
+    {
+      getCurrentUser
+    }
+  )
+)(PrivateRoute)
diff --git a/packages/component-authentication/src/components/Signup.js b/packages/component-authentication/src/components/Signup.js
index 2f71a0700..3c698b42e 100644
--- a/packages/component-authentication/src/components/Signup.js
+++ b/packages/component-authentication/src/components/Signup.js
@@ -1,6 +1,6 @@
 import React from 'react'
 import { Field } from 'redux-form'
-import { Link } from 'react-router'
+import { Link } from 'react-router-dom'
 import { Button, TextField } from 'xpub-ui'
 import classes from './Form.local.scss'
 
diff --git a/packages/component-authentication/src/components/index.js b/packages/component-authentication/src/components/index.js
index 1498d5d4b..09e29ab7e 100644
--- a/packages/component-authentication/src/components/index.js
+++ b/packages/component-authentication/src/components/index.js
@@ -1,4 +1,4 @@
-export { default as AuthenticatedPage } from './AuthenticatedPage'
+export { default as PrivateRoute } from './PrivateRoute'
 export { default as LoginPage } from './LoginPage'
 export { default as LogoutPage } from './LogoutPage'
 export { default as SignupPage } from './SignupPage'
diff --git a/packages/component-authentication/src/redux/currentUser.js b/packages/component-authentication/src/redux/currentUser.js
index 776b49c7c..67179d725 100644
--- a/packages/component-authentication/src/redux/currentUser.js
+++ b/packages/component-authentication/src/redux/currentUser.js
@@ -28,7 +28,7 @@ export const getCurrentUser = () => dispatch => {
   dispatch(getCurrentUserRequest())
   return api.get('/users/authenticate').then(
     user => {
-      dispatch(getCurrentUserSuccess(user))
+      return dispatch(getCurrentUserSuccess(user))
     },
     error => {
       dispatch(getCurrentUserFailure(error))
@@ -41,6 +41,7 @@ export const getCurrentUser = () => dispatch => {
 
 const initialState = {
   isFetching: false,
+  isFetched: false,
   isAuthenticated: false,
   user: null,
   error: null
@@ -51,6 +52,7 @@ export default (state = initialState, action) => {
     case GET_CURRENT_USER_REQUEST:
       return {
         isFetching: true,
+        isFetched: false,
         isAuthenticated: false,
         user: null,
         error: null,
@@ -59,6 +61,7 @@ export default (state = initialState, action) => {
     case GET_CURRENT_USER_FAILURE:
       return {
         isFetching: false,
+        isFetched: true,
         isAuthenticated: false,
         user: null,
         error: action.error
@@ -67,6 +70,7 @@ export default (state = initialState, action) => {
     case GET_CURRENT_USER_SUCCESS:
       return {
         isFetching: false,
+        isFetched: true,
         isAuthenticated: true,
         user: action.user,
         error: null
@@ -76,6 +80,7 @@ export default (state = initialState, action) => {
     case LOGOUT_SUCCESS:
       return {
         isFetching: false,
+        isFetched: false,
         isAuthenticated: false,
         user: null,
         error: null,
diff --git a/packages/component-authentication/src/redux/login.js b/packages/component-authentication/src/redux/login.js
index f0ab01b75..a448f0c1d 100644
--- a/packages/component-authentication/src/redux/login.js
+++ b/packages/component-authentication/src/redux/login.js
@@ -1,5 +1,4 @@
 import * as api from 'pubsweet-client/src/helpers/api'
-import { push } from 'react-router-redux'
 import { getCurrentUser } from './currentUser'
 
 // TODO: This will break when rendered on a server
@@ -26,14 +25,13 @@ export const loginFailure = error => ({
   error
 })
 
-export const login = (credentials, redirectTo) => dispatch => {
+export const login = (credentials) => dispatch => {
   dispatch(loginRequest())
   return api.create('/users/authenticate', credentials).then(
     user => {
       localStorage.setItem('token', user.token)
       dispatch(loginSuccess())
-      dispatch(getCurrentUser())
-      dispatch(push(redirectTo || '/'))
+      return dispatch(getCurrentUser())
     },
     error => {
       dispatch(loginFailure(error))
diff --git a/packages/component-authentication/src/redux/logout.js b/packages/component-authentication/src/redux/logout.js
index ea97faf8f..570dc989c 100644
--- a/packages/component-authentication/src/redux/logout.js
+++ b/packages/component-authentication/src/redux/logout.js
@@ -1,5 +1,3 @@
-import { push } from 'react-router-redux'
-
 /* constants */
 
 export const LOGOUT_SUCCESS = 'LOGOUT_SUCCESS'
@@ -10,8 +8,7 @@ export const logoutSuccess = () => ({
   type: LOGOUT_SUCCESS
 })
 
-export const logout = redirectTo => dispatch => {
+export const logout = () => dispatch => {
   localStorage.removeItem('token')
-  dispatch(logoutSuccess())
-  if (redirectTo) dispatch(push(redirectTo))
+  return dispatch(logoutSuccess())
 }
diff --git a/packages/component-dashboard/package.json b/packages/component-dashboard/package.json
index c54202e5a..18c6263e7 100644
--- a/packages/component-dashboard/package.json
+++ b/packages/component-dashboard/package.json
@@ -17,7 +17,7 @@
     "react": "^15.6.1",
     "react-dom": "^15.6.1",
     "react-redux": "^5.0.2",
-    "react-router": "^3.0.5",
+    "react-router-dom": "^4.2.2",
     "react-dropzone": "^4.1.2",
     "react-moment": "^0.6.1",
     "recompose": "^0.25.0",
diff --git a/packages/component-dashboard/src/components/DashboardPage.js b/packages/component-dashboard/src/components/DashboardPage.js
index 1b3b4cc4d..5b37e7530 100644
--- a/packages/component-dashboard/src/components/DashboardPage.js
+++ b/packages/component-dashboard/src/components/DashboardPage.js
@@ -1,5 +1,6 @@
 import { compose, withProps } from 'recompose'
 import { connect } from 'react-redux'
+import { withRouter } from 'react-router-dom'
 import { actions } from 'pubsweet-client'
 import { newestFirst, selectCurrentUser } from 'xpub-selectors'
 import { ConnectPage } from 'xpub-connect'
@@ -67,13 +68,14 @@ export default compose(
 
       return { collections, currentUser, conversion, dashboard }
     },
-    {
-      uploadManuscript,
-      reviewerResponse,
-      deleteProject: actions.deleteCollection,
-    }
+    (dispatch, { history }) => ({
+      uploadManuscript: acceptedFiles => dispatch(uploadManuscript(acceptedFiles, history)),
+      reviewerResponse: (project, version, reviewer, status) => dispatch(reviewerResponse(project, version, reviewer, status)),
+      deleteProject: collection => dispatch(actions.deleteCollection(collection)),
+    })
   ),
   withProps({
     AssignEditor: AssignEditorContainer
-  })
+  }),
+  withRouter,
 )(Dashboard)
diff --git a/packages/component-dashboard/src/components/ProjectLink.js b/packages/component-dashboard/src/components/ProjectLink.js
index 646c1e439..42d6362a1 100644
--- a/packages/component-dashboard/src/components/ProjectLink.js
+++ b/packages/component-dashboard/src/components/ProjectLink.js
@@ -1,5 +1,5 @@
 import React from 'react'
-import { Link } from 'react-router'
+import { Link } from 'react-router-dom'
 
 const projectUrl = ({ project, version, page, id }) => {
   const parts = []
diff --git a/packages/component-dashboard/src/redux/conversion.js b/packages/component-dashboard/src/redux/conversion.js
index cac57d9f1..8cca2741c 100644
--- a/packages/component-dashboard/src/redux/conversion.js
+++ b/packages/component-dashboard/src/redux/conversion.js
@@ -1,4 +1,3 @@
-import { push } from 'react-router-redux'
 import { actions } from 'pubsweet-client'
 import { ink as convertToHTML } from 'pubsweet-component-ink-frontend/actions'
 import uploadFile from 'xpub-upload'
@@ -27,7 +26,7 @@ export const uploadManuscriptFailure = error => ({
   error
 })
 
-export const uploadManuscript = acceptedFiles => dispatch => {
+export const uploadManuscript = (acceptedFiles, history) => dispatch => {
   if (acceptedFiles.length > 1) {
     throw new Error('Only one manuscript file can be uploaded')
   }
@@ -84,7 +83,7 @@ export const uploadManuscript = acceptedFiles => dispatch => {
 
             // redirect after a short delay
             window.setTimeout(() => {
-              dispatch(push(route))
+              history.push(route)
             }, 1000)
           })
         })
diff --git a/packages/component-manuscript/package.json b/packages/component-manuscript/package.json
index 4430213c0..724b0c737 100644
--- a/packages/component-manuscript/package.json
+++ b/packages/component-manuscript/package.json
@@ -17,7 +17,7 @@
     "react": "^15.6.1",
     "react-dom": "^15.6.1",
     "react-redux": "^5.0.2",
-    "react-router": "^3.0.5",
+    "react-router-dom": "^4.2.2",
     "recompose": "^0.25.0"
   },
   "devDependencies": {
@@ -43,7 +43,7 @@
     "react": "^15.6.1",
     "react-dom": "^15.6.1",
     "react-redux": "^5.0.2",
-    "react-router": "^3.0.5",
+    "react-router-dom": "^4.2.2",
     "xpub-styleguide": "^0.0.2"
   },
   "scripts": {
diff --git a/packages/component-manuscript/src/components/Manuscript.js b/packages/component-manuscript/src/components/Manuscript.js
index 32089ec4d..113bc4b18 100644
--- a/packages/component-manuscript/src/components/Manuscript.js
+++ b/packages/component-manuscript/src/components/Manuscript.js
@@ -1,17 +1,17 @@
 import React from 'react'
-import { browserHistory } from 'react-router'
+import { withRouter } from 'react-router-dom'
 import SimpleEditor from 'wax-editor-react'
 import classes from './Manuscript.local.scss'
 
 // TODO: convert user teams to roles (see SimpleEditorWrapper)?
 
-const Manuscript = ({ content, currentUser, fileUpload, updateManuscript }) => (
+const Manuscript = ({ content, currentUser, fileUpload, history, updateManuscript }) => (
   <SimpleEditor
     classes={classes.fullscreen}
     content={content}
     user={currentUser}
     fileUpload={fileUpload}
-    history={browserHistory}
+    history={history}
     readOnly={false}
     trackChanges={false}
     update={data => updateManuscript(data)}
@@ -19,4 +19,4 @@ const Manuscript = ({ content, currentUser, fileUpload, updateManuscript }) => (
   />
 )
 
-export default Manuscript
+export default withRouter(Manuscript)
diff --git a/packages/component-manuscript/src/components/ManuscriptPage.js b/packages/component-manuscript/src/components/ManuscriptPage.js
index 40511f6de..ef878cd06 100644
--- a/packages/component-manuscript/src/components/ManuscriptPage.js
+++ b/packages/component-manuscript/src/components/ManuscriptPage.js
@@ -6,15 +6,15 @@ import { selectCurrentUser, selectCollection, selectFragment } from 'xpub-select
 import Manuscript from './Manuscript'
 
 export default compose(
-  ConnectPage(({ params }) => [
-    actions.getCollection({ id: params.project }),
-    actions.getFragment({ id: params.project }, { id: params.version })
+  ConnectPage(({ match }) => [
+    actions.getCollection({ id: match.params.project }),
+    actions.getFragment({ id: match.params.project }, { id: match.params.version })
   ]),
   connect(
-    (state, { params }) => {
+    (state, { match }) => {
       const currentUser = selectCurrentUser(state)
-      const project = selectCollection(state, params.project)
-      const version = selectFragment(state, params.version)
+      const project = selectCollection(state, match.params.project)
+      const version = selectFragment(state, match.params.version)
 
       const content = version.source // TODO: load from a file
 
diff --git a/packages/component-review/package.json b/packages/component-review/package.json
index 01c9ea64b..7b0307711 100644
--- a/packages/component-review/package.json
+++ b/packages/component-review/package.json
@@ -18,8 +18,7 @@
     "react-dom": "^15.6.1",
     "react-moment": "^0.6.1",
     "react-redux": "^5.0.2",
-    "react-router": "^3.0.5",
-    "react-router-redux": "^4.0.7",
+    "react-router-dom": "^4.2.2",
     "react-select": "^1.0.0-rc.10",
     "recompose": "^0.25.0",
     "redux": "^3.6.0",
@@ -58,7 +57,7 @@
     "react": "^15.6.1",
     "react-dom": "^15.6.1",
     "react-redux": "^5.0.2",
-    "react-router": "^3.0.5"
+    "react-router-dom": "^4.2.2"
   },
   "scripts": {
     "styleguide": "styleguidist server",
diff --git a/packages/component-review/src/components/DecisionPage.js b/packages/component-review/src/components/DecisionPage.js
index 2f0c4a4ca..795400cbb 100644
--- a/packages/component-review/src/components/DecisionPage.js
+++ b/packages/component-review/src/components/DecisionPage.js
@@ -1,7 +1,7 @@
 import { debounce, pick } from 'lodash'
 import { compose, withProps } from 'recompose'
 import { connect } from 'react-redux'
-import { push } from 'react-router-redux'
+import { withRouter } from 'react-router-dom'
 import { reduxForm, SubmissionError } from 'redux-form'
 import { actions } from 'pubsweet-client'
 import { ConnectPage } from 'xpub-connect'
@@ -57,7 +57,7 @@ const handleDecision = (project, version) => dispatch => {
   })
 }
 
-const onSubmit = (values, dispatch, { project, version }) => {
+const onSubmit = (values, dispatch, { project, version, history }) => {
   console.log('submit', values)
 
   version.decision = {
@@ -69,7 +69,7 @@ const onSubmit = (values, dispatch, { project, version }) => {
 
   return dispatch(handleDecision(project, version)).then(() => {
     // TODO: show "thanks for your review" message
-    dispatch(push('/'))
+    history.push('/')
   }).catch(error => {
     if (error.validationErrors) {
       throw new SubmissionError()
@@ -95,15 +95,15 @@ const onChange = (values, dispatch, { project, version }) => {
 }
 
 export default compose(
-  ConnectPage(({params}) => [
-    actions.getCollection({id: params.project}),
-    actions.getFragments({id: params.project}),
+  ConnectPage(({ match }) => [
+    actions.getCollection({id: match.params.project}),
+    actions.getFragments({id: match.params.project}),
   ]),
   connect(
-    (state, { params }) => {
-      const project = selectCollection(state, params.project)
+    (state, { match }) => {
+      const project = selectCollection(state, match.params.project)
       const versions = selectFragments(state, project.fragments)
-      const version = selectFragment(state, params.version)
+      const version = selectFragment(state, match.params.version)
       const currentVersion = selectCurrentVersion(state, project)
 
       return { project, versions, version, currentVersion }
@@ -112,6 +112,7 @@ export default compose(
       uploadFile
     }
   ),
+  withRouter,
   withProps(({decision}) => {
     return {
       initialValues: decision
diff --git a/packages/component-review/src/components/ReviewPage.js b/packages/component-review/src/components/ReviewPage.js
index befd3481d..485ee1d98 100644
--- a/packages/component-review/src/components/ReviewPage.js
+++ b/packages/component-review/src/components/ReviewPage.js
@@ -1,7 +1,7 @@
 import { debounce } from 'lodash'
 import { compose, withProps } from 'recompose'
 import { connect } from 'react-redux'
-import { push } from 'react-router-redux'
+import { withRouter } from 'react-router-dom'
 import { reduxForm, SubmissionError } from 'redux-form'
 import { actions } from 'pubsweet-client'
 import { ConnectPage } from 'xpub-connect'
@@ -9,7 +9,7 @@ import { selectCurrentUser, selectCollection, selectFragments, selectCurrentVers
 import uploadFile from 'xpub-upload'
 import ReviewLayout from './review/ReviewLayout'
 
-const onSubmit = (values, dispatch, { project, version, reviewer }) => {
+const onSubmit = (values, dispatch, { history, project, version, reviewer }) => {
   console.log('submit', values)
 
   Object.assign(reviewer, {
@@ -24,7 +24,7 @@ const onSubmit = (values, dispatch, { project, version, reviewer }) => {
     reviewers: version.reviewers
   })).then(() => {
     // TODO: show "thanks for your review" message
-    dispatch(push(`/`))
+    history.push('/')
   }).catch(error => {
     if (error.validationErrors) {
       throw new SubmissionError()
@@ -50,23 +50,23 @@ const onChange = (values, dispatch, { project, version, reviewer }) => {
 }
 
 export default compose(
-  ConnectPage(({ params }) => [
-    actions.getCollection({ id: params.project }),
-    actions.getFragments({ id: params.project }),
+  ConnectPage(({ match }) => [
+    actions.getCollection({ id: match.params.project }),
+    actions.getFragments({ id: match.params.project }),
     actions.getTeams(),
     actions.getUsers(),
   ]),
   connect(
-    (state, { params }) => {
+    (state, { match }) => {
       const currentUser = selectCurrentUser(state)
-      const project = selectCollection(state, params.project)
+      const project = selectCollection(state, match.params.project)
       const versions = selectFragments(state, project.fragments)
-      const version = selectFragment(state, params.version)
+      const version = selectFragment(state, match.params.version)
       const currentVersion = selectCurrentVersion(state, project)
 
       const handlingEditors = state.teams.find(team => (
         team.object.type === 'collection'
-          && team.object.id === params.project
+          && team.object.id === match.params.project
           && team.teamType.name === 'handlingEditor'
       )).members.map(id => selectUser(state, id))
 
@@ -78,6 +78,7 @@ export default compose(
       uploadFile
     }
   ),
+  withRouter,
   withProps(({ reviewer }) => {
     return {
       initialValues: reviewer,
diff --git a/packages/component-review/src/components/ReviewersPage.js b/packages/component-review/src/components/ReviewersPage.js
index 1ece1a9b3..c1e814c3d 100644
--- a/packages/component-review/src/components/ReviewersPage.js
+++ b/packages/component-review/src/components/ReviewersPage.js
@@ -9,17 +9,17 @@ import ReviewerFormContainer from './reviewers/ReviewerFormContainer'
 import ReviewerContainer from './reviewers/ReviewerContainer'
 
 export default compose(
-  ConnectPage(({ params }) => [
-    actions.getCollection({ id: params.project }),
-    actions.getFragments({ id: params.project }),
+  ConnectPage(({ match }) => [
+    actions.getCollection({ id: match.params.project }),
+    actions.getFragments({ id: match.params.project }),
     // actions.getTeams(),
     actions.getUsers(),
-    // actions.getFragment({ id: params.project }, { id: params.version }),
+    // actions.getFragment({ id: match.params.project }, { id: match.params.version }),
   ]),
   connect(
-    (state, ownProps) => {
-      const project = selectCollection(state, ownProps.params.project)
-      const version = selectFragment(state, ownProps.params.version)
+    (state, { match }) => {
+      const project = selectCollection(state, match.params.project)
+      const version = selectFragment(state, match.params.version)
       const reviewers = (version.reviewers || []).filter(reviewer => reviewer.reviewer)
 
       const reviewerUsers = state.users.users
diff --git a/packages/component-submit/package.json b/packages/component-submit/package.json
index 93c982af8..fddba7d55 100644
--- a/packages/component-submit/package.json
+++ b/packages/component-submit/package.json
@@ -16,8 +16,7 @@
     "react": "^15.6.1",
     "react-dom": "^15.6.1",
     "react-redux": "^5.0.2",
-    "react-router": "^3.0.5",
-    "react-router-redux": "^4.0.7",
+    "react-router-dom": "^4.2.2",
     "recompose": "^0.25.0",
     "redux": "^3.6.0",
     "redux-form": "^7.0.3",
@@ -53,7 +52,7 @@
     "react": "^15.6.1",
     "react-dom": "^15.6.1",
     "react-redux": "^5.0.2",
-    "react-router": "^3.0.5"
+    "react-router-dom": "^4.2.2"
   },
   "scripts": {
     "styleguide": "styleguidist server",
diff --git a/packages/component-submit/src/components/Submit.js b/packages/component-submit/src/components/Submit.js
index 756fb354b..585c0d5f5 100644
--- a/packages/component-submit/src/components/Submit.js
+++ b/packages/component-submit/src/components/Submit.js
@@ -1,6 +1,6 @@
 import React from 'react'
 import classnames from 'classnames'
-import { Link } from 'react-router'
+import { Link } from 'react-router-dom'
 import { Button } from 'xpub-ui'
 import Metadata from './Metadata'
 import Declarations from './Declarations'
diff --git a/packages/component-submit/src/components/SubmitPage.js b/packages/component-submit/src/components/SubmitPage.js
index 9e1d50bab..f4994d268 100644
--- a/packages/component-submit/src/components/SubmitPage.js
+++ b/packages/component-submit/src/components/SubmitPage.js
@@ -1,7 +1,6 @@
 import { pick, debounce } from 'lodash'
 import { compose, withProps, withState, withHandlers } from 'recompose'
 import { connect } from 'react-redux'
-import { push } from 'react-router-redux'
 import { reduxForm, SubmissionError } from 'redux-form'
 import { actions } from 'pubsweet-client'
 import uploadFile from 'xpub-upload'
@@ -9,22 +8,22 @@ import { ConnectPage } from 'xpub-connect'
 import { selectCollection, selectFragment } from 'xpub-selectors'
 import Submit from './Submit'
 
-const onSubmit = (values, dispatch, props) => {
+const onSubmit = (values, dispatch, { history, project, version }) => {
   console.log('submit', values)
 
-  return dispatch(actions.updateFragment(props.project, {
-    id: props.version.id,
-    rev: props.version.rev,
+  return dispatch(actions.updateFragment(project, {
+    id: version.id,
+    rev: version.rev,
     submitted: new Date(),
     ...values
   })).then(() => {
     return dispatch(actions.updateCollection({
-      id: props.project.id,
-      rev: props.project.rev,
+      id: project.id,
+      rev: project.rev,
       status: 'submitted'
     }))
   }).then(() => {
-    dispatch(push(`/`))
+    history.push('/')
   }).catch(error => {
     if (error.validationErrors) {
       throw new SubmissionError()
@@ -33,12 +32,12 @@ const onSubmit = (values, dispatch, props) => {
 }
 
 // TODO: redux-form doesn't have an onBlur handler(?)
-const onChange = (values, dispatch, props) => {
+const onChange = (values, dispatch, { project, version }) => {
   console.log('change', values)
 
-  return dispatch(actions.updateFragment(props.project, {
-    id: props.version.id,
-    rev: props.version.rev,
+  return dispatch(actions.updateFragment(project, {
+    id: version.id,
+    rev: version.rev,
     // submitted: false,
     ...values
   }))
@@ -47,14 +46,14 @@ const onChange = (values, dispatch, props) => {
 }
 
 export default compose(
-  ConnectPage(({ params }) => [
-    actions.getCollection({ id: params.project }),
-    actions.getFragment({ id: params.project }, { id: params.version })
+  ConnectPage(({ match }) => [
+    actions.getCollection({ id: match.params.project }),
+    actions.getFragment({ id: match.params.project }, { id: match.params.version })
   ]),
   connect(
-    (state, { params }) => {
-      const project = selectCollection(state, params.project)
-      const version = selectFragment(state, params.version)
+    (state, { match }) => {
+      const project = selectCollection(state, match.params.project)
+      const version = selectFragment(state, match.params.version)
 
       return { project, version }
     },
diff --git a/packages/xpub-collabra/app/Root.js b/packages/xpub-collabra/app/Root.js
deleted file mode 100644
index cb05fdaa2..000000000
--- a/packages/xpub-collabra/app/Root.js
+++ /dev/null
@@ -1,19 +0,0 @@
-import React from 'react'
-import { Provider as StoreProvider } from 'react-redux'
-import { Router, browserHistory } from 'react-router'
-import { syncHistoryWithStore } from 'react-router-redux'
-import { configureStore } from 'pubsweet-client'
-import { JournalProvider } from 'xpub-journal'
-
-const store = configureStore(browserHistory, {})
-const history = syncHistoryWithStore(browserHistory, store)
-
-export default ({ routes, journal }) => (
-  <StoreProvider store={store}>
-    <JournalProvider journal={journal}>
-      <Router history={history}>
-        {routes}
-      </Router>
-    </JournalProvider>
-  </StoreProvider>
-)
diff --git a/packages/xpub-collabra/app/app.js b/packages/xpub-collabra/app/app.js
index 7942f76e6..4a272279a 100644
--- a/packages/xpub-collabra/app/app.js
+++ b/packages/xpub-collabra/app/app.js
@@ -1,24 +1,37 @@
 import React from 'react'
 import ReactDOM from 'react-dom'
 import { AppContainer } from 'react-hot-loader'
-import Root from './Root'
-import routes from './routes'
+import { Provider as StoreProvider } from 'react-redux'
+import { Router } from 'react-router-dom'
+import { configureStore } from 'pubsweet-client'
+import createHistory from 'history/createBrowserHistory'
+import { JournalProvider } from 'xpub-journal'
 import * as journal from './config/journal'
+import Root from './routes'
 import 'xpub-fonts'
 
-const render = routes => {
+const history = createHistory()
+const store = configureStore(history, {})
+
+const render = () => {
   ReactDOM.render(
     <AppContainer>
-      <Root routes={routes} journal={journal}/>
+      <StoreProvider store={store}>
+        <JournalProvider journal={journal}>
+          <Router history={history}>
+            <Root/>
+          </Router>
+        </JournalProvider>
+      </StoreProvider>
     </AppContainer>,
     document.getElementById('root')
   )
 }
 
-render(routes)
+render()
 
 if (module.hot) {
   module.hot.accept('./routes', () => {
-    render(routes)
+    render()
   })
 }
diff --git a/packages/xpub-collabra/app/routes.js b/packages/xpub-collabra/app/routes.js
index 8e60fa488..b66c227d2 100644
--- a/packages/xpub-collabra/app/routes.js
+++ b/packages/xpub-collabra/app/routes.js
@@ -1,8 +1,15 @@
 import React from 'react'
-import { Redirect, Route } from 'react-router'
+import { Route, withRouter } from 'react-router-dom'
 import loadable from 'loadable-components'
+
 import { App } from 'pubsweet-component-xpub-app/src/components'
-import { AuthenticatedPage, SignupPage, LoginPage, LogoutPage } from 'pubsweet-component-xpub-authentication/src/components'
+
+import {
+  PrivateRoute,
+  SignupPage,
+  LoginPage,
+  LogoutPage
+} from 'pubsweet-component-xpub-authentication/src/components'
 
 const DashboardPage = loadable(() =>
   import('pubsweet-component-xpub-dashboard/src/components/DashboardPage'))
@@ -22,23 +29,24 @@ const ReviewPage = loadable(() =>
 const DecisionPage = loadable(() =>
   import('pubsweet-component-xpub-review/src/components/DecisionPage'))
 
-export default (
-  <Route>
-    <Redirect from="/" to="/dashboard"/>
-
-    <Route path="/" component={App}>
-      <Route component={AuthenticatedPage}>
-        <Route path="dashboard" component={DashboardPage}/>
-        <Route path="projects/:project/versions/:version/submit" component={SubmitPage}/>
-        <Route path="projects/:project/versions/:version/manuscript" component={ManuscriptPage}/>
-        <Route path="projects/:project/versions/:version/reviewers" component={ReviewersPage}/>
-        <Route path="projects/:project/versions/:version/reviews/:review" component={ReviewPage}/>
-        <Route path="projects/:project/versions/:version/decisions/:decision" component={DecisionPage}/>
-      </Route>
-
-      <Route path="signup" component={SignupPage}/>
-      <Route path="login" component={LoginPage}/>
-      <Route path="logout" component={LogoutPage}/>
-    </Route>
-  </Route>
+// TODO: use componentDidMount to fetch the current user before rendering?
+
+const Root = () => (
+  <App>
+    <PrivateRoute exact path="/" component={DashboardPage}/>
+    <PrivateRoute exact path="/projects/:project/versions/:version/submit" component={SubmitPage}/>
+    <PrivateRoute exact path="/projects/:project/versions/:version/manuscript" component={ManuscriptPage}/>
+    <PrivateRoute exact path="/projects/:project/versions/:version/reviewers" component={ReviewersPage}/>
+    <PrivateRoute exact path="/projects/:project/versions/:version/reviews/:review" component={ReviewPage}/>
+    <PrivateRoute exact path="/projects/:project/versions/:version/decisions/:decision" component={DecisionPage}/>
+
+    <PrivateRoute exact path="/logout" component={LogoutPage}/>
+
+    <Route exact path="/signup" component={SignupPage}/>
+    <Route exact path="/login" component={LoginPage}/>
+
+    {/*<Redirect from="/" to="/dashboard"/>*/}
+  </App>
 )
+
+export default withRouter(Root)
diff --git a/packages/xpub-collabra/package.json b/packages/xpub-collabra/package.json
index 1093daa57..d253365a8 100644
--- a/packages/xpub-collabra/package.json
+++ b/packages/xpub-collabra/package.json
@@ -9,12 +9,13 @@
   },
   "dependencies": {
     "babel-core": "^6.26.0",
-    "fs-extra": "^4.0.2",
+    "config": "^1.26.2",
     "font-awesome": "^4.7.0",
+    "fs-extra": "^4.0.2",
+    "history": "^4.7.2",
     "joi": "^10.4.1",
     "loadable-components": "^0.2.1",
     "moment": "^2.18.1",
-    "config": "^1.26.2",
     "prop-types": "^15.5.10",
     "pubsweet": "^1.0.0-beta.2",
     "pubsweet-client": "^1.0.0-beta.7",
@@ -31,8 +32,7 @@
     "react-dom": "^15.6.1",
     "react-loadable": "^4.0.3",
     "react-redux": "^5.0.2",
-    "react-router": "^3.0.5",
-    "react-router-redux": "^4.0.7",
+    "react-router-dom": "^4.2.2",
     "recompose": "^0.25.0",
     "redux": "^3.6.0",
     "redux-form": "^7.0.3",
@@ -55,13 +55,13 @@
     "html-webpack-plugin": "^2.24.0",
     "joi-browser": "^10.0.6",
     "node-sass": "^4.5.3",
-    "react-hot-loader": "^3.0.0-beta.6",
+    "react-hot-loader": "^3.1.1",
     "sass-loader": "^6.0.6",
     "string-replace-loader": "^1.3.0",
     "style-loader": "^0.18.2",
-    "webpack": "^2.3.2",
+    "webpack": "^3.8.1",
     "webpack-dev-middleware": "^1.12.0",
-    "webpack-hot-middleware": "^2.18.1"
+    "webpack-hot-middleware": "^2.20.0"
   },
   "scripts": {
     "setupdb": "pubsweet setupdb ./",
diff --git a/packages/xpub-connect/package.json b/packages/xpub-connect/package.json
index f106ae5ff..a0e4f479b 100644
--- a/packages/xpub-connect/package.json
+++ b/packages/xpub-connect/package.json
@@ -12,8 +12,7 @@
     "react": "^15.6.1",
     "react-dom": "^15.6.1",
     "react-redux": "^5.0.2",
-    "react-router": "^3.0.5",
-    "react-router-redux": "^4.0.7",
+    "react-router-dom": "^4.2.2",
     "recompose": "^0.25.0",
     "redux": "^3.6.0"
   },
@@ -21,8 +20,7 @@
     "react": "^15.6.1",
     "react-dom": "^15.6.1",
     "react-redux": "^5.0.2",
-    "react-router": "^3.0.5",
-    "react-router-redux": "^4.0.7",
+    "react-router-dom": "^4.2.2",
     "redux": "^3.6.0"
   }
 }
diff --git a/packages/xpub-connect/src/components/ConnectPage.js b/packages/xpub-connect/src/components/ConnectPage.js
index fbfb91e63..8d89bd3e2 100644
--- a/packages/xpub-connect/src/components/ConnectPage.js
+++ b/packages/xpub-connect/src/components/ConnectPage.js
@@ -1,7 +1,7 @@
 import React from 'react'
 import { compose } from 'recompose'
 import { connect } from 'react-redux'
-import { withRouter } from 'react-router'
+import { withRouter } from 'react-router-dom'
 import classes from './ConnectPage.local.scss'
 
 const ConnectPage = requirements => WrappedComponent => {
@@ -68,12 +68,12 @@ const ConnectPage = requirements => WrappedComponent => {
   }
 
   return compose(
+    withRouter,
     connect(
       state => ({
         isAuthenticated: state.currentUser.isAuthenticated
       })
-    ),
-    withRouter
+    )
   )(ConnectedComponent)
 }
 
diff --git a/packages/xpub-ui/package.json b/packages/xpub-ui/package.json
index 6d247574f..6528d9157 100644
--- a/packages/xpub-ui/package.json
+++ b/packages/xpub-ui/package.json
@@ -16,7 +16,7 @@
     "react-dom": "^15.6.1",
     "react-feather": "^1.0.7",
     "react-redux": "^5.0.2",
-    "react-router": "^3.0.5",
+    "react-router-dom": "^4.2.2",
     "react-tag-autocomplete": "^5.4.1",
     "recompose": "^0.25.0",
     "redux": "^3.6.0",
diff --git a/packages/xpub-ui/src/molecules/AppBar.js b/packages/xpub-ui/src/molecules/AppBar.js
index 5373dcbe4..4af63917e 100644
--- a/packages/xpub-ui/src/molecules/AppBar.js
+++ b/packages/xpub-ui/src/molecules/AppBar.js
@@ -1,5 +1,5 @@
 import React from 'react'
-import { Link } from 'react-router'
+import { Link } from 'react-router-dom'
 import classnames from 'classnames'
 import classes from './AppBar.local.scss'
 import Icon from '../atoms/Icon'
diff --git a/packages/xpub-ui/test/AppBar.test.js b/packages/xpub-ui/test/AppBar.test.js
index cefb04773..ce92d01be 100644
--- a/packages/xpub-ui/test/AppBar.test.js
+++ b/packages/xpub-ui/test/AppBar.test.js
@@ -1,7 +1,7 @@
 import React from 'react'
 import { clone } from 'lodash'
 import { shallow } from 'enzyme'
-import { Link } from 'react-router'
+import { Link } from 'react-router-dom'
 import renderer from 'react-test-renderer'
 
 import AppBar from '../src/molecules/AppBar'
diff --git a/packages/xpub-upload/package.json b/packages/xpub-upload/package.json
index 48666619e..c4edb4c34 100644
--- a/packages/xpub-upload/package.json
+++ b/packages/xpub-upload/package.json
@@ -13,8 +13,7 @@
     "react": "^15.6.1",
     "react-dom": "^15.6.1",
     "react-redux": "^5.0.2",
-    "react-router": "^3.0.5",
-    "react-router-redux": "^4.0.7",
+    "react-router-dom": "^4.2.2",
     "recompose": "^0.25.0",
     "redux": "^3.6.0"
   },
@@ -22,8 +21,7 @@
     "react": "^15.6.1",
     "react-dom": "^15.6.1",
     "react-redux": "^5.0.2",
-    "react-router": "^3.0.5",
-    "react-router-redux": "^4.0.7",
+    "react-router-dom": "^4.2.2",
     "redux": "^3.6.0"
   }
 }
diff --git a/packages/xpub-upload/src/upload.js b/packages/xpub-upload/src/upload.js
index 842f7eebf..bd08981d9 100644
--- a/packages/xpub-upload/src/upload.js
+++ b/packages/xpub-upload/src/upload.js
@@ -1,5 +1,3 @@
-/* global CONFIG */
-
 import endpoint from 'pubsweet-client/src/helpers/endpoint'
 import token from 'pubsweet-client/src/helpers/token'
 
-- 
GitLab