The idea of this repository is to try out all new concepts and libraries which work great for React.js. Additionally, this will be the boilerplate for koa isomorphic (or universal) application.
So far, I manage to put together these following technologies:
- Koa.js
- Webpack
- Babel
- Flowtype
- Marko
- Bootstrap and FontAwesome
- Redux
- Relay
- Immutablejs
- ServiceWorker and AppCache
- PostCSS, CSSNext, CSSNano
- Mocha, Chai, Testdouble, Nock and Istanbul
- Install redux-devtools-extension to have better experience when developing.
- Install yarn
What initially gets run is build/server.js
, which is complied by Webpack to utilise the power of ES6 and ES7 in server-side code.
In server.js
, I initialse all middlewares from config/middleware/index
, then start server at localhost:3000
. API calls
from client side eventually will request to /api/*
, which are created by app/server/apis
. Rendering tasks will be delegated to
React-Router to do server rendering for React.
Leverage the power of webpack-isomorphic-tools to hack require
module with
the support of external webpack.
(context, request, callback) => {
const regexp = new RegExp(`${assets}$`);
return regexp.test(request)
? callback(null, `commonjs ${path.join(context.replace(ROOT, './../'), request)}`)
: callback();
},
Contains all components and routing.
Binds root component to <div id='app'></div>
, and prepopulate redux store with server-rendering data from window.prerenderData
Handles routing for server, and generates page which will be returned by react-router and marko. I make a facade getUrl
for data fetching in both client and server.
Then performs server-side process.
Custom taglibs are defined under app/server/templates/helpers
. To see it usage, look
for app/server/templates/layout/application.marko
. For example:
<prerender-data data=data.layoutData.prerenderData />
For now, the way to pass data to template is done via layout-data=data
. This makes the current data
accesible at the layouts/application.marko
.
To be able to use the default node require
instead of webpack dynamic require
, use global.nodeRequire
. This is defined
in prod-server.js
to fix the problem that server wants to require somethings that are not bundled into current build. For example,
const { ROOT, PUBLIC } = global.nodeRequire('./config/path-helper');
Note: nodeRequire
will resolve the path from project root directory.
We ask react-router for route which matches the current request and then check to see if has a static fetchData()
function.
If it does, we pass the redux dispatcher to it and collect the promises returned. Those promises will be resolved when each matching route has loaded its
necessary data from the API server. The current implementation is based on redial.
export default (callback) => (ComposedComponent) => {
class FetchDataEnhancer extends ComposedComponent {
render() {
return (
<ComposedComponent { ...this.props } />
);
}
}
return provideHooks({
fetchData(...args) {
return callback(...args);
},
})(FetchDataEnhancer);
};
and
export function serverFetchData(renderProps, store) {
return trigger('fetchData', map('component', renderProps.routes), getLocals(store, renderProps));
}
export function clientFetchData(routes, store) {
browserHistory.listen(location => {
match({ routes, location }, (error, redirectLocation, renderProps) => {
if (error) {
window.location.href = '/500.html';
} else if (redirectLocation) {
window.location.href = redirectLocation.pathname + redirectLocation.search;
} else if (renderProps) {
if (window.prerenderData) {
// Delete initial data so that subsequent data fetches can occur
delete window.prerenderData;
} else {
// Fetch mandatory data dependencies for 2nd route change onwards
trigger('fetchData', renderProps.components, getLocals(store, renderProps));
}
} else {
window.location.href = '/404.html';
}
});
});
}
Takes a look at templates/todos
, we will have sth like @fetchDataEnhancer(({ store }) => store.dispatch(fetchTodos()))
to let the server calls fetchData()
function
on a component from the server.
We rely on isomorphic-relay-router to do the server-rendering path.
IsomorphicRouter.prepareData(renderProps)
.then(({ data: prerenderData, props }) => {
const prerenderComponent = renderToString(
<IsomorphicRouter.RouterContext {...props} />
);
resolve(
this.render(template, {
...parameters,
prerenderComponent,
prerenderData,
})
);
});
this.render:
this.render = this.render || function (template: string, parameters: Object = {}) {...}
Will receive a template and its additional parameters. See settings.js for more info. It will pass this object to template.
this.prerender:
this.prerender = this.prerender || function (template: string, parameters: Object = {}, initialState: Object = {}) {...}
Will receive additional parameter initialState
which is the state of redux store (This will not apply for relay branch).
- Immutablejs: Available on features/immutablejs
- Relay: Available on features/relay
Add .async
to current file will give it the ability to load async (for example, big-component.async.js
)
using react-proxy-loader.
{
test: /\.async\.js$/,
loader: 'react-proxy-loader!exports-loader?exports.default',
},
For now, the best way is to place all logic in the same place with components to make it less painful when scaling the application. Current structure is the combination of ideas from organizing-redux and ducks-modular-redux. Briefly, we will have our reducer, action-types, and actions in the same place with featured components.
Sample for logic-bundle:
import fetch from 'isomorphic-fetch';
import { createAction, handleActions } from 'redux-actions';
import getUrl from 'client/helpers/get-url';
export const ADD_TODO = 'todos/ADD_TODO';
export const REMOVE_TODO = 'todos/REMOVE_TODO';
export const COMPLETE_TODO = 'todos/COMPLETE_TODO';
export const SET_TODOS = 'todos/SET_TODOS';
export const addTodo = createAction(ADD_TODO);
export const removeTodo = createAction(REMOVE_TODO);
export const completeTodo = createAction(COMPLETE_TODO);
export const setTodos = createAction(SET_TODOS);
export const fetchTodos = () => dispatch =>
fetch(getUrl('/api/v1/todos'))
.then(res => res.json())
.then(res => dispatch(setTodos(res)));
const initialState = [];
export default handleActions({
[ADD_TODO]: (state, { payload: text }) => [
...state, { text, complete: false },
],
[REMOVE_TODO]: (state, { payload: index }) => [
...state.slice(0, index),
...state.slice(index + 1),
],
[COMPLETE_TODO]: (state, { payload: index }) => [
...state.slice(0, index),
{ ...state[index], complete: !state[index].complete },
...state.slice(index + 1),
],
[SET_TODOS]: (state, { payload: todos }) => todos,
}, initialState);
- Phusion Passenger server with Nginx
$ git clone [email protected]:hung-phan/koa-react-isomorphic.git
$ cd koa-react-isomorphic
$ yarn install
$ yarn run watch
$ yarn run dev
$ SERVER_RENDERING=true yarn run watch
$ yarn run dev
$ yarn run flow:watch
$ yarn run flow:stop # to terminate the server
You need to add annotation to the file to enable flowtype (// @flow
)
$ yarn test
$ yarn run test:watch
$ yarn run test:lint
$ yarn run test:coverage
$ yarn run watch
$ yarn run debug
If you use tool like Webstorm or any JetBrains product to debug, you need update cli
option in .node-inspectorrc
to prevent
using default browser to debug. Example:
{
"web-port": 9999,
"web-host": null,
"debug-port": 5858,
"save-live-edit": true,
"no-preload": true,
"cli": true,
"hidden": [],
"stack-trace-limit": 50
}
$ yarn run build
$ SECRET_KEY=your_env_key yarn start
$ yarn run build
$ SECRET_KEY=your_env_key yarn run pm2:start
$ yarn run pm2:stop # to terminate the server
$ docker-compose build
$ docker-compose up
Access http://localhost:3000
to see the application
$heroku config:set BUILD_ASSETS=1 # run once
$ heroku create
$ git push heroku master
Feel free to open an issue on the repo.