Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Lazy load routes #3

Closed
wants to merge 5 commits into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions Procfile
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
web: npm run build && npm run start:prod
48 changes: 46 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,2 +1,46 @@
# react-production-starter
Isomorphic React Starter with Redux, React Router, Redial, CSS Modules, Express, Webpack.
# React Production Starter

While there are tons of react/redux/react-router boilerplates on Github, this one is built to scale: it comes with multiple entry points (a.k.a. codesplitting), asynchronous "lazy" react-router routes, and asynchronous redux reducers. These features allow you to separate your app into as many mini-apps as you'd like to without affecting initial page load.

Out of the box, the app is client-side only. However, refactoring to a universal (isomorphic) app is relatively straightforward. The only thing you'll have to decide on is which data-resolving logic you want. See `server-iso.js`, `client-iso.js`, `routes-iso.js` for an example that uses Redial (formerly react-fetchr).


Folder Structure:
```bash
.
├── /build/ # The folder for compiled output
├── /node_modules/ # 3rd-party libraries and utilities
├── /src/ # The source code of the application
│ ├── /components/ # Global React components
│ ├── /reducers/ # Synchronous reducers (reducers required for initial page load)
│ ├── /middleware/ # Redux middleware (comes with callAPIMiddileware)
│ ├── /routes/ # React-router routes
│ | ├── /App/ # Main wrapper component
│ | ├── /PostList/ # PostList page
│ | ├── /Editor/ # Editor (async stub)
│ | ├── /Post/ # Post (async)
│ | | ├── /components/ # Post components (async)
│ | | ├── actions.js # Post actions (async)
│ | | ├── reducer.js # Post reducer (async)
│ | | ├── index.js # Post Route (async)
│ | ├── /root.js # React-router root
│ ├── /client.js # Client-side entry point
│ ├── /store.js # Redux store configuration
│ ├── /constants.js # Global constants (Action types, Aphrodite layout/style vars)
│ ├── /(routes-iso.js) # (Synchronous vanilla react-router routes, in case you want to use those)
│ ├── /server/ # Server
│ | ├── /api/ # API endpoints
│ | | ├── /posts.js # Posts endpoint
│ | | ├── /post.js # Single Post endpoint
│ | ├── /fakeDB.js # Database Stub
│ | ├── /server.js # Express app
│ | ├── /server-iso.js # Express app with isomorphic rendering (must use routes-iso.js)
│ | ├── /index.js # Server entry point (with babel-register)
├── /test/ # Mocha tests (e.g. xxx_spec.js)
├── /coverage/ # Code coverage data
│── .env # **Server-side configuration variables**
│── Procfile # Heroku startup commands
│── package.json # The list of 3rd party libraries and utilities and NPM scripts
│── webpack.config.dev.js # Webpack Development Configuration File
└── webpack.config.prod.js # Webpack Production Configuration File
```
11 changes: 11 additions & 0 deletions app.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
{
"name": "react-production-starter",
"scripts": {
},
"env": {

},
"addons": [

]
}
15 changes: 7 additions & 8 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -7,9 +7,10 @@
"test": "mocha --compilers js:babel-core/register --recursive",
"test:watch": "npm test -- --watch",
"start": "NODE_ENV=development node ./src/server",
"start:prod": "NODE_ENV=production node ./src/server",
"build:client": "npm run clean && webpack -d --inline --config ./webpack.config.prod.js",
"clean": "rm -rf build"
"start:prod": "npm run build && NODE_ENV=production node ./src/server",
"build": "npm run clean && webpack -p --inline --config ./webpack.config.prod.js",
"clean": "rm -rf build",
"postinstall": "npm run build"
},
"repository": {
"type": "git",
Expand Down Expand Up @@ -39,14 +40,12 @@
"morgan": "^1.6.1",
"react": "^0.14.7",
"react-dom": "^0.14.7",
"react-helmet": "^2.3.1",
"react-redux": "^4.4.0",
"react-router": "^1.0.3",
"redial": "^0.4.1",
"react-router": "^2.0.0",
"redux": "^3.3.1",
"redux-thunk": "^1.0.3",
"superagent": "^1.7.2"
},
"devDependencies": {
"superagent": "^1.7.2",
"autoprefixer": "^6.3.1",
"babel-core": "^6.5.1",
"babel-loader": "^6.2.2",
Expand Down
57 changes: 19 additions & 38 deletions src/client.js
Original file line number Diff line number Diff line change
@@ -1,43 +1,28 @@
// import 'babel-polyfill';
import 'babel-polyfill';
import { trigger } from 'redial';

import React from 'react';
import { render } from 'react-dom';
import createBrowserHistory from 'history/lib/createBrowserHistory';
import useQueries from 'history/lib/useQueries';
import { Router, match } from 'react-router';
import { createStore, applyMiddleware, compose } from 'redux';
import { Router, match, browserHistory as history } from 'react-router';
import { Provider } from 'react-redux';
import thunk from 'redux-thunk';
import {callAPIMiddleware} from './middleware/callAPIMiddleware';
import { StyleSheet } from 'aphrodite';

// Your app's reducer and routes:
import createReducer from './reducers';
import createRoutes from './routes/root';
import { configureStore } from './store';

// Render the app client-side to a given container element:
// Your server rendered response needs to expose the state of the store, e.g.
// <script>
// window.INITIAL_STATE = <%- JSON.stringify(store.getState())%>
// </script>
const initialState = window.INITIAL_STATE || {};

// Set up Redux (note: this API requires redux@>=3.1.0):
const store = configureStore(initialState);

const { dispatch } = store;

// Set up history for router and listen for changes:
const history = useQueries(createBrowserHistory)();

// const gen = genRoutes(store);
const { pathname, search, hash } = window.location;
const location = `${pathname}${search}${hash}`;
const routes = createRoutes(store);
console.log('Client: ' + routes);

const callProvidedHooks = (location, callback) => {
StyleSheet.rehydrate(window.renderedClassNames);

const callProvidedHooks = (location, callback) => {
// Match routes based on location object:
match({ routes, location }, (routerError, redirectLocation, renderProps) => {
// Get array of route components:
Expand All @@ -52,36 +37,32 @@ const callProvidedHooks = (location, callback) => {
// Allow lifecycle hooks to dispatch Redux actions:
dispatch,
};

// Don't fetch data for initial route, server has already done the work:
const invoke = window.INITIAL_STATE ? Promise.resolve.bind(Promise) : trigger;

invoke('fetch', components, locals) // Fetch mandatory data dependencies for 2nd route change onwards
.then(() => trigger('defer', components, locals)) // Fetch deferred, client-only data dependencies
.then(() => trigger('done', components, locals)) // Finally, trigger 'done' lifecycle hooks
.then(callback);
.then(() => trigger('defer', components, locals)) // Fetch deferred, client-only data dependencies
.then(() => trigger('done', components, locals)) // Finally, trigger 'done' lifecycle hooks
.then(callback);
});
};

// Handle initial rendering
history.listen((location) => {
if (window.INITIAL_STATE) {
// Delete global data so that subsequent data fetches can occur:
callProvidedHooks(location, () => {
delete window.INITIAL_STATE;
delete window.renderedClassNames;
});
callProvidedHooks(location, () => delete window.INITIAL_STATE);
}
});

//
// // // // Handle following rendering
// Handle following rendering
history.listenBefore(callProvidedHooks);
StyleSheet.rehydrate(window.renderedClassNames);

// Render app with Redux and router context to container element:
render((
<Provider store={store}>
<Router history={history} routes={routes} />
</Provider>
), document.getElementById('root'));

match({ routes, location }, () => {
render((
<Provider store={store}>
<Router routes={routes} history={history}/>
</Provider>
), document.getElementById('root'));
});
14 changes: 0 additions & 14 deletions src/routes-old.js

This file was deleted.

2 changes: 2 additions & 0 deletions src/routes/Editor/index.js
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
if (typeof require.ensure !== 'function') require.ensure = (d, c) => c(require);

export default {
path: 'edit',
getComponent(location, cb) {
Expand Down
1 change: 1 addition & 0 deletions src/routes/Post/index.js
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
if (typeof require.ensure !== 'function') require.ensure = (d, c) => c(require);
import { injectAsyncReducer } from '../../store';

export default function createRoutes(store) {
Expand Down
135 changes: 0 additions & 135 deletions src/server/server-copy.js

This file was deleted.

Loading