Skip to content

Instantly share code, notes, and snippets.

@jaredpalmer
Created May 30, 2017 17:45
Show Gist options
  • Star 19 You must be signed in to star a gist
  • Fork 5 You must be signed in to fork a gist
  • Save jaredpalmer/a73bc00cac8926ff0ad5281879b1eb90 to your computer and use it in GitHub Desktop.
Save jaredpalmer/a73bc00cac8926ff0ad5281879b1eb90 to your computer and use it in GitHub Desktop.
Next.js-like SSR without Next.js.
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;
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);
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;
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;
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';
}
@fetimo
Copy link

fetimo commented Jul 5, 2017

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.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment