diff --git a/packages/component-app/package.json b/packages/component-app/package.json
index bea5558d44b5f7445db2a8acfc1f85ed755ba121..19d61fe3ab866821becb8d0924c1dd8f0b83780c 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 9d0a249adbad25301434b7a96bca7e5414b02aa0..a65975f6d2d0332bed6370171577ef49bb6ea861 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 4f72e541ba75e59b9493072211191b48271876ed..343c9fdf5c5151f2bc1589af5bfc84400c5d0f8e 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 f2a62da9d85013e274b441e6b4e5fb7e4aa67471..0000000000000000000000000000000000000000
--- 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 fbcd62a73f72c491f6cf7fe9104e2efac2d7251c..0113eac6a850f87bec63e942d5fac29e54e72f6a 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 efda21f783e1599cab1dafd099c7d11b6a1134a5..840b42c48f6cf2a51d1405fd65ea5eb7695fa29f 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 0000000000000000000000000000000000000000..4a00b6073980492708c5569a04c7639a5ff8ef0d
--- /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 2f71a070092c51ad079cfe624fb48653c86d46d3..3c698b42ed708adfd6b48dd17c272455da3c97ac 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 1498d5d4bc54fc883c6ac4dbcde57e853c8ddc60..09e29ab7e68f3f486663c7aea776432680211d4c 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 776b49c7c462e7e6498bad204ee664668f727b4c..67179d725b0fe48ee99e11c36250a87ab85b9a72 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 f0ab01b75a96966b9ee54b3e87f6f1be62860191..a448f0c1d733e13c2a0a4ba6479918a1018c1216 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 ea97faf8fd33cb20c7edad5ef2ba4657fc76fdd8..570dc989c4b524c638eae7ea9f71015ef5168be6 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 c54202e5a151e098ae38762f3eb632ccc9fee2e1..18c6263e7b6359f945c8ba3bbf8f22b27871ae3f 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 1b3b4cc4d7e8f1334c56caca95eb5e048cff292e..5b37e7530af6ff2374c7c2cf3f0b8eae41ca7e3c 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 646c1e4396750a7620cfea037bae02d5defc305b..42d6362a1d3cd3e23a73131771b6edeba212a406 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 cac57d9f185e603a05cf2188510ddf83ab37a9dc..8cca2741c0e8868e8334dc88bad2579c7021a023 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 4430213c08a905598119eb35650c0fc2284fa9b8..724b0c737e186a521561219714e828d51067efca 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 32089ec4d4599fa484c40fa5132900e0f0b1c28c..113bc4b18c02a9ca9a8e28ef801116cf75aecc8e 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 40511f6de6ce24c7c06b1bcfeeecae1a387fa2c2..ef878cd0666eefeed0837d553fa0b7ddfb148d21 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 01c9ea64ba6bef1cb5b62912ffca69269d8ed4ba..7b030771102858b41287e2558147ba0d92a3e577 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 2f0c4a4ca4a3097f4ca5b3cfbcd4666eab13aa9e..795400cbb648546bc1683f41196ff48c01fdcde5 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 befd3481da0c694fc69c72f6c1bf95a5691b9b7a..485ee1d98bb6e84500cbb0d5f42ef6e4262a692c 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 1ece1a9b3b5cfab87c9ddbdfa7d3f7548652224e..c1e814c3ddce666afe3338d52362bedacad431fc 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 93c982af89bbc1e1eb8eb4794b392c4d3436b6fe..fddba7d554c883aec8e92167a70461305b88a4f9 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 756fb354b66c93ca11d9dc4d06c9a7a67ddb8d3d..585c0d5f5a9493213f816e2547fcc1b802b2e192 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 9e1d50babf7d2e8bd398a67465021d90ddefe1e4..f4994d268dafd5ccb743bb69affb3019b1de9e00 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 cb05fdaa2a1c56adbc5f0c1352f17e3ebc558827..0000000000000000000000000000000000000000
--- 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 7942f76e6effee6ded2847d59916ffe79966e943..4a272279aafc566d58fbe7d09719cbb5135d8458 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 8e60fa488f5c44f7b4286cfa21eaa848b6bf9a08..b66c227d2bf92b9923c312d072643ce85365b26a 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 1093daa57260d1f7e11d8a0a083c77145d838a8f..d253365a84808d234b99826f502fbb4acd6c5776 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 f106ae5ff7960a609c95c4d506e9b913fef3984a..a0e4f479b419a826f864e133696f49424b9957fb 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 fbfb91e635d686d9cf56e62aadcbdf91c55a5743..8d89bd3e21855233972b04057a44942d3f11dedf 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 6d247574f994d05186391dae1d0bc737a7a24957..6528d91571e4f3b97f23fa5925859964b939ea70 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 5373dcbe44850be9e04842fd78ff6ce2ffd501a2..4af63917e70389548e029c0d42166b5329bdbf30 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 cefb04773e17917acf16a5344b1d83f30763be25..ce92d01be9567d2bfa8532c107ec50eeb963046d 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 48666619e095872d5db98d06e5554e885d799521..c4edb4c345af7d8391f25fdf200b2ec2505fbc96 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 842f7eebfa481f730c414751d4f928e37cd0cdd4..bd08981d9b957be1372e4e00ab0c9e080bd8dd24 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'