Table of Contents
Before.js enables data fetching with any React SSR app that uses React Router 4.
For page components, you can add a static async getInitialProps({ req, res, match, history, location, ...context })
function.
This will be called on both initial server render, and then on componentDidUpdate.
Results are made available on this.props
.
import React, { PureComponent } from 'react';
export default class About extends PureComponent {
static async getInitialProps({ req, res, match, history, location, ...rest }) {
const stuff = await asyncDataFecthing();
return { stuff };
}
render() {
return (
<div>
<h1>About</h1>
{this.props.stuff ? this.props.stuff : 'Loading...'}
</div>
);
}
}
import React from 'react';
export default const About = ({ stuff }) => (
<div>
<h1>About</h1>
{stuff ? stuff : 'Loading...'}
</div>
);
About.getInitialProps = async ({ req, res, match, history, location, ...rest }) {
const stuff = await asyncDataFecthing();
return { stuff };
}
Notice that to load data when the page loads, we use getInitialProps
which is an async static method. It can asynchronously fetch anything that resolves to a JavaScript plain Object, which populates props.
Data returned from getInitialProps
is serialized when server rendering, similar to a JSON.stringify. Make sure the returned object from getInitialProps
is a plain Object and not using Date, Map or Set.
For the initial page load, getInitialProps
will execute on the server only. getInitialProps
will only be executed on the client when navigating to a different route via the Link component or using the routing APIs.
type DataType = {
[key: any]: any
};
type Context = {
req: {
url: string,
query: { [key: string]: string },
originalUrl: string,
path: string,
[key: string]: any
},
res?: {
status(code: number): void,
redirect(code: number, redirectTo: string): void,
[key: string]: any
},
assets?: {
client: {
css: string,
js: string
}
},
data?: ?DataType,
filterServerData?: (data: ?DataType) => DataType,
renderPage?: (data: ?DataType) => Promise<Page>,
generateCriticalCSS?: () => string | boolean,
title?: string,
extractor?: ?Extractor,
location?: {
hash: string,
key?: string,
pathname: string,
search: string,
state?: any
}
};
As you have probably figured out, React Router 4 powers all of Before.js's routing. You can use any and all parts of RR4.
Before will inject location
and match
properties in each route props which are the same properties from React Router. Also, the match
property will include a parsed object with the actual query string values from the location.search
.
Take in mind that if you use the withRouter HOC from React Router It will still work as expected but it will override the values from Before.js.
// ./src/routes.js
import Home from './Home';
import About from './About';
import Detail from './Detail';
// Internally these will become:
// <Route path={path} exact={exact} render={props => <component {...props} data={data} />} />
const routes = [
{
path: '/',
exact: true,
component: Home
},
{
path: '/about',
component: About
},
{
path: '/detail/:id',
component: Detail
}
];
export default routes;
// ./src/Detail.js
import React from 'react';
import NavLink from 'react-router-dom/NavLink';
class Detail extends React.Component {
// Notice that this will be called for
// /detail/:id
// /detail/:id/more
// /detail/:id/other
static async getInitialProps({ req, res, match }) {
const item = await CallMyApi(`/v1/item${match.params.id}`);
return { item };
}
render() {
return (
<div>
<h1>Detail</h1>
{this.props.item ? this.props.item : 'Loading...'}
<Route path="/detail/:id/more" exact render={() => <div>{this.props.item.more}</div>} />
<Route path="/detail/:id/other" exact render={() => <div>{this.props.item.other}</div>} />
</div>
);
}
}
export default Detail;
In some parts of your application, you may not need server data fetching at all (e.g. settings). With Before.js, you just use React Router 4 as you normally would in client land: You can fetch data (in componentDidMount) and do routing the same exact way.
Before.js lets you easily define lazy-loaded or code-split routes in your routes.js
file. To do this, you'll need to modify the relevant route's component
definition like so:
// ./src/_routes.js
import React from 'react';
import Home from './Home';
import { asyncComponent } from '@before/client';
import loadable from '@loadable/component';
import Loading from './Loading';
export default [
// normal route
{
path: '/',
exact: true,
component: Home
},
// codesplit route
{
path: '/about',
exact: true,
component: asyncComponent({
loader: () => import(* webpackChunkName: "about" */ './About'), // required in order to get initial props from this route.
LoadableComponent: loadable(
() => import(* webpackChunkName: "about" */ './About'),
{ fallback: () => <Loading /> }
)
})
}
];
Before.js use @loadable components to support server-side chunks/code-split. In order to use this feature all you have to do is the following setup:
- Install
@loadable/babel-plugin
and add it to the .babelrc
{
"plugins": ["@loadable/babel-plugin"]
}
- Install
@loadable/webpack-plugin
and include it in the plugins definition of the webpack.config.js
const LoadablePlugin = require('@loadable/webpack-plugin');
module.exports = {
// ...
plugins: [new LoadablePlugin({ writeToDisk: true })]
};
- Setup
ChunkExtractor
server-side, pass the loadable-stats.json (file generated by webpack lodable plugin) path to the Before.js render method.
import { render } from '@before/server';
// ...
const statsPath = '../dist/loadable-stats.json';
await render({
req: req,
res: {},
routes: propOr([], 'routes', config),
assets,
statsPath
});
- Use the
ensureClientReady
method in the client-side.
import React from 'react';
import routes from './routes';
import { hydrate } from 'react-dom';
import { ensureReady, ensureClientReady, Before } from '@before/client';
ensureReady(routes).then(data => {
return ensureClientReady(() => {
hydrate(
<BrowserRouter>
<Before data={data} routes={routes} />
</BrowserRouter>,
document.getElementById('root')
);
});
});
Before.js works similarly to Next.js with respect to overriding HTML document structure. This comes in handy if you are using a CSS-in-JS library or just want to collect data out of react context before or Before render. To do this, create a file in ./src/Document.js
like so:
// ./src/Document.js
import React, { PureComponent } from 'react';
class Document extends PureComponent {
static async getInitialProps({ assets, data, renderPage }) {
const page = await renderPage();
return { assets, data, ...page };
}
render() {
const { helmet, assets, data, title, error, ErrorComponent } = this.props;
// get attributes from React Helmet
const htmlAttrs = helmet.htmlAttributes.toComponent();
const bodyAttrs = helmet.bodyAttributes.toComponent();
return (
<html {...htmlAttrs}>
<head>
<meta httpEquiv="X-UA-Compatible" content="IE=edge" />
<meta charSet="utf-8" />
<title>{title}</title>
<meta name="viewport" content="width=device-width, initial-scale=1" />
{helmet.title.toComponent()}
{helmet.meta.toComponent()}
{helmet.link.toComponent()}
{assets.client.css && <link rel="stylesheet" href={assets.client.css} />}
</head>
<body {...bodyAttrs}>
<Root />
<Data data={data} />
{error && ErrorComponent && <ErrorComponent error={error} />}
<script type="text/javascript" src={assets.client.js} defer crossOrigin="anonymous" />
</body>
</html>
);
}
}
export default Document;
If you were using something like styled-components
, and you need to wrap you entire app with some sort of additional provider or function, you can do this with renderPage()
.
// ./src/Document.js
import React, { PureComponent } from 'react';
import { ServerStyleSheet } from 'styled-components';
export default class Document extends PureComponent {
static async getInitialProps({ assets, data, renderPage }) {
const sheet = new ServerStyleSheet();
const page = await renderPage(App => props => sheet.collectStyles(<App {...props} />));
const styleTags = sheet.getStyleElement();
return { assets, data, ...page, styleTags };
}
render() {
const { helmet, assets, data, title, error, ErrorComponent } = this.props;
// get attributes from React Helmet
const htmlAttrs = helmet.htmlAttributes.toComponent();
const bodyAttrs = helmet.bodyAttributes.toComponent();
return (
<html {...htmlAttrs}>
<head>
<meta httpEquiv="X-UA-Compatible" content="IE=edge" />
<meta charSet="utf-8" />
<title>{title}</title>
<meta name="viewport" content="width=device-width, initial-scale=1" />
{helmet.title.toComponent()}
{helmet.meta.toComponent()}
{helmet.link.toComponent()}
{assets.client.css && <link rel="stylesheet" href={assets.client.css} />}
</head>
<body {...bodyAttrs}>
<Root />
<Data data={data} />
{error && ErrorComponent && <ErrorComponent error={error} />}
<script type="text/javascript" src={assets.client.js} defer crossOrigin="anonymous" />
</body>
</html>
);
}
}
To use your custom <Document>
, pass it to the Document
option of your Before.js render
function.
// ./src/server.js
import express from 'express';
import { render } from '@before/server';
import routes from './routes';
import MyDocument from './Document';
const assets = require(process.env.ASSETS_MANIFEST);
const server = express();
server
.disable('x-powered-by')
.use(express.static(process.env.PUBLIC_DIR))
.get('/*', async (req, res) => {
try {
// Pass document in here.
const html = await render({
req,
res,
document: MyDocument,
routes,
assets,
statsPath
});
res.send(html);
} catch (error) {
console.log(error);
res.json(error);
}
});
export default server;
You can provide a custom (potentially async) rendering function as an option to Before.js render
function.
If present, it will be used instead of the default ReactDOMServer renderToString function.
It has to return an object of shape { html : string!, ...otherProps }
, in which html
will be used as the rendered string
Thus, setting customRenderer = (node) => ({ html: ReactDOMServer.renderToString(node) })
is the the same as default option.
otherProps
will be passed as props to the rendered Document
Example:
// ./src/server.js
import React from 'react';
import express from 'express';
import { render } from '@before/server';
import { renderToString } from 'react-dom/server';
import { ApolloProvider, getDataFromTree } from 'react-apollo';
import routes from './routes';
import createApolloClient from './createApolloClient';
import Document from './Document';
const assets = require(process.env.ASSETS_MANIFEST);
const server = express();
server
.disable('x-powered-by')
.use(express.static(process.env.PUBLIC_DIR))
.get('/*', async (req, res) => {
const client = createApolloClient({ ssrMode: true });
const customRenderer = node => {
const App = <ApolloProvider client={client}>{node}</ApolloProvider>;
return getDataFromTree(App).then(() => {
const initialApolloState = client.extract();
const html = renderToString(App);
return { html, initialApolloState };
});
};
try {
const html = await render({
req,
res,
routes,
assets,
customRenderer,
document: Document,
statsPath
});
res.send(html);
} catch (error) {
res.json(error);
}
});
export default server;