├── .gitignore ├── src ├── styles │ ├── components │ │ ├── _all.scss │ │ └── _Hello.scss │ ├── base │ │ ├── _all.scss │ │ ├── _typography.scss │ │ └── _reset.scss │ └── main.scss ├── containers │ ├── routes │ │ ├── Routes.js │ │ ├── Bar.js │ │ ├── Foo.js │ │ ├── Home.js │ │ └── RoutesAsync.js │ ├── Root.js │ └── App.js ├── components │ └── Hello.js ├── redux │ ├── modules │ │ ├── promise.js │ │ ├── index.js │ │ └── sample.js │ └── configureStore.js ├── helpers │ ├── createPromiseAction.js │ ├── promiseWaiter.js │ └── pender.js ├── index.js └── serverRenderer.js ├── public └── index.html ├── server └── index.js ├── config ├── paths.js ├── webpack.config.server.js ├── webpack.config.js └── webpack.config.dev.js ├── package.json └── README.md /.gitignore: -------------------------------------------------------------------------------- 1 | yarn.lock 2 | node_modules 3 | build 4 | -------------------------------------------------------------------------------- /src/styles/components/_all.scss: -------------------------------------------------------------------------------- 1 | @import 'Hello'; 2 | -------------------------------------------------------------------------------- /src/styles/base/_all.scss: -------------------------------------------------------------------------------- 1 | @import 'reset'; 2 | @import 'typography'; 3 | -------------------------------------------------------------------------------- /src/styles/components/_Hello.scss: -------------------------------------------------------------------------------- 1 | .hello { 2 | font-weight: 300; 3 | } 4 | -------------------------------------------------------------------------------- /src/styles/main.scss: -------------------------------------------------------------------------------- 1 | @import 'base/all'; 2 | @import 'components/all'; 3 | -------------------------------------------------------------------------------- /src/styles/base/_typography.scss: -------------------------------------------------------------------------------- 1 | @import url(https://fonts.googleapis.com/earlyaccess/notosanskr.css); 2 | -------------------------------------------------------------------------------- /src/styles/base/_reset.scss: -------------------------------------------------------------------------------- 1 | body { 2 | background: #f1f3f5; 3 | font-family: 'Noto Sans KR', 'NanumGothic', sans-serif; 4 | } 5 | -------------------------------------------------------------------------------- /src/containers/routes/Routes.js: -------------------------------------------------------------------------------- 1 | export { default as Home } from './Home'; 2 | export { default as Foo } from './Foo'; 3 | export { default as Bar } from './Bar'; 4 | -------------------------------------------------------------------------------- /src/components/Hello.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | 3 | const Hello = ({name}) => ( 4 |

Hello, {name}

5 | ); 6 | 7 | export default Hello; 8 | -------------------------------------------------------------------------------- /src/redux/modules/promise.js: -------------------------------------------------------------------------------- 1 | export default (state = [], { type, promise }) => { 2 | return !process.browser && type === 'AWAIT_PROMISE' ? state.concat(promise) : state 3 | } -------------------------------------------------------------------------------- /src/containers/routes/Bar.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import Hello from 'components/Hello'; 3 | 4 | const Bar = () => ( 5 | 6 | ); 7 | 8 | export default Bar; -------------------------------------------------------------------------------- /src/containers/routes/Foo.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import Hello from 'components/Hello'; 3 | 4 | const Foo = () => ( 5 | 6 | ); 7 | 8 | export default Foo; -------------------------------------------------------------------------------- /src/helpers/createPromiseAction.js: -------------------------------------------------------------------------------- 1 | const createPromiseAction = ({type, promiseCreator}) => (payload) => ({ 2 | type, 3 | payload: { 4 | promise: promiseCreator(payload) 5 | } 6 | }) 7 | 8 | 9 | export default createPromiseAction; -------------------------------------------------------------------------------- /src/redux/modules/index.js: -------------------------------------------------------------------------------- 1 | import { combineReducers } from 'redux'; 2 | import sample from './sample'; 3 | import promise from './promise'; 4 | 5 | 6 | const modules = combineReducers({ 7 | sample, 8 | promise 9 | }); 10 | 11 | export default modules; 12 | -------------------------------------------------------------------------------- /public/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | React App 6 | 7 | 8 |
9 | 10 | 11 | -------------------------------------------------------------------------------- /server/index.js: -------------------------------------------------------------------------------- 1 | const express = require('express'); 2 | const app = express(); 3 | const path = require('path'); 4 | const renderer = require('./renderer'); 5 | 6 | 7 | app.get('*', renderer); 8 | app.use(express.static(path.join(__dirname, '../build'))); 9 | 10 | 11 | app.listen(8080, ()=>{ 12 | console.log('Listening on port 8080'); 13 | }) -------------------------------------------------------------------------------- /src/helpers/promiseWaiter.js: -------------------------------------------------------------------------------- 1 | const promiseWaiter = store => next => action => { 2 | // if it is browser, skip this process 3 | if(process.browser) return next(action); 4 | 5 | const { payload } = action; 6 | 7 | if(!payload) return next(action); 8 | 9 | const promise = action.payload.promise; 10 | 11 | if(!promise) return next(action); 12 | 13 | store.dispatch({ type: 'AWAIT_PROMISE', promise}); 14 | next(action); 15 | } 16 | 17 | export default promiseWaiter; -------------------------------------------------------------------------------- /src/index.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import ReactDOM from 'react-dom'; 3 | import { AppContainer } from 'react-hot-loader'; 4 | import Root from './containers/Root'; 5 | 6 | const rootEl = document.getElementById('root'); 7 | 8 | const render = Component => 9 | ReactDOM.render( 10 | 11 | 12 | , 13 | rootEl 14 | ); 15 | 16 | 17 | render(Root); 18 | 19 | if (module.hot) module.hot.accept('./containers/Root', () => render(Root)); -------------------------------------------------------------------------------- /config/paths.js: -------------------------------------------------------------------------------- 1 | const path = require('path'); 2 | const fs = require('fs'); 3 | 4 | const appDirectory = fs.realpathSync(process.cwd()); 5 | 6 | function resolvePath(relativePath) { 7 | return path.resolve(appDirectory, relativePath); 8 | } 9 | 10 | module.exports = { 11 | context: resolvePath('src/'), 12 | appIndexJs: resolvePath('src/index.js'), 13 | appStyle: resolvePath('src/styles/main.scss'), 14 | appHtml: resolvePath('public/index.html'), 15 | vendor: ['react', 'react-dom'], 16 | appBuild: resolvePath('build'), 17 | serverRendererJs: resolvePath('src/serverRenderer.js'), 18 | server: resolvePath('server') 19 | } -------------------------------------------------------------------------------- /src/containers/Root.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | 3 | import App from './App'; 4 | 5 | import { Provider } from 'react-redux'; 6 | import BrowserRouter from 'react-router-dom/BrowserRouter' 7 | import configureStore from 'redux/configureStore'; 8 | 9 | import transit from 'transit-immutable-js'; 10 | 11 | // deserialize the JSON to Immutable 12 | const preloadedState = window.__PRELOADED_STATE__ ? transit.fromJSON(window.__PRELOADED_STATE__) : undefined; 13 | 14 | const store = configureStore(preloadedState); 15 | 16 | console.log(store); 17 | const Root = () => ( 18 | 19 | 20 | 21 | 22 | 23 | ); 24 | 25 | export default Root -------------------------------------------------------------------------------- /src/redux/configureStore.js: -------------------------------------------------------------------------------- 1 | import { createStore, applyMiddleware, combineReducers, compose } from 'redux'; 2 | import promiseMiddleware from 'redux-promise-middleware'; 3 | import promiseWaiter from 'helpers/promiseWaiter'; 4 | 5 | import modules from './modules'; 6 | 7 | 8 | const composeEnhancers = process.browser ? (window.__REDUX_DEVTOOLS_EXTENSION_COMPOSE__ || compose) : compose; 9 | 10 | export default function configureStore(initialState) { 11 | const store = createStore(modules, initialState, composeEnhancers(applyMiddleware(promiseWaiter, promiseMiddleware()))); 12 | 13 | if (module.hot) { 14 | // Enable Webpack hot module replacement for reducers 15 | module.hot.accept('./modules', () => { 16 | const nextRootReducer = require('./modules'); 17 | store.replaceReducer(nextRootReducer); 18 | }); 19 | } 20 | 21 | return store; 22 | } -------------------------------------------------------------------------------- /src/containers/routes/Home.js: -------------------------------------------------------------------------------- 1 | import React, {Component} from 'react'; 2 | import Hello from 'components/Hello'; 3 | import * as sample from 'redux/modules/sample'; 4 | import { connect } from 'react-redux'; 5 | import { bindActionCreators } from 'redux'; 6 | 7 | 8 | class Home extends Component { 9 | componentWillMount() { 10 | const { SampleActions } = this.props; 11 | SampleActions.fetchData({ 12 | text: 'hello' 13 | }); 14 | } 15 | 16 | render() { 17 | const { status } = this.props; 18 | 19 | return ( 20 |
21 | 22 | { JSON.stringify(status.sample.get('result')) } 23 |
24 | ) 25 | } 26 | } 27 | 28 | export default connect( 29 | state => ({ 30 | status: { 31 | sample: state.sample 32 | } 33 | }), 34 | dispatch => ({ 35 | SampleActions: bindActionCreators(sample, dispatch) 36 | }) 37 | )(Home); -------------------------------------------------------------------------------- /config/webpack.config.server.js: -------------------------------------------------------------------------------- 1 | const paths = require('./paths'); 2 | const webpack = require('webpack'); 3 | 4 | process.env.NODE_ENV = 'development'; 5 | 6 | module.exports = { 7 | context: paths.context, 8 | entry: [paths.serverRendererJs], 9 | target: 'node', 10 | output: { 11 | path: paths.server, 12 | filename: 'renderer.js', 13 | libraryTarget: "commonjs2" 14 | }, 15 | module: { 16 | rules: [ 17 | { 18 | exclude: [ 19 | /\.(js|jsx)$/, 20 | /\.json$/ 21 | ], 22 | loader: 'ignore', 23 | }, 24 | { 25 | test: /\.js$/, 26 | exclude: [/node_modules/], 27 | use: [{ 28 | loader: 'babel-loader', 29 | options: { presets: ['react-app'] } 30 | }], 31 | }, 32 | ], 33 | }, 34 | resolve: { 35 | modules: [ 36 | paths.context, 37 | 'node_modules' 38 | ] 39 | } 40 | }; 41 | -------------------------------------------------------------------------------- /src/containers/routes/RoutesAsync.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | 3 | function asyncRoute(getComponent) { 4 | return class AsyncComponent extends React.Component { 5 | static Component = null; 6 | mounted = false; 7 | 8 | state = { 9 | Component: AsyncComponent.Component 10 | }; 11 | 12 | componentWillMount() { 13 | if ( this.state.Component === null ) { 14 | getComponent().then(m => m.default).then(Component => { 15 | AsyncComponent.Component = Component; 16 | if ( this.mounted ) { 17 | this.setState({Component}); 18 | } 19 | }) 20 | } 21 | } 22 | 23 | componentDidMount() { 24 | this.mounted = true; 25 | } 26 | 27 | componentWillUnmount() { 28 | this.mounted = false; 29 | } 30 | 31 | render() { 32 | const {Component} = this.state; 33 | 34 | if ( Component !== null ) { 35 | return 36 | } 37 | return null; // or
with a loading spinner, etc.. 38 | } 39 | } 40 | } 41 | 42 | export const Home = asyncRoute(() => System.import('./Home')); 43 | export const Foo = asyncRoute(() => System.import('./Foo')); 44 | export const Bar = asyncRoute(() => System.import('./Bar')); -------------------------------------------------------------------------------- /src/redux/modules/sample.js: -------------------------------------------------------------------------------- 1 | import { createAction, handleActions } from 'redux-actions'; 2 | import createPromiseAction from 'helpers/createPromiseAction'; 3 | import { Map } from 'immutable'; 4 | import pender from 'helpers/pender'; 5 | 6 | const SOMETHING_DO = 'sample/SOMETHING_DO'; 7 | const DATA_FETCH = 'sample/DATA_FETCH'; 8 | 9 | function fakeFetch(payload) { 10 | return new Promise(resolve => { 11 | setTimeout(()=>{ 12 | resolve({ result: payload }); 13 | }, 500); 14 | }); 15 | } 16 | 17 | export const doSomething = createAction(SOMETHING_DO); 18 | 19 | export const fetchData = createPromiseAction({ 20 | type: DATA_FETCH, 21 | promiseCreator: fakeFetch 22 | }); 23 | 24 | const initialState = Map({ 25 | pending: Map({ 26 | fetchData: false 27 | }), 28 | something: null, 29 | result: null 30 | }); 31 | 32 | export default handleActions({ 33 | // sample action handler 34 | [SOMETHING_DO]: (state, action) => { 35 | return state.set('something', 'done'); 36 | }, 37 | 38 | ...pender({ 39 | type: DATA_FETCH, 40 | name: 'fetchData', 41 | onFulfill: (state, action) => { 42 | return state.set('result', action.payload.result); 43 | } 44 | }) 45 | 46 | }, initialState); -------------------------------------------------------------------------------- /src/containers/App.js: -------------------------------------------------------------------------------- 1 | import React, { Component } from 'react'; 2 | 3 | import Router from 'react-router-dom/BrowserRouter' 4 | import Route from 'react-router-dom/Route' 5 | import Link from 'react-router-dom/Link'; 6 | import * as Routes from './routes/Routes'; 7 | 8 | // import asyncComponent from 'helpers/async-component'; 9 | 10 | // const Home = asyncComponent(() => 11 | // System.import('./routes/Home').then(module => module.default) 12 | // ); 13 | // const Foo = asyncComponent(() => 14 | // System.import('./routes/Foo').then(module => module.default) 15 | // ); 16 | // const Bar = asyncComponent(() => 17 | // System.import('./routes/Bar').then(module => module.default) 18 | // ); 19 | 20 | class App extends Component { 21 | render() { 22 | return ( 23 | 24 |
25 |
    26 |
  • Home
  • 27 |
  • Foo
  • 28 |
  • Bar
  • 29 |
30 |
31 | 32 | 33 | 34 |
35 | 36 | ); 37 | } 38 | } 39 | 40 | export default App; 41 | -------------------------------------------------------------------------------- /src/helpers/pender.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Transforms action type to ACTION\_TYPE\_[PENDING, FULFILLED, REJECTED] 3 | * 4 | * @param {string} type 5 | * @returns {object} actions 6 | */ 7 | const actionize = (type) => { 8 | return { 9 | PENDING: `${type}_PENDING`, 10 | FULFILLED: `${type}_FULFILLED`, 11 | REJECTED: `${type}_REJECTED` 12 | } 13 | } 14 | 15 | /* 16 | usage: 17 | handleActions({ 18 | ...pender({ 19 | type: SOMETHING, 20 | onFulfill: (state, action) => { 21 | return state; 22 | }, 23 | onReject: (state, ction) => { 24 | return state; 25 | } 26 | }) 27 | }) 28 | */ 29 | 30 | 31 | /** 32 | * Creates Promise Handlers 33 | */ 34 | const pender = ({ 35 | type, 36 | name, 37 | onFulfill = (state) => state, // in case function not given 38 | onReject = (state) => state 39 | }) => { 40 | const actionized = actionize(type); 41 | 42 | return { 43 | [actionized.PENDING]: (state, action) => { 44 | return state.setIn(['pending', name], true); 45 | }, 46 | [actionized.FULFILLED]: (state, action) => { 47 | return onFulfill(state, action).setIn(['pending', name], false); 48 | }, 49 | [actionized.REJECTED]: (state, action) => { 50 | return onReject(state,action).setIn(['pending', name], false); 51 | } 52 | } 53 | } 54 | 55 | export default pender; -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "react-webpack2-skeleton", 3 | "version": "1.0.0", 4 | "description": "Get started with React with Webpack2", 5 | "main": "index.js", 6 | "license": "MIT", 7 | "scripts": { 8 | "start:dev": "webpack-dev-server --config config/webpack.config.dev.js", 9 | "start": "node server", 10 | "build": "rimraf build && webpack --config config/webpack.config.js", 11 | "build:server": "webpack --config config/webpack.config.server.js" 12 | }, 13 | "devDependencies": { 14 | "babel-core": "^6.23.1", 15 | "babel-loader": "^6.3.2", 16 | "babel-plugin-transform-class-properties": "^6.23.0", 17 | "babel-plugin-transform-object-rest-spread": "^6.23.0", 18 | "babel-plugin-transform-regenerator": "^6.22.0", 19 | "babel-plugin-transform-runtime": "^6.23.0", 20 | "babel-preset-latest": "^6.22.0", 21 | "babel-preset-react": "^6.23.0", 22 | "babel-preset-react-app": "^2.1.1", 23 | "babel-runtime": "^6.23.0", 24 | "css-loader": "^0.26.2", 25 | "extract-text-webpack-plugin": "^2.0.0", 26 | "html-webpack-plugin": "^2.28.0", 27 | "ignore-loader": "^0.1.2", 28 | "node-sass": "^4.5.0", 29 | "postcss-loader": "^1.3.2", 30 | "react-hot-loader": "next", 31 | "rimraf": "^2.6.1", 32 | "sass-loader": "^6.0.2", 33 | "style-loader": "^0.13.2", 34 | "url-loader": "^0.5.8", 35 | "webpack": "^2.2.1", 36 | "webpack-dev-server": "^2.4.1" 37 | }, 38 | "dependencies": { 39 | "express": "^4.15.0", 40 | "immutable": "^3.8.1", 41 | "react": "^15.4.2", 42 | "react-dom": "^15.4.2", 43 | "react-redux": "^5.0.3", 44 | "react-router-dom": "next", 45 | "redux": "^3.6.0", 46 | "redux-actions": "^1.2.2", 47 | "redux-promise-middleware": "^4.2.0", 48 | "transit-immutable-js": "^0.7.0", 49 | "transit-js": "^0.8.846" 50 | } 51 | } 52 | -------------------------------------------------------------------------------- /src/serverRenderer.js: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import ReactDOMServer from 'react-dom/server' 3 | import { StaticRouter } from 'react-router' 4 | import App from 'containers/App'; 5 | 6 | import { Provider } from 'react-redux'; 7 | import configureStore from 'redux/configureStore'; 8 | import transit from 'transit-immutable-js'; 9 | 10 | const renderApp = (context, store, location) => ReactDOMServer.renderToString( 11 | 12 | 13 | 14 | 15 | 16 | ); 17 | 18 | const renderPage = (html, preloadedState) => { 19 | return ` 20 | 21 | 22 | 23 | 24 | 25 | React App 26 | 27 | 28 | 29 | 30 |
31 | ${html} 32 |
33 | 38 | 39 | 40 | 41 | 42 | 43 | ` 44 | } 45 | 46 | const renderer = (req, res, next) => { 47 | 48 | // skips static directory 49 | if(req.path === '/favicon.ico' || req.path.split('/')[1] === 'static') return next(); 50 | 51 | // This context object contains the results of the render 52 | const context = {} 53 | 54 | const store = configureStore(); 55 | const location = req.url; 56 | 57 | renderApp(context, store, location); 58 | 59 | // context.url will contain the URL to redirect to if a was used 60 | if (context.url) { 61 | res.writeHead(302, { 62 | Location: context.url 63 | }) 64 | res.end() 65 | } else { 66 | Promise.all(store.getState().promise).then( 67 | () => { 68 | // serialize the store, except the promise reducer (transit cannot handle it) 69 | store.getState().promise = []; 70 | var serialized = transit.toJSON(store.getState()); 71 | res.write(renderPage(renderApp(context, store, location), serialized)); 72 | res.end(); 73 | } 74 | ).catch( 75 | (error) => { 76 | console.log(error); 77 | res.status(400); 78 | res.end(); 79 | } 80 | ) 81 | 82 | } 83 | } 84 | 85 | module.exports = renderer; -------------------------------------------------------------------------------- /config/webpack.config.js: -------------------------------------------------------------------------------- 1 | const paths = require('./paths'); 2 | const webpack = require('webpack'); 3 | const HtmlWebpackPlugin = require('html-webpack-plugin'); 4 | const ExtractTextPlugin = require('extract-text-webpack-plugin'); 5 | 6 | process.env.NODE_ENV = 'production'; 7 | 8 | module.exports = { 9 | context: paths.context, 10 | entry: [paths.appIndexJs, paths.appStyle], 11 | output: { 12 | path: paths.appBuild, 13 | filename: 'static/js/[name].bundle.js' 14 | }, 15 | module: { 16 | rules: [ 17 | { 18 | exclude: [ 19 | /\.html$/, 20 | /\.(js|jsx)$/, 21 | /\.(css|scss)$/, 22 | /\.json$/ 23 | ], 24 | loader: 'url', 25 | query: { 26 | limit: 10000, 27 | name: 'static/media/[name].[hash:8].[ext]' 28 | } 29 | }, 30 | { 31 | test: /\.js$/, 32 | exclude: [/node_modules/], 33 | use: [{ 34 | loader: 'babel-loader', 35 | options: { presets: ['react-app'] } 36 | }], 37 | }, 38 | { 39 | test: /\.css$/, 40 | loader: ExtractTextPlugin.extract({ 41 | fallback: 'style-loader', 42 | use: [ 43 | { 44 | loader: 'css-loader', 45 | options: { 46 | importLoaders: 1 47 | } 48 | }, 49 | { 50 | loader: 'postcss-loader', 51 | options: { 52 | plugins: function () { 53 | return [ 54 | require('autoprefixer'), 55 | require('cssnano') 56 | ]; 57 | } 58 | } 59 | } 60 | ] 61 | }) 62 | }, 63 | { 64 | test: /\.scss$/, 65 | loader: ExtractTextPlugin.extract({ 66 | fallback: 'style-loader', 67 | use: [ 68 | { 69 | loader: 'css-loader', 70 | options: { 71 | importLoaders: 1 72 | } 73 | }, 74 | { 75 | loader: 'postcss-loader', 76 | options: { 77 | plugins: function () { 78 | return [ 79 | require('autoprefixer'), 80 | require('cssnano') 81 | ]; 82 | } 83 | } 84 | }, 85 | 'sass-loader' 86 | ] 87 | }) 88 | } 89 | ], 90 | }, 91 | plugins: [ 92 | // Generates an `index.html` file with the