├── .babelrc ├── .eslintignore ├── .eslintrc ├── .gitignore ├── .hound.yml ├── .travis.yml ├── Procfile ├── README.md ├── app.json ├── appveyor.yml ├── package.json ├── server.js ├── src ├── actions │ └── app.js ├── constants │ └── action-types.js ├── containers │ ├── __tests__ │ │ └── app.js │ ├── app.css │ ├── app.js │ └── layout.js ├── index.html ├── index.js ├── reducers │ ├── app.js │ └── index.js ├── routes.js ├── store │ └── index.js └── test │ ├── polyfills.js │ └── setup.js ├── webpack.config.js └── webpack.production.js /.babelrc: -------------------------------------------------------------------------------- 1 | { 2 | "presets": ["react", "es2015", "stage-0"], 3 | "env": { 4 | "development": { 5 | "plugins": [ 6 | ["react-transform", { 7 | "transforms": [ 8 | { 9 | "transform": "react-transform-hmr", 10 | "imports": ["react"], 11 | "locals": ["module"], 12 | }, { 13 | "transform": "react-transform-catch-errors", 14 | "imports": ["react", "redbox-react"], 15 | }, 16 | ] 17 | }] 18 | ] 19 | } 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /.eslintignore: -------------------------------------------------------------------------------- 1 | dist/* 2 | node_modules/* 3 | **/node_modules/* 4 | -------------------------------------------------------------------------------- /.eslintrc: -------------------------------------------------------------------------------- 1 | { 2 | "parser": "babel-eslint", 3 | "extends": "airbnb", 4 | "rules": { 5 | "comma-dangle": 0, 6 | "react/jsx-filename-extension": 0, 7 | "arrow-parens": 0, 8 | "jsx-a11y/href-no-hash": "off" 9 | }, 10 | "globals": { 11 | "__DEV__": true, 12 | "expect": true, 13 | "window": true, 14 | "document": true 15 | }, 16 | "env": { 17 | "mocha": true, 18 | "browser": true, 19 | }, 20 | "settings": { 21 | "import/resolver": "webpack" 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Ignore installed node modules 2 | node_modules 3 | 4 | # Ignore final files 5 | dist 6 | 7 | # Ignore logs 8 | npm-debug.log 9 | -------------------------------------------------------------------------------- /.hound.yml: -------------------------------------------------------------------------------- 1 | javascript: 2 | enabled: false 3 | eslint: 4 | enabled: true 5 | config_file: .eslintrc 6 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: node_js 2 | node_js: 3 | - "6" 4 | script: 5 | - npm test 6 | after_script: 7 | - npm run eslint 8 | -------------------------------------------------------------------------------- /Procfile: -------------------------------------------------------------------------------- 1 | web: npm run production 2 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # React 2 | 3 | [![Build Status](https://travis-ci.org/werein/react.svg)](https://travis-ci.org/werein/react)[![Windows Build Status](https://ci.appveyor.com/api/projects/status/github/werein/react?branch=master&svg=true)](https://ci.appveyor.com/project/jirikolarik/react) [![Code Climate](https://codeclimate.com/github/werein/react/badges/gpa.svg)](https://codeclimate.com/github/werein/react) 4 | 5 | [![Deploy](https://www.herokucdn.com/deploy/button.svg)](https://heroku.com/deploy) 6 | 7 | Extremely simple boilerplate, __easiest you can find__, for React application including all the necessary tool 8 | 9 | * React 16 10 | * Redux 11 | * Webpack 3 12 | * Babel 6 13 | * CSS modules 14 | * React Router 15 | * Connected Router (react router + redux) 16 | * Redux DevTools 17 | * Eslint 18 | * Jest & Enzyme 19 | * Express 20 | 21 | And optional (just install missing dependencies): 22 | 23 | * Sass/SCSS - `npm i node-sass sass-loader --save` 24 | * Surge - `npm i surge -g` 25 | 26 | __Also take a look [here](http://github.com/werein/react-native) for also extremely simple React Native boilerplate__ 27 | 28 | ## Flow 29 | 30 | There is a version with `flow` enabled, since everyone is using it these days and quite a lot of libraries has direct support, I've also created a branch where `flow` is supported. This setup allows you for example to see an action and theirs data in correctly in reducers switch statement, doesn't let you to dispatch and undefined action or to see state in a `mapStateToProp`. Please thumbs up or don't in a PR for merge: https://github.com/werein/react/pull/52 31 | 32 | ## Installation 33 | 34 | All you need to do is clone this repository 35 | ``` 36 | git clone https://github.com/werein/react.git 37 | ``` 38 | 39 | ### Keep it up to date 40 | 41 | Track this repo 42 | 43 | ``` 44 | git remote add upstream https://github.com/werein/react.git 45 | ``` 46 | 47 | Get the latest version and apply onto your stack 48 | 49 | ``` 50 | git fetch upstream 51 | git merge upstream/master 52 | ``` 53 | 54 | ## Running 55 | Application has very few dependencies, so it’s most probably very easy to understand when you scan through the code, but there is at least few steps you should know 56 | 57 | ### Start front-end React application 58 | Application is divided into two parts. One is pure React front-end, powered by `webpack-dev-server` in development mode. 59 | 60 | To start this application run command below and open your app on `http://localhost:8080` 61 | 62 | ```javascript 63 | npm start 64 | ``` 65 | 66 | To test your application, run 67 | 68 | * `npm run test` - single run - good for CI or precook 69 | * `npm run test:watch` - watches for changes, good for development 70 | 71 | If you don’t plan to connect to your own backend, you should be just fine 72 | 73 | ### Start Express back-end 74 | Second part of this application is back end written in Express. This is a place, where you provide API for front-end or/and server yours production application. 75 | 76 | To start backend server, run npm command bellow and open `http://localhost:8181` 77 | 78 | ```javascript 79 | npm run server 80 | ``` 81 | This is also watching for changes, so when you update some code on backend, you don’t have to restart the server, it does that automatically 82 | 83 | __Every call which goes to `/api` is proxied to this backend__ so for example when you make a request to `/api/locales` on front-end, it will go to this express backend server using the same path 84 | 85 | ## Production 86 | 87 | Running `npm run build` will create production ready application into your `dist` folder. All you need to do is make this `dist` folder publicly available. You can use `surge.sh` as described bellow to do so. 88 | 89 | Included Express server is preconfigured to serve `/dist` folder. All you need to do is run `npm run server` on your production server. The same is happening automatically, when you deploy to Heroku (It executes this command from `Procfile` 90 | 91 | This is also good to run on your local computer to ensure, that your application is running as it should. 92 | 93 | _Current production size is 205kb and 56.8kb gziped_ 94 | 95 | ## Deployment 96 | 97 | ### Surge.sh 98 | 99 | Simple, single-command web publishing. Publish HTML, CSS, and JS for free, without leaving the command line. 100 | 101 | * Don't forge to install Surge `npm i surge -g` 102 | * Run deployment command - `npm run surge` 103 | 104 | 105 | ### Heroku 106 | 107 | Heroku works out of the box, just use "deploy to heroku" button 108 | 109 | 110 | ## Tools 111 | This project works with ReduxDevtool extension for chrome. [Read more](https://github.com/zalmoxisus/redux-devtools-extension) 112 | 113 | ## License 114 | MIT 115 | -------------------------------------------------------------------------------- /app.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "react", 3 | "description": "A React app using Express", 4 | "keywords": [ 5 | "node", 6 | "react", 7 | "express" 8 | ], 9 | "website": "https://wereinhq.com/guides/react", 10 | "repository": "https://github.com/werein/react", 11 | "success_url": "/", 12 | "env": { 13 | "PORT": { 14 | "description": "Port on which express is running.", 15 | "value": "443" 16 | } 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /appveyor.yml: -------------------------------------------------------------------------------- 1 | environment: 2 | nodejs_version: "6.0" 3 | 4 | install: 5 | - ps: Install-Product node $env:nodejs_version 6 | - npm install 7 | 8 | test_script: 9 | - node --version 10 | - npm --version 11 | - npm test 12 | 13 | build: off 14 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "app", 3 | "version": "1.0.0", 4 | "description": "", 5 | "scripts": { 6 | "start": "webpack-dev-server --hot", 7 | "server": "babel-watch server", 8 | "production": "babel-node server", 9 | "test": "jest", 10 | "test:watch": "jest --watch", 11 | "test:coverage": "jest --coverage", 12 | "postinstall": "npm run build", 13 | "eslint": "eslint .", 14 | "build": "cross-env NODE_ENV=production webpack --config ./webpack.production.js --progress --profile --colors", 15 | "surge": "rm -rf dist && npm run build && cp dist/index.html dist/200.html && surge dist" 16 | }, 17 | "jest": { 18 | "moduleDirectories": [ 19 | "node_modules", 20 | "src" 21 | ], 22 | "moduleNameMapper": { 23 | "^.+\\.(css)$": "identity-obj-proxy" 24 | }, 25 | "setupTestFrameworkScriptFile": "/src/test/setup.js" 26 | }, 27 | "author": "We're in ", 28 | "license": "ISC", 29 | "devDependencies": { 30 | "babel-eslint": "^8.0.1", 31 | "babel-jest": "^21.2.0", 32 | "babel-plugin-react-transform": "^3.0.0", 33 | "babel-watch": "^2.0.7", 34 | "cross-env": "^5.0.5", 35 | "enzyme-adapter-react-16": "^1.0.0", 36 | "eslint": "^4.8.0", 37 | "eslint-config-airbnb": "^15.1.0", 38 | "eslint-import-resolver-webpack": "^0.8.3", 39 | "eslint-plugin-import": "^2.7.0", 40 | "eslint-plugin-jsx-a11y": "^6.0.2", 41 | "eslint-plugin-react": "^7.4.0", 42 | "identity-obj-proxy": "^3.0.0", 43 | "imports-loader": "^0.7.1", 44 | "jest-cli": "^21.2.1", 45 | "react-addons-test-utils": "^15.6.2", 46 | "react-test-renderer": "^16.0.0", 47 | "react-transform-catch-errors": "^1.0.2", 48 | "react-transform-hmr": "^1.0.4", 49 | "redbox-react": "^1.5.0", 50 | "webpack-dev-server": "2.9.1" 51 | }, 52 | "dependencies": { 53 | "babel": "^6.23.0", 54 | "babel-cli": "^6.26.0", 55 | "babel-core": "^6.26.0", 56 | "babel-loader": "^7.1.2", 57 | "babel-polyfill": "^6.26.0", 58 | "babel-preset-es2015": "^6.24.1", 59 | "babel-preset-react": "^6.24.1", 60 | "babel-preset-stage-0": "^6.24.1", 61 | "compression-webpack-plugin": "^1.0.1", 62 | "connected-react-router": "4.2.3", 63 | "css-loader": "^0.28.7", 64 | "enzyme": "^3.0.0", 65 | "express": "^4.16.1", 66 | "extract-text-webpack-plugin": "^3.0.0", 67 | "file-loader": "^1.1.3", 68 | "history": "^4.7.2", 69 | "html-loader": "^0.5.1", 70 | "html-webpack-plugin": "^2.30.1", 71 | "null-loader": "^0.1.1", 72 | "react": "^16.0.0", 73 | "react-dom": "^16.0.0", 74 | "react-redux": "^5.0.6", 75 | "react-router": "^4.2.0", 76 | "redux": "^3.7.2", 77 | "style-loader": "^0.18.2", 78 | "webpack": "3.6.0" 79 | } 80 | } 81 | -------------------------------------------------------------------------------- /server.js: -------------------------------------------------------------------------------- 1 | const path = require('path'); 2 | const express = require('express'); 3 | 4 | const app = express(); 5 | 6 | const port = process.env.PORT ? process.env.PORT : 8181; 7 | const dist = path.join(__dirname, 'dist'); 8 | 9 | app.use(express.static(dist)); 10 | 11 | app.get('*', (req, res) => { 12 | res.sendFile(path.join(dist, 'index.html')); 13 | }); 14 | 15 | app.listen(port, (error) => { 16 | if (error) { 17 | console.log(error); // eslint-disable-line no-console 18 | } 19 | console.info('Express is listening on port %s.', port); // eslint-disable-line no-console 20 | }); 21 | -------------------------------------------------------------------------------- /src/actions/app.js: -------------------------------------------------------------------------------- 1 | import { APP_LOAD } from 'constants/action-types'; 2 | 3 | export function loadApp() { 4 | return { 5 | type: APP_LOAD, 6 | }; 7 | } 8 | 9 | export default { loadApp }; 10 | -------------------------------------------------------------------------------- /src/constants/action-types.js: -------------------------------------------------------------------------------- 1 | export const APP_LOAD = 'APP_LOAD'; 2 | 3 | export default { APP_LOAD }; 4 | -------------------------------------------------------------------------------- /src/containers/__tests__/app.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { shallow } from 'enzyme'; 3 | import { AppContainer as App } from '../app'; 4 | 5 | const dispatch = () => null; 6 | 7 | describe('', () => { 8 | it('renders container', () => { 9 | const wrapper = shallow(); 10 | expect(wrapper.find('div').length).toBe(0); 11 | }); 12 | 13 | it('renders container', () => { 14 | const wrapper = shallow(); 15 | expect(wrapper.find('div').length).toBe(1); 16 | }); 17 | }); 18 | -------------------------------------------------------------------------------- /src/containers/app.css: -------------------------------------------------------------------------------- 1 | .container { 2 | /*App container styles belongs here*/ 3 | } 4 | -------------------------------------------------------------------------------- /src/containers/app.js: -------------------------------------------------------------------------------- 1 | import React, { Component } from 'react'; 2 | import { connect } from 'react-redux'; 3 | import { loadApp } from 'actions/app'; 4 | import styles from './app.css'; 5 | 6 | type Props = { 7 | dispatch: () => void, 8 | loaded: boolean 9 | } 10 | 11 | export class AppContainer extends Component { 12 | componentDidMount() { 13 | this.props.dispatch(loadApp()); 14 | } 15 | 16 | props: Props; 17 | 18 | render() { 19 | if (!this.props.loaded) { 20 | return null; 21 | } 22 | 23 | return ( 24 |
25 | ); 26 | } 27 | } 28 | 29 | function mapStateToProperties(state) { 30 | return { 31 | loaded: state.app.loaded 32 | }; 33 | } 34 | 35 | export default connect(mapStateToProperties)(AppContainer); 36 | -------------------------------------------------------------------------------- /src/containers/layout.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | 3 | type Props = { 4 | children: any 5 | } 6 | 7 | const Layout = (props: Props) => 8 |
{props.children}
; 9 | 10 | export default Layout; 11 | -------------------------------------------------------------------------------- /src/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | App 6 | 7 | 8 |
9 | 10 | 11 | -------------------------------------------------------------------------------- /src/index.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { render } from 'react-dom'; 3 | import { Provider } from 'react-redux'; 4 | import store from 'store'; 5 | import routes from 'routes'; 6 | 7 | render( 8 | {routes}, 9 | document.getElementById('react') 10 | ); 11 | -------------------------------------------------------------------------------- /src/reducers/app.js: -------------------------------------------------------------------------------- 1 | import { APP_LOAD } from 'constants/action-types'; 2 | 3 | const initialState = { 4 | loaded: false, 5 | }; 6 | 7 | export default function app(state = initialState, action) { 8 | switch (action.type) { 9 | case APP_LOAD: 10 | return { ...state, loaded: true }; 11 | default: 12 | return state; 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /src/reducers/index.js: -------------------------------------------------------------------------------- 1 | export { default as app } from './app'; 2 | 3 | export default {}; 4 | -------------------------------------------------------------------------------- /src/routes.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { Switch, Route } from 'react-router'; 3 | import { ConnectedRouter } from 'connected-react-router'; 4 | import { history } from 'store/index'; 5 | import Layout from 'containers/layout'; 6 | import App from 'containers/app'; 7 | 8 | const routes = ( 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | ); 17 | 18 | export default routes; 19 | -------------------------------------------------------------------------------- /src/store/index.js: -------------------------------------------------------------------------------- 1 | // Create final store using all reducers and applying middleware 2 | import { createBrowserHistory } from 'history'; 3 | // Redux utility functions 4 | import { compose, createStore, combineReducers, applyMiddleware } from 'redux'; 5 | import { routerMiddleware, connectRouter } from 'connected-react-router'; 6 | // Import all reducers 7 | import * as reducers from 'reducers'; 8 | 9 | // Configure reducer to store state at state.router 10 | // You can store it elsewhere by specifying a custom `routerStateSelector` 11 | // in the store enhancer below 12 | export const history = createBrowserHistory(); 13 | const reducer = combineReducers({ ...reducers }); 14 | 15 | const store = compose( 16 | // Enables your middleware: 17 | // applyMiddleware(thunk), // any Redux middleware, e.g. redux-thunk 18 | applyMiddleware(routerMiddleware(history)), 19 | // Provides support for DevTools via Chrome extension 20 | window.devToolsExtension ? window.devToolsExtension() : f => f 21 | )(createStore)(connectRouter(history)(reducer)); 22 | 23 | export default store; 24 | -------------------------------------------------------------------------------- /src/test/polyfills.js: -------------------------------------------------------------------------------- 1 | /* eslint-disable */ 2 | 3 | const raf = global.requestAnimationFrame = (cb) => 4 | setTimeout(cb, 0); 5 | 6 | export default raf; 7 | -------------------------------------------------------------------------------- /src/test/setup.js: -------------------------------------------------------------------------------- 1 | /* eslint-disable */ 2 | 3 | import './polyfills'; 4 | import { configure } from 'enzyme'; 5 | import Adapter from 'enzyme-adapter-react-16'; 6 | 7 | configure({ adapter: new Adapter() }); 8 | -------------------------------------------------------------------------------- /webpack.config.js: -------------------------------------------------------------------------------- 1 | const path = require('path'); 2 | const webpack = require('webpack'); 3 | const HtmlWebpackPlugin = require('html-webpack-plugin'); 4 | 5 | const htmlWebpackPlugin = new HtmlWebpackPlugin({ template: 'index.html' }); 6 | const definePlugin = new webpack.DefinePlugin({ 7 | __DEV__: JSON.stringify(JSON.parse(process.env.NODE_ENV === 'development' || 'true')) 8 | }); 9 | 10 | const stylesheetsLoaders = [ 11 | { loader: 'style-loader' }, 12 | { loader: 'css-loader', 13 | options: { 14 | modules: true, 15 | localIdentName: '[path]-[local]-[hash:base64:3]', 16 | sourceMap: true 17 | } 18 | } 19 | ]; 20 | 21 | module.exports = { 22 | context: path.join(__dirname, 'src'), 23 | entry: './index', 24 | output: { 25 | filename: '[hash].js', 26 | }, 27 | devtool: 'source-map', 28 | plugins: [htmlWebpackPlugin, definePlugin], 29 | resolve: { 30 | modules: ['node_modules', path.join(__dirname, 'src')] 31 | }, 32 | module: { 33 | rules: [ 34 | { 35 | test: /\.js$/, 36 | exclude: /node_modules/, 37 | loader: 'babel-loader' 38 | }, { 39 | test: /\.html$/, 40 | loader: 'html-loader' 41 | }, { 42 | test: /\.css$/, 43 | use: stylesheetsLoaders 44 | }, { 45 | test: /\.scss$/, 46 | use: [...stylesheetsLoaders, { 47 | loader: 'sass-loader', 48 | options: { 49 | sourceMap: true 50 | } 51 | }] 52 | }, { 53 | test: /\.sass$/, 54 | use: [...stylesheetsLoaders, { 55 | loader: 'sass-loader', 56 | options: { 57 | indentedSyntax: 'sass', 58 | sourceMap: true 59 | } 60 | }] 61 | }, { 62 | test: /\.less$/, 63 | use: [...stylesheetsLoaders, { 64 | loader: 'less-loader', 65 | options: { 66 | sourceMap: true 67 | } 68 | }] 69 | } 70 | ] 71 | }, 72 | devServer: { 73 | historyApiFallback: true, 74 | proxy: { 75 | '/api/*': { 76 | target: 'http://localhost:8181', 77 | changeOrigin: true, 78 | pathRewrite: { 79 | '^/api': '' 80 | } 81 | } 82 | } 83 | } 84 | }; 85 | -------------------------------------------------------------------------------- /webpack.production.js: -------------------------------------------------------------------------------- 1 | const path = require('path'); 2 | const webpack = require('webpack'); 3 | const HtmlWebpackPlugin = require('html-webpack-plugin'); 4 | const ExtractTextPlugin = require('extract-text-webpack-plugin'); 5 | const CompressionPlugin = require('compression-webpack-plugin'); 6 | 7 | const stylesheetsLoaders = [{ 8 | loader: 'css-loader', 9 | options: { 10 | modules: true, 11 | localIdentName: '[path]-[local]-[hash:base64:3]', 12 | sourceMap: true 13 | } 14 | } 15 | ]; 16 | 17 | const stylesheetsPlugin = new ExtractTextPlugin('[hash].css'); 18 | const htmlWebpackPlugin = new HtmlWebpackPlugin({ template: 'index.html' }); 19 | const definePlugin = new webpack.DefinePlugin({ 20 | __DEV__: JSON.stringify(JSON.parse(process.env.NODE_ENV === 'development' || 'false')), 21 | 'process.env': { 22 | NODE_ENV: JSON.stringify(process.env.NODE_ENV || 'production') 23 | } 24 | }); 25 | const uglifyPlugin = new webpack.optimize.UglifyJsPlugin({ compress: { warnings: false } }); 26 | const compressionPlugin = new CompressionPlugin(); 27 | 28 | module.exports = { 29 | context: path.join(__dirname, 'src'), 30 | entry: './index', 31 | output: { 32 | publicPath: '/', 33 | filename: '[hash].js', 34 | path: path.join(__dirname, 'dist') 35 | }, 36 | devtool: 'cheap-source-map', 37 | plugins: [ 38 | stylesheetsPlugin, 39 | htmlWebpackPlugin, 40 | definePlugin, 41 | uglifyPlugin, 42 | compressionPlugin 43 | ], 44 | resolve: { 45 | modules: ['node_modules', path.join(__dirname, 'src')] 46 | }, 47 | module: { 48 | rules: [ 49 | { 50 | test: /\.js$/, 51 | exclude: /node_modules/, 52 | loader: 'babel-loader' 53 | }, { 54 | test: /\.html$/, 55 | loader: 'html-loader' 56 | }, { 57 | test: /\.css$/, 58 | use: ExtractTextPlugin.extract({ 59 | fallback: 'style-loader', 60 | use: stylesheetsLoaders 61 | }) 62 | }, { 63 | test: /\.scss$/, 64 | use: ExtractTextPlugin.extract({ 65 | fallback: 'style-loader', 66 | use: [...stylesheetsLoaders, { 67 | loader: 'sass-loader' 68 | }] 69 | }) 70 | }, { 71 | test: /\.sass$/, 72 | use: ExtractTextPlugin.extract({ 73 | fallback: 'style-loader', 74 | use: [...stylesheetsLoaders, { 75 | loader: 'sass-loader', 76 | options: { 77 | indentedSyntax: 'sass', 78 | } 79 | }] 80 | }) 81 | }, { 82 | test: /\.less$/, 83 | use: ExtractTextPlugin.extract({ 84 | fallback: 'style-loader', 85 | use: [...stylesheetsLoaders, { 86 | loader: 'less-loader' 87 | }] 88 | }) 89 | } 90 | ] 91 | } 92 | }; 93 | --------------------------------------------------------------------------------