From d4dc11f1a960cc991293650f65ace31ebe803ee3 Mon Sep 17 00:00:00 2001 From: Pavel Lang Date: Fri, 29 Jul 2016 15:48:01 +0200 Subject: [PATCH] Isomorphic redirect --- src/actions/route.js | 9 ++++++ src/client.js | 6 ++-- src/components/App/App.js | 3 ++ src/components/Link/Link.js | 24 +++++++++++--- src/core/{history.js => createHistory.js} | 4 +-- src/server.js | 39 ++++++++++++++++++----- src/store/createHelpers.js | 1 + 7 files changed, 68 insertions(+), 18 deletions(-) create mode 100644 src/actions/route.js rename src/core/{history.js => createHistory.js} (79%) diff --git a/src/actions/route.js b/src/actions/route.js new file mode 100644 index 000000000..478abb73f --- /dev/null +++ b/src/actions/route.js @@ -0,0 +1,9 @@ + +// Pseudo action. All is handled through history module +export function redirect(descriptor) { + return (dispatch, _, { history }) => history.replace(descriptor); +} + +export function navigate(descriptor) { + return (dispatch, _, { history }) => history.push(descriptor); +} diff --git a/src/client.js b/src/client.js index dbe65ec6b..9e04aa659 100644 --- a/src/client.js +++ b/src/client.js @@ -12,7 +12,7 @@ import ReactDOM from 'react-dom'; import FastClick from 'fastclick'; import UniversalRouter from 'universal-router'; import routes from './routes'; -import history from './core/history'; +import createHistory from './core/createHistory'; import configureStore from './store/configureStore'; import { readState, saveState } from 'history/lib/DOMStateStorage'; import { @@ -90,6 +90,7 @@ function render(container, state, component) { } function run() { + const history = createHistory(); const container = document.getElementById('app'); const initialState = JSON.parse( document. @@ -101,7 +102,8 @@ function run() { // Make taps on links and buttons work fast on mobiles FastClick.attach(document.body); - context.store = configureStore(initialState, {}); + context.store = configureStore(initialState, { history }); + context.createHref = history.createHref; // Re-render the app when window.location changes function onLocationChange(location) { diff --git a/src/components/App/App.js b/src/components/App/App.js index b2fb52986..effcb3dcb 100644 --- a/src/components/App/App.js +++ b/src/components/App/App.js @@ -19,6 +19,7 @@ class App extends Component { static propTypes = { context: PropTypes.shape({ + createHref: PropTypes.func.isRequired, store: PropTypes.object.isRequired, insertCss: PropTypes.func, setTitle: PropTypes.func, @@ -29,6 +30,7 @@ class App extends Component { }; static childContextTypes = { + createHref: PropTypes.func.isRequired, insertCss: PropTypes.func.isRequired, setTitle: PropTypes.func.isRequired, setMeta: PropTypes.func.isRequired, @@ -37,6 +39,7 @@ class App extends Component { getChildContext() { const context = this.props.context; return { + createHref: context.createHref, insertCss: context.insertCss || emptyFunction, setTitle: context.setTitle || emptyFunction, setMeta: context.setMeta || emptyFunction, diff --git a/src/components/Link/Link.js b/src/components/Link/Link.js index a60ee19ab..8877c0441 100644 --- a/src/components/Link/Link.js +++ b/src/components/Link/Link.js @@ -8,7 +8,8 @@ */ import React, { Component, PropTypes } from 'react'; -import history from '../../core/history'; +import { connect } from 'react-redux'; +import { navigate } from '../../actions/route'; function isLeftClickEvent(event) { return event.button === 0; @@ -23,6 +24,13 @@ class Link extends Component { // eslint-disable-line react/prefer-stateless-fun static propTypes = { to: PropTypes.oneOfType([PropTypes.string, PropTypes.object]).isRequired, onClick: PropTypes.func, + + // actions + navigate: PropTypes.func, + }; + + static contextTypes = { + createHref: PropTypes.func.isRequired, }; handleClick = (event) => { @@ -44,9 +52,9 @@ class Link extends Component { // eslint-disable-line react/prefer-stateless-fun if (allowTransition) { if (this.props.to) { - history.push(this.props.to); + this.props.navigate(this.props.to); } else { - history.push({ + this.props.navigate({ pathname: event.currentTarget.pathname, search: event.currentTarget.search, }); @@ -56,9 +64,15 @@ class Link extends Component { // eslint-disable-line react/prefer-stateless-fun render() { const { to, ...props } = this.props; // eslint-disable-line no-use-before-define - return ; + return ; } } -export default Link; +const mapState = null; + +const mapDispatch = { + navigate, +}; + +export default connect(mapState, mapDispatch)(Link); diff --git a/src/core/history.js b/src/core/createHistory.js similarity index 79% rename from src/core/history.js rename to src/core/createHistory.js index c7458a8ce..a236f601d 100644 --- a/src/core/history.js +++ b/src/core/createHistory.js @@ -11,6 +11,4 @@ import createHistory from 'history/lib/createBrowserHistory'; import createMemoryHistory from 'history/lib/createMemoryHistory'; import useQueries from 'history/lib/useQueries'; -const history = useQueries(process.env.BROWSER ? createHistory : createMemoryHistory)(); - -export default history; +export default useQueries(process.env.BROWSER ? createHistory : createMemoryHistory); diff --git a/src/server.js b/src/server.js index c2c0edf7a..afad18a0c 100644 --- a/src/server.js +++ b/src/server.js @@ -26,6 +26,7 @@ import passport from './core/passport'; import models from './data/models'; import schema from './data/schema'; import routes from './routes'; +import createHistory from './core/createHistory'; import assets from './assets'; // eslint-disable-line import/no-unresolved import configureStore from './store/configureStore'; import { setRuntimeVariable } from './actions/runtime'; @@ -85,13 +86,31 @@ app.use('/graphql', expressGraphQL(req => ({ // Register server-side rendering middleware // ----------------------------------------------------------------------------- app.get('*', async (req, res, next) => { - try { - let css = []; - let statusCode = 200; - const data = { title: '', description: '', style: '', script: assets.main.js, children: '' }; + let css = []; + let statusCode = 200; + const data = { title: '', description: '', style: '', script: assets.main.js, children: '' }; + + const history = createHistory(req.url); + // let currentLocation = history.getCurrentLocation(); + let sent = false; + const removeHistoryListener = history.listen(location => { + const newUrl = `${location.pathname}${location.search}`; + if (req.originalUrl !== newUrl) { + // console.log(`R ${req.originalUrl} -> ${newUrl}`); // eslint-disable-line no-console + if (!sent) { + res.redirect(303, newUrl); + sent = true; + next(); + } else { + console.error(`${req.path}: Already sent!`); // eslint-disable-line no-console + } + } + }); + try { const store = configureStore({}, { cookie: req.headers.cookie, + history, }); store.dispatch(setRuntimeVariable({ @@ -104,6 +123,7 @@ app.get('*', async (req, res, next) => { query: req.query, context: { store, + createHref: history.createHref, insertCss: (...styles) => { styles.forEach(style => css.push(style._getCss())); // eslint-disable-line no-underscore-dangle, max-len }, @@ -120,12 +140,15 @@ app.get('*', async (req, res, next) => { }, }); - const html = ReactDOM.renderToStaticMarkup(); - - res.status(statusCode); - res.send(`${html}`); + if (!sent) { + const html = ReactDOM.renderToStaticMarkup(); + res.status(statusCode); + res.send(`${html}`); + } } catch (err) { next(err); + } finally { + removeHistoryListener(); } }); diff --git a/src/store/createHelpers.js b/src/store/createHelpers.js index bd1142476..400d66992 100644 --- a/src/store/createHelpers.js +++ b/src/store/createHelpers.js @@ -46,5 +46,6 @@ export default function createHelpers(config) { return { fetch: fetchKnowingCookie, graphqlRequest, + history: config.history, }; }