https://github.com/flybondi/before.js
Because the party is before react
https://github.com/flybondi/before.js
javascript react-hooks reactjs server-side-rendering
Last synced: about 2 months ago
JSON representation
Because the party is before react
- Host: GitHub
- URL: https://github.com/flybondi/before.js
- Owner: flybondi
- Created: 2018-08-04T20:21:49.000Z (over 6 years ago)
- Default Branch: master
- Last Pushed: 2022-12-06T17:26:38.000Z (over 2 years ago)
- Last Synced: 2024-04-16T08:50:59.338Z (about 1 year ago)
- Topics: javascript, react-hooks, reactjs, server-side-rendering
- Language: JavaScript
- Homepage:
- Size: 1.02 MB
- Stars: 1
- Watchers: 8
- Forks: 0
- Open Issues: 0
-
Metadata Files:
- Readme: README.md
- Changelog: CHANGELOG.md
- Codeowners: .github/CODEOWNERS
Awesome Lists containing this project
README
![]()
Before.js
[](https://flybondi.com)
   **Table of Contents**
- [Getting Started with Before.js](#getting-started-with-Beforejs)
- [Data Fetching](#data-fetching)
- [`getInitialProps: (ctx) => Data`](#getinitialprops-ctx--data)
- [Injected Page Props](#injected-page-props)
- [Routing](#routing)
- [Parameterized Routing](#parameterized-routing)
- [Client Only Data and Routing](#client-only-data-and-routing)
- [Code Splitting](#code-splitting)
- [Custom ``](#custom-document)## Getting Started with Before.js
**Before.js** enables data fetching with any React SSR app that uses React Router 4.
## Data Fetching
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`.```js
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 (
About
{this.props.stuff ? this.props.stuff : 'Loading...'}
);
}
}
``````js
import React from 'react';export default const About = ({ stuff }) => (
About
{stuff ? stuff : 'Loading...'}
);About.getInitialProps = async ({ req, res, match, history, location, ...rest }) {
const stuff = await asyncDataFecthing();
return { stuff };
}
```### `static async getInitialProps(context): InitialProps`
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.```js
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,
generateCriticalCSS?: () => string | boolean,
title?: string,
extractor?: ?Extractor,
location?: {
hash: string,
key?: string,
pathname: string,
search: string,
state?: any
}
};
```## Routing
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.### React Router _withRouter_ HOC
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**.### Parameterized Routing
```js
// ./src/routes.js
import Home from './Home';
import About from './About';
import Detail from './Detail';// Internally these will become:
// } />
const routes = [
{
path: '/',
exact: true,
component: Home
},
{
path: '/about',
component: About
},
{
path: '/detail/:id',
component: Detail
}
];export default routes;
``````js
// ./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 (
Detail
{this.props.item ? this.props.item : 'Loading...'}
{this.props.item.more}} />
{this.props.item.other}} />
);
}
}export default Detail;
```### Client Only Data and Routing
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.## Code Splitting
**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:
```js
// ./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: () => }
)
})
}
];
```**Before.js** use [@loadable](https://www.smooth-code.com/open-source/loadable-components/docs/getting-started/) components to support server-side chunks/code-split. In order to use this feature all you have to do is the following setup:
1. Install `@loadable/babel-plugin` and add it to the _.babelrc_
```json
{
"plugins": ["@loadable/babel-plugin"]
}
```2. Install `@loadable/webpack-plugin` and include it in the plugins definition of the _webpack.config.js_
```js
const LoadablePlugin = require('@loadable/webpack-plugin');module.exports = {
// ...
plugins: [new LoadablePlugin({ writeToDisk: true })]
};
```3. Setup `ChunkExtractor` server-side, pass the _loadable-stats.json_ (file generated by _webpack lodable plugin_) path to the **Before.js** render method.
```js
import { render } from '@before/server';
// ...const statsPath = '../dist/loadable-stats.json';
await render({
req: req,
res: {},
routes: propOr([], 'routes', config),
assets,
statsPath
});
```4. Use the `ensureClientReady` method in the client-side.
```jsx
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(
,
document.getElementById('root')
);
});
});
```## Custom ``
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:
```jsx
// ./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 (
{title}
{helmet.title.toComponent()}
{helmet.meta.toComponent()}
{helmet.link.toComponent()}
{assets.client.css && }
{error && ErrorComponent && }
);
}
}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()`.
```jsx
// ./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());
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 (
{title}
{helmet.title.toComponent()}
{helmet.meta.toComponent()}
{helmet.link.toComponent()}
{assets.client.css && }
{error && ErrorComponent && }
);
}
}
```To use your custom ``, pass it to the `Document` option of your Before.js `render` function.
```js
// ./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;
```## Custom/Async Rendering
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:
```js
// ./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 = {node};
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;
```