Giter Club home page Giter Club logo

koa-react-isomorphic's Introduction

React and Koa boilerplate

build status Dependency Status devDependency Status

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:

Explanation

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.

Requirement

Install redux-devtools-extension to have better experience when developing.

Require assets in server

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();
    },

app/routes.js

Contains all components and routing.

app/app.js

Binds root component to <div id='app'></div>, and prepopulate redux store with server-rendering data from window.__data

app/server.js

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.

Marko template

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 />

Partial template data

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.

Node require in server

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.

Server-side data fetching

Redux

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.__data) {
          // Delete initial data so that subsequent data fetches can occur
          delete window.__data;
        } 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.

Relay

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,
      })
    );
  });

Render methods

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).

Features

Async react components

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',
  },

Idea to structure redux application

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.

alt text

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);

Hacky stub

You can use the global.nodeRequire in app to get back the original require of node. It will be usefull in the case you want to require sth at runtime instead of compile time

const module = global.nodeRequire('path');

Upcoming

  • Rxjs
  • Phusion Passenger server with Nginx

Development

$ git clone [email protected]:hung-phan/koa-react-isomorphic.git
$ cd koa-react-isomorphic
$ npm install

Hot reload

$ npm run watch
$ npm run dev

With server rendering - encourage for testing only

$ SERVER_RENDERING=true npm run watch
$ npm run dev

Test

$ npm test
$ npm run test:watch
$ npm run test:lint
$ npm run test:coverage

Debug

$ npm run watch
$ npm run debug

If you use tool like Webstorm or any JetBrains product to debug, you need add -c option to scripts/debug.sh to prevent using default browser to debug. Example: node-debug -p 9999 -b -c prod-server.js.

Enable flowtype in development

$ npm run flow:watch
$ npm run flow:stop # to terminate the server

You need to add annotation to the file to enable flowtype (// @flow)

Production

Normal run

$ npm run build
$ SECRET_KEY=your_env_key npm start

With pm2

$ npm run build
$ SECRET_KEY=your_env_key npm run pm2:start
$ npm run pm2:stop # to terminate the server

Deploy heroku

$ heroku create
$ git push heroku master

koa-react-isomorphic's People

Contributors

hung-phan avatar jounqin avatar nqbao avatar

Watchers

 avatar  avatar  avatar

Recommend Projects

  • React photo React

    A declarative, efficient, and flexible JavaScript library for building user interfaces.

  • Vue.js photo Vue.js

    ๐Ÿ–– Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.

  • Typescript photo Typescript

    TypeScript is a superset of JavaScript that compiles to clean JavaScript output.

  • TensorFlow photo TensorFlow

    An Open Source Machine Learning Framework for Everyone

  • Django photo Django

    The Web framework for perfectionists with deadlines.

  • D3 photo D3

    Bring data to life with SVG, Canvas and HTML. ๐Ÿ“Š๐Ÿ“ˆ๐ŸŽ‰

Recommend Topics

  • javascript

    JavaScript (JS) is a lightweight interpreted programming language with first-class functions.

  • web

    Some thing interesting about web. New door for the world.

  • server

    A server is a program made to process requests and deliver data to clients.

  • Machine learning

    Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.

  • Game

    Some thing interesting about game, make everyone happy.

Recommend Org

  • Facebook photo Facebook

    We are working to build community through open source technology. NB: members must have two-factor auth.

  • Microsoft photo Microsoft

    Open source projects and samples from Microsoft.

  • Google photo Google

    Google โค๏ธ Open Source for everyone.

  • D3 photo D3

    Data-Driven Documents codes.