Skip to content

Commit

Permalink
New router prefetcher (#111)
Browse files Browse the repository at this point in the history
  • Loading branch information
antonybudianto authored Oct 7, 2018
1 parent 5961afc commit 478ee03
Show file tree
Hide file tree
Showing 20 changed files with 498 additions and 87 deletions.
1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@
"ls": "lerna ls",
"release:next": "npm run release -- --npm-tag=next",
"exec:rp": "lerna exec --scope @cra-express/redux-prefetcher --",
"exec:rr": "lerna exec --scope @cra-express/router-prefetcher --",
"exec:ul": "lerna exec --scope @cra-express/universal-loader --",
"ul:test": "npm run exec:ul -- npm t --"
},
Expand Down
3 changes: 3 additions & 0 deletions packages/@cra-express/router-prefetcher/.babelrc
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
{
"presets": ["@babel/preset-env", "@babel/preset-react"]
}
4 changes: 4 additions & 0 deletions packages/@cra-express/router-prefetcher/.prettierrc
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
{
"tabWidth": 2,
"singleQuote": true
}
166 changes: 166 additions & 0 deletions packages/@cra-express/router-prefetcher/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,166 @@
# @cra-express/router-prefetcher

> Simple utility to map your routes and prefetch your data on server
> You might want to wait for React Suspense, [demo](https://github.com/acdlite/suspense-ssr-demo/)
> :warning: **Alpha** stage. API may change, don't use on production yet!
## Prerequisites

- React Router with array config
- Promise support

## Start

```
npm i @cra-express/router-prefetcher
```

## Setup and Usage

- Add `{{SCRIPT}}` to public/index.html

```html
<div id="root"></div>
{{SCRIPT}}
```

```js
// server/app.js

import { createReactAppExpress } from '@cra-express/core';
import { getInitialData } from '@cra-express/router-prefetcher';
import routes from '../src/routes';
const path = require('path');
const React = require('react');
const { StaticRouter } = require('react-router');

const { default: App } = require('../src/App');
const clientBuildPath = path.resolve(__dirname, '../client');
let AppClass = App;
let serverData;
const app = createReactAppExpress({
clientBuildPath,
universalRender: handleUniversalRender,
onEndReplace(html) {
const state = store.getState();
return html.replace(
'{{SCRIPT}}',
`<script>
window.__INITIAL_DATA__ = ${JSON.stringify(serverData).replace(
/</g,
'\\u003c'
)};
</script>`
);
}
});

function handleUniversalRender(req, res) {
const context = {};
return getInitialData(req, res, routes)
.then(data => {
serverData = data;
const app = (
<StaticRouter location={req.url} context={context}>
<AppClass routes={routes} initialData={data} />
</StaticRouter>
);
return app;
})
.catch(err => {
console.error(err);
res.send(500);
});
}

export default app;
```

```js
// src/index.js

import routes from './routes';

const data = window.__INITIAL_DATA__;
ReactDOM.hydrate(
<BrowserRouter>
<App routes={routes} initialData={data} />
</BrowserRouter>,
document.getElementById('root')
);
```

```js
// src/App.js

import React from 'react';
import { Route, Switch } from 'react-router';

const App = ({ routes, initialData }) => {
return (
<Switch>
{routes.map((route, index) => {
return (
<Route
key={index}
path={route.path}
exact={route.exact}
render={props =>
React.createElement(route.component, {
...props,
routes: route.routes,
initialData: initialData[index] || null
})
}
/>
);
})}
</Switch>
);
};
```

```js
// src/routes/DemoPage/AboutView.js

import React from 'react';

import withSSR from '../../components/withSSR';

class AboutView extends React.Component {
static getInitialData({ match, req, res }) {
return new Promise((resolve, reject) => {
setTimeout(() => {
resolve({
article: `
This text is ALSO server rendered if and only if it's the initial render.
`,
currentRoute: match.pathname
});
}, 500);
});
}

render() {
const { isLoading, article, error } = this.props;
return (
<div>
<h1>About</h1>
{isLoading && <div>Loading from client...</div>}
{error && <div>{JSON.stringify(error, null, 2)}</div>}
{article && <div>{article}</div>}
</div>
);
}
}

export default withSSR(AboutView);
```

Please get the `withSSR` code [here](https://github.com/jaredpalmer/react-router-nextjs-like-data-fetching/blob/master/src/components/withSSR.js)

## License

MIT
7 changes: 7 additions & 0 deletions packages/@cra-express/router-prefetcher/jest-config.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
{
"testEnvironment": "node",
"moduleFileExtensions": ["js"],
"collectCoverageFrom": ["src/**/*.{js}"],
"testRegex": "(src)/.*\\.spec\\.js$"
}

28 changes: 28 additions & 0 deletions packages/@cra-express/router-prefetcher/package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
{
"name": "@cra-express/router-prefetcher",
"version": "4.0.0-alpha.2",
"description": "Router Prefetcher for prefetching on server",
"main": "lib/index.js",
"files": [
"lib"
],
"publishConfig": {
"access": "public"
},
"scripts": {
"build": "babel src -d lib --ignore spec.js",
"watch": "babel src -d lib --watch --ignore spec.js",
"test": "jest --watch --config=jest-config.json",
"test:ci": "jest --config=jest-config.json --coverage"
},
"author": "Antony Budianto <antonybudianto@gmail.com>",
"license": "MIT",
"peerDependencies": {
"react": "*",
"react-router-dom": "*"
},
"devDependencies": {
"react": "^16.5.2",
"react-router-dom": "^4.3.1"
}
}
25 changes: 25 additions & 0 deletions packages/@cra-express/router-prefetcher/src/index.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
import { matchPath } from 'react-router-dom';

export function getInitialData(req, res, routes) {
const matches = routes.map((route, index) => {
const match = matchPath(req.url, route.path, route);
if (match) {
const obj = {
route,
match,
promise: route.component.getInitialData
? route.component.getInitialData({
match,
req,
res
})
: Promise.resolve(null)
};
return obj;
}
return null;
});

const promises = matches.map(match => (match ? match.promise : null));
return Promise.all(promises);
}
43 changes: 43 additions & 0 deletions packages/@cra-express/router-prefetcher/src/index.spec.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
import { getInitialData } from './index';

jest.mock('react-router-dom');

const rrd = require('react-router-dom');

describe('getInitialData', () => {
it('should handle not match path', done => {
rrd.matchPath.mockReturnValue(false);
getInitialData({}, {}, [{}]).then(res => {
expect(res[0]).toBeNull();
done();
});
});

it('should handle matched path', done => {
rrd.matchPath.mockReturnValue(true);
getInitialData({}, {}, [
{
component: {
getInitialData() {
return Promise.resolve(1);
}
}
}
]).then(res => {
expect(res[0]).toBe(1);
done();
});
});

it('should handle matched path with no getInitialData', done => {
rrd.matchPath.mockReturnValue(true);
getInitialData({}, {}, [
{
component: {}
}
]).then(res => {
expect(res[0]).toBeNull();
done();
});
});
});
4 changes: 4 additions & 0 deletions packages/demo/.prettierrc
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
{
"tabWidth": 2,
"singleQuote": true
}
1 change: 1 addition & 0 deletions packages/demo/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@
"dependencies": {
"@cra-express/core": "^4.0.0-alpha.2",
"@cra-express/redux-prefetcher": "^4.0.0-alpha.2",
"@cra-express/router-prefetcher": "^4.0.0-alpha.2",
"basscss": "8.0.4",
"font-awesome": "4.7.0",
"loadable-components": "2.2.3",
Expand Down
15 changes: 10 additions & 5 deletions packages/demo/server/app.js
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import { getLoadableState } from 'loadable-components/server';
import thunk from 'redux-thunk';
import { createReactAppExpress } from '@cra-express/core';
import { getInitialData } from '@cra-express/redux-prefetcher';
import { getInitialData } from '@cra-express/router-prefetcher';
import routes from '../src/routes';
const path = require('path');
const React = require('react');
Expand All @@ -15,6 +15,7 @@ const clientBuildPath = path.resolve(__dirname, '../client');
let tag = '';
let store;
let AppClass = App;
let serverData;
const app = createReactAppExpress({
clientBuildPath,
universalRender: handleUniversalRender,
Expand All @@ -27,6 +28,10 @@ const app = createReactAppExpress({
/</g,
'\\u003c'
)};
window.__INITIAL_DATA__ = ${JSON.stringify(serverData).replace(
/</g,
'\\u003c'
)};
</script>`
);
}
Expand All @@ -35,13 +40,13 @@ const app = createReactAppExpress({
function handleUniversalRender(req, res) {
const context = {};
store = createStore(reducer, applyMiddleware(thunk));
const expressCtx = { req, res };
return getInitialData(expressCtx, store, routes)
.then(result => {
return getInitialData(req, res, routes)
.then(data => {
serverData = data;
const app = (
<StaticRouter location={req.url} context={context}>
<Provider store={store}>
<AppClass />
<AppClass routes={routes} initialData={data} />
</Provider>
</StaticRouter>
);
Expand Down
Loading

0 comments on commit 478ee03

Please sign in to comment.