diff --git a/package.json b/package.json index a418328..04edcef 100644 --- a/package.json +++ b/package.json @@ -66,7 +66,6 @@ "express-session": "^1.12.1", "file-loader": "^0.8.5", "invariant": "^2.2.0", - "history": "1.17.0", "hoist-non-react-statics": "^1.0.3", "less": "^2.5.3", "less-loader": "^2.2.1", @@ -74,19 +73,19 @@ "map-props": "^1.0.0", "piping": "^0.3.0", "pretty-error": "^1.2.0", - "query-string": "^3.0.0", "react": "^0.14.2", "react-bootstrap": "^0.28.1", "react-helmet": "^2.2.0", "react-dom": "^0.14.1", "react-inline-css": "^2.0.0", "react-redux": "^4.0.0", - "react-router": "1.0.3", - "react-router-bootstrap": "^0.19.3", + "react-router": "2.0.0", + "react-router-bootstrap": "^0.20.1", + "react-router-redux": "^3.0.0", "redux": "^3.0.4", + "redux-async-connect": "andresgutgon/redux-async-connect#feature/deferred-first-class-citizen", "redux-form": "^3.0.12", - "redux-router": "1.0.0-beta5", - "scroll-behavior": "^0.3.0", + "scroll-behavior": "^0.3.2", "serialize-javascript": "^1.1.2", "serve-favicon": "^2.3.0", "superagent": "^1.4.0", diff --git a/src/client.js b/src/client.js index 718269e..003305b 100755 --- a/src/client.js +++ b/src/client.js @@ -4,27 +4,34 @@ import 'babel/polyfill'; import React from 'react'; import ReactDOM from 'react-dom'; -import createHistory from 'history/lib/createBrowserHistory'; -import useScroll from 'scroll-behavior/lib/useStandardScroll'; import createStore from './redux/create'; import ApiClient from './helpers/ApiClient'; import {Provider} from 'react-redux'; -import {reduxReactRouter, ReduxRouter} from 'redux-router'; + +import { Router, browserHistory } from 'react-router'; +import { ReduxAsyncConnect } from 'redux-async-connect'; +import useScroll from 'scroll-behavior/lib/useStandardScroll'; import getRoutes from './routes'; -import makeRouteHooksSafe from './helpers/makeRouteHooksSafe'; const client = new ApiClient(); -// Three differnt types of scroll behavior available. -// Documented here: https://github.com/rackt/scroll-behavior -const scrollableHistory = useScroll(createHistory); - +const history = useScroll(() => browserHistory)(); const dest = document.getElementById('content'); -const store = createStore(reduxReactRouter, makeRouteHooksSafe(getRoutes), scrollableHistory, client, window.__data); +const store = createStore(history, client, window.__data); const component = ( - + + + } + history={history} + > + {getRoutes(store)} + ); ReactDOM.render( diff --git a/src/containers/App/App.js b/src/containers/App/App.js index bd5b351..6d40587 100755 --- a/src/containers/App/App.js +++ b/src/containers/App/App.js @@ -1,31 +1,41 @@ import React, { Component, PropTypes } from 'react'; -import { bindActionCreators } from 'redux'; import { IndexLink } from 'react-router'; import { LinkContainer } from 'react-router-bootstrap'; -import { Navbar, Nav, NavItem } from 'react-bootstrap'; +import Navbar from 'react-bootstrap/lib/Navbar'; +import Nav from 'react-bootstrap/lib/Nav'; +import NavItem from 'react-bootstrap/lib/NavItem'; import { connect } from 'react-redux'; import Helmet from 'react-helmet'; import { isLoaded as isAuthLoaded, load as loadAuth, logout } from 'redux/modules/auth'; -import { pushState } from 'redux-router'; -import connectData from 'helpers/connectData'; +import { routeActions } from 'react-router-redux'; +import { asyncConnect } from 'redux-async-connect'; + import config from '../../config'; -function fetchData(getState, dispatch) { - if (!isAuthLoaded(getState())) { - return dispatch(loadAuth()); - } -} +@asyncConnect([{ + promise: (options) => { + const { + store: { dispatch, getState }, + } = options; + const promises = []; -@connectData(fetchData) + if (!isAuthLoaded(getState())) { + promises.push(dispatch(loadAuth())); + } + + return Promise.all(promises); + } +}]) @connect( - state => ({user: state.auth.user}), - dispatch => bindActionCreators({logout, pushState}, dispatch)) + state => ({user: state.auth.user}), + {logout, push: routeActions.push} +) export default class App extends Component { static propTypes = { children: PropTypes.object.isRequired, user: PropTypes.object, logout: PropTypes.func.isRequired, - pushState: PropTypes.func.isRequired + push: PropTypes.func.isRequired }; static contextTypes = { @@ -33,10 +43,12 @@ export default class App extends Component { }; componentWillReceiveProps(nextProps) { + const { push } = this.props; + if (!this.props.user && nextProps.user) { - this.props.pushState(null, '/groups'); + push('/groups'); } else if (this.props.user && !nextProps.user) { - this.props.pushState(null, '/login'); + push('/login'); // Full real page reload to clean local data window.location.reload(); } diff --git a/src/containers/DevTools/DevTools.js b/src/containers/DevTools/DevTools.js index ca930ca..feb12d8 100644 --- a/src/containers/DevTools/DevTools.js +++ b/src/containers/DevTools/DevTools.js @@ -5,8 +5,8 @@ import DockMonitor from 'redux-devtools-dock-monitor'; export default createDevTools( + toggleVisibilityKey="ctrl+H" + changePositionKey="ctrl+Q"> ); diff --git a/src/containers/Group/Base.js b/src/containers/Group/Base.js index a03dbea..45e4fe8 100644 --- a/src/containers/Group/Base.js +++ b/src/containers/Group/Base.js @@ -1,64 +1,63 @@ import _ from 'underscore'; import React, {Component, PropTypes} from 'react'; import {connect} from 'react-redux'; +import { asyncConnect } from 'redux-async-connect'; -import connectData from 'helpers/connectData'; import { loadEntity as loadGroup } from 'redux/modules/groups/groups'; import { load as loadUsers } from 'redux/modules/users/users'; import { load as loadMemberships } from 'redux/modules/groups/memberships'; -import { load as loadInvitations } from 'redux/modules/invitations/list'; import { membersWithUserSelector } from 'selectors/members'; -function fetchData(getState, dispatch, location, params) { - const { - groupsReducer: {groups: { byId }}, - usersReducer: { users }, - membershipsReducer: { memberships }, - } = getState(); - const promises = []; - const id = params.id; - const group = byId[id]; - - promises.push(dispatch(loadInvitations(id))); - - if (!users.entities.length) { - promises.push(dispatch(loadUsers())); - } - - if (!memberships.entities.length) { - promises.push(dispatch(loadMemberships())); - } - - - if (!group) { - promises.push(dispatch(loadGroup(id))); - } - - return Promise.all(promises); -} - function groupSelector(state) { return { ...membersWithUserSelector(state), - invitations: state.invitationsReducer.invitations, user: state.auth.user, }; } -@connectData(fetchData) +@asyncConnect([{ + promise: (options) => { + const { + store: { dispatch, getState }, + params, + } = options; + + const { + groupsReducer: {groups: { byId }}, + usersReducer: { users }, + membershipsReducer: { memberships }, + } = getState(); + const promises = []; + const id = params.id; + const group = byId[id]; + + if (!users.entities.length) { + promises.push(dispatch(loadUsers())); + } + + if (!memberships.entities.length) { + promises.push(dispatch(loadMemberships())); + } + + if (!group) { + promises.push(dispatch(loadGroup(id))); + } + + return Promise.all(promises); + } +}]) @connect(groupSelector, {}) export default class GroupBase extends Component { static propTypes = { children: PropTypes.object.isRequired, groups: PropTypes.object.isRequired, user: PropTypes.object, - invitations: PropTypes.object.isRequired, members: PropTypes.object.isRequired, params: PropTypes.object.isRequired, } render() { - const { user, invitations, groups, members, params: { id }} = this.props; + const { user, groups, members, params: { id }} = this.props; const group = groups.byId[id]; const membersOfGroup = members.byGroupID[group.id] || []; @@ -74,7 +73,6 @@ export default class GroupBase extends Component { group: group, currentUser: currentUser, members: membersOfGroup, - invitations: invitations, } )} diff --git a/src/containers/Group/Members/Invitations/index.js b/src/containers/Group/Members/Invitations/index.js index 696e15a..40a8ae8 100644 --- a/src/containers/Group/Members/Invitations/index.js +++ b/src/containers/Group/Members/Invitations/index.js @@ -9,9 +9,9 @@ import { isRole } from 'helpers/entities/member'; const mapStateToProps = (state) => ({ bulkErrors: state.bulkInvitationsReducer.bulkErrors, + invitations: state.invitationsReducer.invitations, invitationStatusByID: state.invitationsReducer.invitationStatusByID, }); - const mapDispatchToProps = { initialize, send, sendBulk }; @connect(mapStateToProps, mapDispatchToProps) @@ -61,7 +61,7 @@ export default class GroupMembersInvitations extends Component { invitationStatusByID, group, } = this.props; - const invitationsByGroup = invitations.byGroupID[group.id]; + const invitationsByGroup = invitations.byGroupID[group.id] || []; const isAdmin = isRole(currentUser, 'admin'); const invitationsList = invitationsByGroup.map((invitation) => { diff --git a/src/containers/Group/Members/index.js b/src/containers/Group/Members/index.js index 9929174..1d9ba9d 100644 --- a/src/containers/Group/Members/index.js +++ b/src/containers/Group/Members/index.js @@ -1,24 +1,36 @@ import React, {Component, PropTypes} from 'react'; -import DocumentMeta from 'react-document-meta'; +import Helmet from 'react-helmet'; +import { asyncConnect } from 'redux-async-connect'; +import { load as loadInvitations } from 'redux/modules/invitations/list'; import Invitations from './Invitations'; import List from './List'; +@asyncConnect([{ + deferred: true, + promise: (options) => { + const { + store: { dispatch }, + params: { id }, + } = options; + + return dispatch(loadInvitations(id)); + }, +}]) export default class GroupMembers extends Component { static propTypes = { group: PropTypes.object, currentUser: PropTypes.object, members: PropTypes.array, - invitations: PropTypes.object, } render() { - const { currentUser, group, members, invitations } = this.props; + const { currentUser, group, members } = this.props; return (
- +

Miembros

@@ -27,7 +39,6 @@ export default class GroupMembers extends Component {
diff --git a/src/containers/Invitation/Complete.js b/src/containers/Invitation/Complete.js index e287c9c..e9e0340 100644 --- a/src/containers/Invitation/Complete.js +++ b/src/containers/Invitation/Complete.js @@ -1,52 +1,46 @@ import React, {Component, PropTypes} from 'react'; import {connect} from 'react-redux'; import {initialize} from 'redux-form'; -import DocumentMeta from 'react-document-meta'; +import Helmet from 'react-helmet'; import * as invitationCompleteActions from 'redux/modules/invitations/complete'; import CompleteSignupForm from 'components/forms/signup/Complete'; @connect( state => ({ validInvitation: state.completeInvitationReducer.validInvitation, + user: state.auth.user, completeInvitationErrors: state.completeInvitationReducer.completeInvitationErrors }), {initialize, complete: invitationCompleteActions.complete}) export default class InvitationComplete extends Component { static propTypes = { initialize: PropTypes.func.isRequired, + user: PropTypes.object, complete: PropTypes.func.isRequired, params: PropTypes.object, completeInvitationErrors: PropTypes.object, validInvitation: PropTypes.bool, history: PropTypes.object, token: PropTypes.string - } - - static onEnter(nextState, replaceState, cb) { - const context = this.context; - const token = nextState.params.token; - - function goHome() { - replaceState(null, '/'); - } + }; - function signupOrHome() { - const {completeInvitationReducer: {validInvitation}} = context.getState(); + static contextTypes = { + router: React.PropTypes.object.isRequired + }; - if (!validInvitation) { - goHome(); - } + componentWillMount() { + const { validInvitation, user } = this.props; - cb(); + if (!validInvitation || user) { + this.context.router.replace('/'); } + } - if (token) { - context.dispatch(invitationCompleteActions.checkInvitation(token)) - .then(signupOrHome); - } else { - goSignup(); - cb(); - } + static reduxAsyncConnect(params, store) { + const { dispatch } = store; + const { token } = params; + + return dispatch(invitationCompleteActions.checkInvitation(token)); } handleSubmit(data) { @@ -57,7 +51,7 @@ export default class InvitationComplete extends Component { return Promise.reject(errors); } - this.props.history.pushState(null, '/groups'); + this.props.history.push('/groups'); return Promise.resolve({}); }); } @@ -67,7 +61,7 @@ export default class InvitationComplete extends Component {
- +

Finaliza el registro

- +

Crea un grupo

diff --git a/src/containers/Signup/Complete.js b/src/containers/Signup/Complete.js index f09b548..a2f4681 100644 --- a/src/containers/Signup/Complete.js +++ b/src/containers/Signup/Complete.js @@ -1,7 +1,7 @@ import React, {Component, PropTypes} from 'react'; import {connect} from 'react-redux'; import {initialize} from 'redux-form'; -import DocumentMeta from 'react-document-meta'; +import Helmet from 'react-helmet'; import * as signupCompleteActions from 'redux/modules/signup/complete'; import CompleteSignupForm from 'components/forms/signup/Complete'; @@ -22,31 +22,23 @@ export default class Complete extends Component { token: PropTypes.string } - static onEnter(nextState, replaceState, cb) { - const context = this.context; - const token = nextState.params.token; + static contextTypes = { + router: React.PropTypes.object.isRequired + }; - function goSignup() { - replaceState(null, '/signup'); - } - - function signupOrCreate() { - const {signupCompleteReducer: {validSignup}} = context.getState(); - - if (!validSignup) { - goSignup(); - } + componentWillMount() { + const { validSignup } = this.props; - cb(); + if (!validSignup) { + this.context.router.replace('/signup'); } + } - if (token) { - context.dispatch(signupCompleteActions.checkSignup(token)) - .then(signupOrCreate); - } else { - goSignup(); - cb(); - } + static reduxAsyncConnect(params, store) { + const { dispatch } = store; + const { token } = params; + + return dispatch(signupCompleteActions.checkSignup(token)); } handleSubmit(data) { @@ -67,7 +59,7 @@ export default class Complete extends Component {
- +

Finaliza el registro

- +

Signup

{successMessage} diff --git a/src/helpers/Html.js b/src/helpers/Html.js index 4929a6a..57b1c80 100644 --- a/src/helpers/Html.js +++ b/src/helpers/Html.js @@ -17,7 +17,7 @@ export default class Html extends Component { assets: PropTypes.object, component: PropTypes.object, store: PropTypes.object - } + }; render() { const {assets, component, store} = this.props; diff --git a/src/helpers/connectData.js b/src/helpers/connectData.js deleted file mode 100644 index 4529529..0000000 --- a/src/helpers/connectData.js +++ /dev/null @@ -1,24 +0,0 @@ -import React, { Component } from 'react'; -import hoistStatics from 'hoist-non-react-statics'; - -/* - Note: - When this decorator is used, it MUST be the first (outermost) decorator. - Otherwise, we cannot find and call the fetchData and fetchDataDeffered methods. - */ - -export default function connectData(fetchData, fetchDataDeferred) { - - return function wrapWithFetchData(WrappedComponent) { - class ConnectData extends Component { - render() { - return ; - } - } - - ConnectData.fetchData = fetchData; - ConnectData.fetchDataDeferred = fetchDataDeferred; - - return hoistStatics(ConnectData, WrappedComponent); - }; -} diff --git a/src/helpers/getDataDependencies.js b/src/helpers/getDataDependencies.js deleted file mode 100755 index 37f4c13..0000000 --- a/src/helpers/getDataDependencies.js +++ /dev/null @@ -1,18 +0,0 @@ -/** - * 1. Skip holes in route component chain and - * only consider components that implement - * fetchData or fetchDataDeferred - * - * 2. Pull out fetch data methods - * - * 3. Call fetch data methods and gather promises - */ -export default (components, getState, dispatch, location, params, deferred) => { - const methodName = deferred ? 'fetchDataDeferred' : 'fetchData'; - - return components - .filter((component) => component && component[methodName]) // 1 - .map((component) => component[methodName]) // 2 - .map((fetchData) => - fetchData(getState, dispatch, location, params)); // 3 -}; diff --git a/src/helpers/getStatusFromRoutes.js b/src/helpers/getStatusFromRoutes.js deleted file mode 100644 index a942cd2..0000000 --- a/src/helpers/getStatusFromRoutes.js +++ /dev/null @@ -1,9 +0,0 @@ -/** - * Return the status code from the last matched route with a status property. - * - * @param matchedRoutes - * @returns {Number|undefined} - */ -export default (matchedRoutes) => { - return matchedRoutes.reduce((prev, cur) => cur.status || prev, null); -}; diff --git a/src/helpers/makeRouteHooksSafe.js b/src/helpers/makeRouteHooksSafe.js deleted file mode 100644 index 7c9f8c5..0000000 --- a/src/helpers/makeRouteHooksSafe.js +++ /dev/null @@ -1,43 +0,0 @@ -import { createRoutes } from 'react-router/lib/RouteUtils'; - -// Wrap the hooks so they don't fire if they're called before -// the store is initialised. This only happens when doing the first -// client render of a route that has an onEnter hook -function makeHooksSafe(routes, store) { - if (Array.isArray(routes)) { - return routes.map((route) => makeHooksSafe(route, store)); - } - - const onEnter = routes.onEnter; - - if (onEnter) { - routes.onEnter = function safeOnEnter(...args) { - try { - store.getState(); - } catch (err) { - if (onEnter.length === 3) { - args[2](); - } - - // There's no store yet so ignore the hook - return; - } - - onEnter.apply(null, args); - }; - } - - if (routes.childRoutes) { - makeHooksSafe(routes.childRoutes, store); - } - - if (routes.indexRoute) { - makeHooksSafe(routes.indexRoute, store); - } - - return routes; -} - -export default function makeRouteHooksSafe(_getRoutes) { - return (store) => makeHooksSafe(createRoutes(_getRoutes(store)), store); -} diff --git a/src/redux/create.js b/src/redux/create.js index f124229..0f52727 100644 --- a/src/redux/create.js +++ b/src/redux/create.js @@ -1,30 +1,31 @@ import { createStore as _createStore, applyMiddleware, compose } from 'redux'; import createMiddleware from './middleware/clientMiddleware'; -import transitionMiddleware from './middleware/transitionMiddleware'; +import { syncHistory } from 'react-router-redux'; -export default function createStore(reduxReactRouter, getRoutes, createHistory, client, data) { - const middleware = [createMiddleware(client), transitionMiddleware]; +export default function createStore(history, client, data) { + // Sync dispatched route actions to the history + const reduxRouterMiddleware = syncHistory(history); + + const middleware = [createMiddleware(client), reduxRouterMiddleware]; let finalCreateStore; if (__DEVELOPMENT__ && __CLIENT__ && __DEVTOOLS__) { const { persistState } = require('redux-devtools'); const DevTools = require('../containers/DevTools/DevTools'); - finalCreateStore = compose( applyMiddleware(...middleware), window.devToolsExtension ? window.devToolsExtension() : DevTools.instrument(), persistState(window.location.href.match(/[?&]debug_session=([^&]+)\b/)) )(_createStore); - } else { finalCreateStore = applyMiddleware(...middleware)(_createStore); } - finalCreateStore = reduxReactRouter({ getRoutes, createHistory })(finalCreateStore); - const reducer = require('./modules/reducer'); const store = finalCreateStore(reducer, data); + reduxRouterMiddleware.listenForReplays(store); + if (__DEVELOPMENT__ && module.hot) { module.hot.accept('./modules/reducer', () => { store.replaceReducer(require('./modules/reducer')); diff --git a/src/redux/middleware/clientMiddleware.js b/src/redux/middleware/clientMiddleware.js index c9cb4a3..e1d9b8c 100644 --- a/src/redux/middleware/clientMiddleware.js +++ b/src/redux/middleware/clientMiddleware.js @@ -5,7 +5,13 @@ export default function clientMiddleware(client) { return action(dispatch, getState); } - const { promise, types, requestData, ...rest } = action; + const { + promise, + types, + requestData, + ...rest, + } = action; + if (!promise) { return next(action); } @@ -16,13 +22,16 @@ export default function clientMiddleware(client) { next({...rest, type: REQUEST, requestData: data}); - return promise(client).then( + const actionPromise = promise(client); + actionPromise.then( (result) => next({...rest, result, type: SUCCESS}), (error) => next({...rest, error, type: FAILURE}) ).catch((error)=> { console.error('MIDDLEWARE ERROR:', error); next({...rest, error, type: FAILURE}); }); + + return actionPromise; }; }; } diff --git a/src/redux/middleware/transitionMiddleware.js b/src/redux/middleware/transitionMiddleware.js deleted file mode 100644 index 13993bd..0000000 --- a/src/redux/middleware/transitionMiddleware.js +++ /dev/null @@ -1,37 +0,0 @@ -import {ROUTER_DID_CHANGE} from 'redux-router/lib/constants'; -import getDataDependencies from '../../helpers/getDataDependencies'; - -const locationsAreEqual = (locA, locB) => { - return (locA.pathname === locB.pathname) && (locA.search === locB.search); -}; - -export default ({getState, dispatch}) => next => action => { - if (action.type === ROUTER_DID_CHANGE) { - if (getState().router && locationsAreEqual(action.payload.location, getState().router.location)) { - return next(action); - } - - const {components, location, params} = action.payload; - const promise = new Promise((resolve) => { - - const doTransition = () => { - next(action); - Promise.all(getDataDependencies(components, getState, dispatch, location, params, true)) - .then(resolve, resolve); - }; - - Promise.all(getDataDependencies(components, getState, dispatch, location, params)) - .then(doTransition, doTransition); - }); - - if (__SERVER__) { - // router state is null until ReduxRouter is created so we can use this to store - // our promise to let the server know when it can render - getState().router = promise; - } - - return promise; - } - - return next(action); -}; diff --git a/src/redux/modules/reducer.js b/src/redux/modules/reducer.js index 84b100a..9cb3e86 100644 --- a/src/redux/modules/reducer.js +++ b/src/redux/modules/reducer.js @@ -13,10 +13,12 @@ import groupsReducer from './groups/groups'; import {reducer as form} from 'redux-form'; import info from './info'; import widgets from './widgets'; -import { routerStateReducer } from 'redux-router'; +import { routeReducer } from 'react-router-redux'; +import {reducer as reduxAsyncConnect} from 'redux-async-connect'; export default combineReducers({ - router: routerStateReducer, + routing: routeReducer, + reduxAsyncConnect, auth, signupCreateReducer, signupCompleteReducer, diff --git a/src/routes.js b/src/routes.js index 37916e7..ea9c99a 100755 --- a/src/routes.js +++ b/src/routes.js @@ -24,7 +24,7 @@ import { } from 'containers'; export default (store) => { - const requireLogin = (nextState, replaceState, cb) => { + const requireLogin = (nextState, replace, cb) => { /** * Check if user must be refetched. * Cases: @@ -45,7 +45,7 @@ export default (store) => { const { auth: { user }} = store.getState(); if (!user) { // oops, not logged in, so can't be here! - replaceState(null, '/login'); + replace('/login'); } cb(); } @@ -59,12 +59,12 @@ export default (store) => { } }; - const redirectToGroups = (nextState, replaceState, cb) => { + const redirectToGroups = (nextState, replace, cb) => { function goToGroups() { const {auth: { user }} = store.getState(); if (user) { - replaceState(null, '/groups'); + replace('/groups'); } cb(); @@ -78,7 +78,7 @@ export default (store) => { } }; - const redirectToGroupsDetail = (nextState, replaceState, cb) => { + const redirectToGroupsDetail = (nextState, replace, cb) => { function getGroupIds() { const {membershipsReducer: {memberships: {entities}}} = store.getState(); return _.uniq(_.pluck(entities, 'group_id')); @@ -88,9 +88,9 @@ export default (store) => { const groupIds = getGroupIds(); if (groupIds.length === 0) { - replaceState(null, '/onboarding'); + replace('/onboarding'); } else if (groupIds.length === 1) { - replaceState(null, `/groups/${groupIds[0]}`); + replace(`/groups/${groupIds[0]}`); } cb(); @@ -108,12 +108,12 @@ export default (store) => { - + {/* Routes signup */} - + diff --git a/src/server.js b/src/server.js index f18d095..00dded0 100755 --- a/src/server.js +++ b/src/server.js @@ -14,13 +14,12 @@ import Html from './helpers/Html'; import PrettyError from 'pretty-error'; import http from 'http'; -import {ReduxRouter} from 'redux-router'; -import createHistory from 'history/lib/createMemoryHistory'; -import {reduxReactRouter, match} from 'redux-router/server'; +import { match } from 'react-router'; +import { ReduxAsyncConnect, loadOnServer } from 'redux-async-connect'; +import createHistory from 'react-router/lib/createMemoryHistory'; + import {Provider} from 'react-redux'; -import qs from 'query-string'; import getRoutes from './routes'; -import getStatusFromRoutes from './helpers/getStatusFromRoutes'; const pretty = new PrettyError(); const app = new Express(); @@ -102,7 +101,8 @@ app.use((req, res, next) => { webpackIsomorphicTools.refresh(); } const client = new ApiClient(req); - const store = createStore(reduxReactRouter, getRoutes, createHistory, client); + const history = createHistory(req.originalUrl); + const store = createStore(history, client); function hydrateOnClient() { res.send('\n' + @@ -114,47 +114,38 @@ app.use((req, res, next) => { return; } - store.dispatch(match(req.originalUrl, (error, redirectLocation, routerState) => { + + match({ history, routes: getRoutes(store), location: req.originalUrl }, (error, redirectLocation, renderProps) => { if (redirectLocation) { res.redirect(redirectLocation.pathname + redirectLocation.search); } else if (error) { console.error('ROUTER ERROR:', pretty.render(error)); res.status(500); hydrateOnClient(); - } else if (!routerState) { - res.status(500); - hydrateOnClient(); - } else { - // Workaround redux-router query string issue: - // https://github.com/rackt/redux-router/issues/106 - if (routerState.location.search && !routerState.location.query) { - routerState.location.query = qs.parse(routerState.location.search); - } - - store.getState().router.then(() => { + } else if (renderProps) { + loadOnServer({...renderProps, store, helpers: {client}}).then(() => { const component = ( - + ); - const status = getStatusFromRoutes(routerState.routes); - - if (status) { - res.status(status); - } + res.status(200); + global.navigator = {userAgent: req.headers['user-agent']}; - res.send('\n' + - ReactDOM.renderToString() + res.send( + '\n' + + ReactDOM.renderToString( + + ) ); - - }).catch((err) => { - console.error('DATA FETCHING ERROR:', pretty.render(err)); - res.status(500); - hydrateOnClient(); }); + } else { + res.status(404).send('Not found'); } - })); + }); }); if (config.port) {