Skip to content

Instantly share code, notes, and snippets.

@Alex-Shilman
Forked from jaredpalmer/App.js
Created May 12, 2020 14:08
Show Gist options
  • Save Alex-Shilman/67d27610f51f69b4cccacbd4adf6c6d1 to your computer and use it in GitHub Desktop.
Save Alex-Shilman/67d27610f51f69b4cccacbd4adf6c6d1 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';
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment