Skip to content

Commit

Permalink
Implement code splitting with react-loadable (#84)
Browse files Browse the repository at this point in the history
  • Loading branch information
richardscarrott authored Apr 14, 2017
1 parent 1ffe020 commit 8958b05
Show file tree
Hide file tree
Showing 25 changed files with 317 additions and 67 deletions.
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@ $ npm start
## Features

- ES2015/16 with [Babel](https://github.com/babel/babel)
- Universal rendering with support for data fetching
- Universal rendering with support for data fetching *and code splitting*.
- Hot reloading on both client and *server*
- Locally scoped CSS with [CSS modules](https://github.com/css-modules)
- Scalable unit testing via [Jest](https://github.com/facebook/jest)
Expand Down
3 changes: 2 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -47,7 +47,7 @@
"webpack": "^2.3.2",
"webpack-dev-middleware": "^1.10.1",
"webpack-hot-middleware": "^2.17.1",
"webpack-hot-server-middleware": "~0.0.5"
"webpack-hot-server-middleware": "~0.0.6"
},
"dependencies": {
"babel-polyfill": "^6.23.0",
Expand All @@ -62,6 +62,7 @@
"react": "^15.3.2",
"react-dom": "^15.4.2",
"react-helmet": "^5.0.0",
"react-loadable": "^3.2.2",
"react-redux": "^5.0.3",
"react-router": "^2.8.1",
"react-router-redux": "^4.0.8",
Expand Down
8 changes: 4 additions & 4 deletions server/routes/static.js
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ const ms = require('ms');

const DIST_DIR = path.join(__dirname, '../../dist');
const SERVER_RENDERER_PATH = path.join(DIST_DIR, 'server.js');
const CLIENT_STATS_PATH = path.join(DIST_DIR, 'stats.json');
const STATS_PATH = path.join(DIST_DIR, 'client-stats.json');
const router = express.Router();

let serverRenderer;
Expand All @@ -15,13 +15,13 @@ let stats;
try {
serverRenderer = require(SERVER_RENDERER_PATH).default;
} catch (ex) {
throw new Error('Server bundle not found. Try running `npm run build`');
throw new Error(`Server bundle not found at ${SERVER_RENDERER_PATH}. Try running \`npm run build\``);
}

try {
stats = require(CLIENT_STATS_PATH);
stats = require(STATS_PATH);
} catch (ex) {
throw new Error('Client bundle stats.json not found. Try running `npm run build`');
throw new Error(`Client stats not found at ${STATS_PATH}. Try running \`npm run build\``);
}

router.use(express.static(DIST_DIR, {
Expand Down
12 changes: 5 additions & 7 deletions src/components/html/Html.js
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import React, { PropTypes } from 'react';
import serialize from 'serialize-javascript';

function Html({ css, js, html, head, initialState }) {
function Html({ js, css, html, head, initialState }) {
return (
<html lang="en">
<head>
Expand All @@ -23,9 +23,7 @@ function Html({ css, js, html, head, initialState }) {
<meta name="theme-color" content="#ffffff" />

{head.link.toComponent()}
{css ? (
<link rel="stylesheet" href={css} />
) : null}
{css.map(css => <link key={css} rel="stylesheet" href={`/${css}`} />)}
</head>
<body>
<div id="root" dangerouslySetInnerHTML={{
Expand All @@ -43,15 +41,15 @@ function Html({ css, js, html, head, initialState }) {
<script dangerouslySetInnerHTML={{
__html: `window.__INITIAL_STATE__ = ${serialize(initialState)}`
}} />
<script src={js}></script>
{js.map(js => <script key={js} src={`/${js}`}></script>)}
</body>
</html>
);
}

Html.propTypes = {
css: PropTypes.string,
js: PropTypes.string.isRequired,
js: PropTypes.array.isRequired,
css: PropTypes.array.isRequired,
html: PropTypes.string,
head: PropTypes.object.isRequired,
initialState: PropTypes.object.isRequired
Expand Down
10 changes: 8 additions & 2 deletions src/components/html/Html.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -11,8 +11,14 @@ describe('components/html/Html', () => {
process.env.REDUX_LOGGER = 'true';
const component = renderer.create(
<Html
css="https://60fram.es/bundle.css"
js="https://60fram.es/bundle.js"
css={[
'bundle.css',
'chunk.css'
]}
js={[
'bundle.js',
'chunk.js'
]}
html={`
<div id="root">
Hello World.
Expand Down
11 changes: 9 additions & 2 deletions src/components/html/__snapshots__/Html.test.js.snap
Original file line number Diff line number Diff line change
Expand Up @@ -66,7 +66,11 @@ exports[`components/html/Html renders correctly 1`] = `
/>
&lt;link /&gt;
<link
href="https://60fram.es/bundle.css"
href="/bundle.css"
rel="stylesheet"
/>
<link
href="/chunk.css"
rel="stylesheet"
/>
</head>
Expand Down Expand Up @@ -98,7 +102,10 @@ exports[`components/html/Html renders correctly 1`] = `
}
/>
<script
src="https://60fram.es/bundle.js"
src="/bundle.js"
/>
<script
src="/chunk.js"
/>
</body>
</html>
Expand Down
6 changes: 4 additions & 2 deletions src/components/index/Index.js
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
import React, { Component } from 'react';
import { connect } from 'react-redux';
import { fetchQuoteIfNeeded } from 'actions/quote/quote';
import Loader from 'components/lib/loader/Loader';
import Error from 'components/lib/error/Error';
import styles from 'components/index/Index.css';

export class Index extends Component {
Expand All @@ -15,10 +17,10 @@ export class Index extends Component {
return (
<div className={styles.root}>
{isFetching ? (
<p>Loading...</p>
<Loader />
) : null}
{error ? (
<p>Error... {error}</p>
<Error>{error}</Error>
) : null}
{value ? (
<p>{value}</p>
Expand Down
30 changes: 30 additions & 0 deletions src/components/index/IndexLoadable.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
import React from 'react';
import { compose } from 'redux';
import loadable from 'components/lib/loadable/loadable';
import liftFetchData from 'components/lib/liftfetchdata/liftFetchData';
import Loader from 'components/lib/loader/Loader';
import Error from 'components/lib/error/Error';
import path from 'path';

const webpackRequireWeakId = () => require.resolveWeak('./Index');

const LoadingComponent = ({ isLoading, error, pastDelay }) => {
if (isLoading && pastDelay) {
return <Loader />;
} else if (error) {
console.log('ERROR IS BEING RENDERED', error);
return <Error>Error! Component failed to load</Error>;
}
return null;
};

// NOTE: We're making a trade off for more aggresive code splitting (i.e. includes
// action creators) for waterfall requests when fetching the chunk and the data
// in the client.
const enhance = compose(liftFetchData(webpackRequireWeakId), loadable);

export default enhance({
loader: () => import('./Index'),
LoadingComponent,
webpackRequireWeakId
});
13 changes: 8 additions & 5 deletions src/components/index/__snapshots__/Index.test.js.snap
Original file line number Diff line number Diff line change
Expand Up @@ -4,9 +4,11 @@ exports[`components/index/Index renders a loading indicator 1`] = `
<div
className="root"
>
<p>
<div
className="root"
>
Loading...
</p>
</div>
</div>
`;

Expand All @@ -24,9 +26,10 @@ exports[`components/index/Index renders an error 1`] = `
<div
className="root"
>
<p>
Error...
<div
className="root"
>
An error
</p>
</div>
</div>
`;
3 changes: 3 additions & 0 deletions src/components/lib/error/Error.css
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
.root {
color: crimson;
}
14 changes: 14 additions & 0 deletions src/components/lib/error/Error.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
import React, { PropTypes } from 'react';
import styles from './Error.css';

const Error = ({ children }) => <div className={styles.root}>{children}</div>;

Error.propTypes = {
children: PropTypes.node
};

Error.defaultProps = {
children: 'Error'
};

export default Error;
16 changes: 16 additions & 0 deletions src/components/lib/liftfetchdata/liftFetchData.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
/**
* Lifts static fetchData method from weakId component to `Component`, deliberately
* circumventing static require / import to prevent Webpack from bundling the
* weakId component and it's deps.
*/
export default webpackRequireWeakId =>
Component => {
const weakId = webpackRequireWeakId();
if (__webpack_modules__[weakId]) {
const module = __webpack_require__(weakId).default;
if (module && typeof module.fetchData === 'function') {
Component.fetchData = (...args) => module.fetchData(...args);
}
}
return Component;
};
30 changes: 30 additions & 0 deletions src/components/lib/loadable/loadable.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
import React, { Component } from 'react';
import Loadable from 'react-loadable';

let moduleIds = new Set();

/**
* Wraps `Loadable` in order to support `flushServerSideRequires` (or a variation thereof)
* as proposed here -- https://medium.com/@thejameskyle/react-loadable-2674c59de178
*/
const WrappedLoadable = options => {
const BaseComponent = Loadable(options);
return class extends Component {
constructor() {
super();
moduleIds.add(options.webpackRequireWeakId());
}

render() {
return <BaseComponent {...this.props} />
}
}
}

WrappedLoadable.flushModuleIds = () => {
const ids = [...moduleIds];
moduleIds.clear();
return ids;
}

export default WrappedLoadable;
3 changes: 3 additions & 0 deletions src/components/lib/loader/Loader.css
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
.root {
color: blue;
}
6 changes: 6 additions & 0 deletions src/components/lib/loader/Loader.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
import React from 'react';
import styles from './Loader.css';

const Loader = () => <div className={styles.root}>Loading...</div>

export default Loader;
4 changes: 2 additions & 2 deletions src/routes.js
Original file line number Diff line number Diff line change
@@ -1,12 +1,12 @@
import React from 'react';
import { Route, IndexRoute, Redirect } from 'react-router';
import App from 'components/app/App';
import Index from 'components/index/Index';
import IndexLoadable from 'components/index/IndexLoadable';
import NotFound from 'components/notfound/NotFound';

export default (
<Route path="/" component={App}>
<IndexRoute component={Index} />
<IndexRoute component={IndexLoadable} />
<Redirect from="foo" to="/" />
<Route path="*" component={NotFound} />
</Route>
Expand Down
Loading

0 comments on commit 8958b05

Please sign in to comment.