Created
May 30, 2017 17:45
-
-
Save jaredpalmer/a73bc00cac8926ff0ad5281879b1eb90 to your computer and use it in GitHub Desktop.
Next.js-like SSR without Next.js.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
import React from 'react'; | |
import Route from 'react-router-dom/Route'; | |
import Link from 'react-router-dom/Link'; | |
import Switch from 'react-router-dom/Switch'; | |
const App = ({ routes, initialData }) => { | |
return routes | |
? <div> | |
<Switch> | |
{routes.map((route, index) => { | |
// pass in the initialData from the server or window.DATA for this | |
// specific route | |
return ( | |
<Route | |
key={index} | |
path={route.path} | |
exact | |
render={props => | |
React.createElement(route.component, { | |
...props, | |
initialData: initialData[index] || null, | |
})} | |
/> | |
); | |
})} | |
</Switch> | |
</div> | |
: null; | |
}; | |
export default App; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
import React, { Component } from 'react'; | |
import Helmet from 'react-helmet'; | |
import withSSR from './withSSR'; | |
import Nav from '../components/Nav'; | |
class Page extends Component { | |
static async getInitialProps({ match, req, res, axios }) { | |
try { | |
const { | |
data, | |
} = await axios.get( | |
`https://xxx.org/wp-json/minipress/v1/path/${match.params.slug}?_embed`, | |
{ | |
headers: { | |
'content-type': 'application/json', | |
accept: 'application/json', | |
}, | |
} | |
); | |
return data; | |
} catch (e) { | |
return { something: 'else' }; | |
} | |
} | |
componentDidUpdate(prevProps, prevState) { | |
if (prevProps.match.params.slug !== this.props.match.params.slug) { | |
this.props.refetch(); | |
} | |
} | |
render() { | |
return ( | |
<div> | |
<Nav /> | |
<div | |
dangerouslySetInnerHTML={{ | |
__html: this.props.content.rendered, | |
}} | |
/> | |
</div> | |
); | |
} | |
} | |
export default withSSR(Page); |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
import Home from './screens/Home'; | |
import Single from './screens/Single'; | |
import Page from './screens/Page'; | |
const routes = [ | |
{ | |
path: '/', | |
component: Home, | |
exact: true, | |
}, | |
{ | |
path: '/(\d*)/(\d*)/:slug', | |
component: Single, | |
exact: true, | |
}, | |
{ | |
path: '/:slug', | |
component: Page, | |
exact: true, | |
}, | |
]; | |
export default routes; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
import express from 'express'; | |
import React from 'react'; | |
import axios from 'axios'; | |
import serialize from 'serialize-javascript'; | |
import ReactHelmet from 'react-helmet'; | |
import { renderToString } from 'react-dom/server'; | |
import { StaticRouter, matchPath } from 'react-router-dom'; | |
import App from '../common/App'; | |
import ErrorComponent from '../common/_error'; | |
import routes from '../common/routes'; | |
const assets = require(process.env.RAZZLE_ASSETS_MANIFEST); | |
const server = express(); | |
server | |
.disable('x-powered-by') | |
.use(express.static(process.env.RAZZLE_PUBLIC_DIR)) | |
.get('/*', async (req, res) => { | |
const context = {}; | |
// This data fetching technique came from a gist by @ryanflorence | |
// @see https://gist.github.com/ryanflorence/efbe562332d4f1cc9331202669763741 | |
try { | |
// We block rendering until all promises have resolved | |
const data = await Promise.all( | |
routes.map((route, index) => { | |
const match = matchPath(req.url, route); | |
return match && route.component.getInitialProps | |
? route.component.getInitialProps({ match, req, res, axios }) | |
: null; | |
}) | |
); | |
// Pass our routes and data array to our App component | |
const markup = renderToString( | |
<StaticRouter context={context} location={req.url}> | |
<App routes={routes} initialData={data} /> | |
</StaticRouter> | |
); | |
// We rewind ReactHelmet for meta tags | |
const head = ReactHelmet.renderStatic(); | |
if (context.url) { | |
res.redirect(context.url); | |
} else { | |
res.status(200).send( | |
`<!doctype html> | |
<html lang=""> | |
<head> | |
<meta httpEquiv="X-UA-Compatible" content="IE=edge" /> | |
<meta charSet='utf-8' /> | |
${head.title.toString()} | |
<meta name="viewport" content="width=device-width, initial-scale=1"> | |
<script src="${assets.client.js}" defer></script> | |
${head.meta.toString()} | |
${head.link.toString()} | |
</head> | |
<body> | |
<div id="root">${markup}</div> | |
<script>window.DATA = ${serialize(data)};</script> | |
</body> | |
</html>` | |
); | |
} | |
} catch (e) { | |
console.log('in server catch'); | |
console.log(e); | |
const markup = renderToString(<ErrorComponent error={e} />); | |
// We rewind ReactHelmet for meta tags | |
const head = ReactHelmet.renderStatic(); | |
res.status(e.response ? e.response.status : 500).send( | |
`<!doctype html> | |
<html lang=""> | |
<head> | |
<meta httpEquiv="X-UA-Compatible" content="IE=edge" /> | |
<meta charSet='utf-8' /> | |
${head.title.toString()} | |
<meta name="viewport" content="width=device-width, initial-scale=1"> | |
${head.meta.toString()} | |
${head.link.toString()} | |
</head> | |
<body> | |
<div id="root">${markup}</div> | |
</body> | |
</html>` | |
); | |
} | |
}); | |
export default server; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
import React from 'react'; | |
import axios from 'axios'; | |
// This is a Higher Order Component that implement's a Next.js-like data | |
// fetching API, but with few UX improvements... | |
// | |
// 1) It does NOT fully block render on client-side transitions after the | |
// first server-render, but rather exposes an `isLoading` prop to the wrapped | |
// component. | |
// | |
// 2) While errors that occur server-side are handled with a custom | |
// `_error.js`, client-side errors are passed down to the wrapped component | |
// through an `error` prop. Other options would be to make the HOC accept | |
// an ErrorComponent on a per-page basis, or just show the `_error`.js component | |
// on the client too. | |
// | |
// 3) getInitialProps() is passed down through `refetch` prop, so it can be | |
// manually called from a wrapped component. This is useful in situations where | |
// you need to use componentDidUpdate() | |
// | |
export default function Page(WrappedComponent) { | |
class Page extends React.Component { | |
static getInitialProps(ctx) { | |
// Need to call the wrapped components getInitialProps if it exists, else | |
// we just return null | |
return WrappedComponent.getInitialProps | |
? WrappedComponent.getInitialProps(ctx) | |
: Promise.resolve(null); | |
} | |
constructor(props) { | |
super(props); | |
this.state = { | |
data: props.initialData, | |
isLoading: !!props.initialData, | |
}; | |
} | |
componentDidMount() { | |
if (!this.state.data) { | |
// This will NOT run on initial server render, because this.state.data | |
// will exist. However, we want to call this on all subsequent client | |
// route changes | |
this.fetchData(); | |
} | |
} | |
fetchData = () => { | |
// if this.state.data is undefined, that means that the we are on the client. | |
// To get the data we need, we just call getInitialProps again. We pass | |
// it react-router's match, as well as an axios instance. As req and res | |
// don't exist in browser-land, they are omitted. | |
this.setState({ isLoading: true }); | |
this.constructor.getInitialProps({ match: this.props.match, axios }).then( | |
data => this.setState({ data, isLoading: false }), | |
error => | |
this.setState({ | |
// We can gracefully expose errors on the client, by also keeping | |
// them in state. | |
data: { error }, | |
isLoading: false, | |
}) | |
); | |
}; | |
render() { | |
// Just like Next.js's `getInitialProps`, we flatten out this.state.data. | |
// However, one big difference from next, is that we do NOT block client | |
// transitions. So we passing `isLoading` down. Finally, we pass down | |
// this.fetchData so it is available to routes that need to do force | |
// refreshes. For example, sibling routes that need to call | |
// componentDidUpdate(), can then just refetch(). | |
const { initialData, ...rest } = this.props; | |
return ( | |
<WrappedComponent | |
{...rest} | |
refetch={this.fetchData} | |
isLoading={this.state.isLoading} | |
{...this.state.data} | |
/> | |
); | |
} | |
} | |
// Set out component's displayName. This just makes debugging easier. | |
// Components will show as Page(MyComponent) in react-dev-tools. | |
Page.displayName = `Page(${getDisplayName(WrappedComponent)})`; | |
return Page; | |
} | |
function getDisplayName(WrappedComponent) { | |
return WrappedComponent.displayName || WrappedComponent.name || 'Component'; | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Thanks, this really helped me put together how to handle routing and fetching data with Razzle inside our React app. I've put together a basic repo which puts together these files into a runnable app.