├── .gitignore
├── README.md
├── examples
├── react-example
│ ├── .gitignore
│ ├── README.md
│ ├── car.js
│ ├── carlist.js
│ ├── carshop.js
│ ├── package.json
│ ├── public
│ │ └── index.html
│ ├── selection.js
│ ├── server.js
│ ├── server
│ │ └── compiler.js
│ └── webpack.config.js
└── redux-example
│ ├── .gitignore
│ ├── README.md
│ ├── actions.js
│ ├── car.js
│ ├── carlist.js
│ ├── carshop.js
│ ├── package.json
│ ├── public
│ └── index.html
│ ├── reducerCars.js
│ ├── root.js
│ ├── selection.js
│ ├── server.js
│ ├── server
│ └── compiler.js
│ └── webpack.config.js
├── full-boilerplate
├── .gitignore
├── actions
│ └── actions.js
├── compiler
│ └── compiler.js
├── package.json
├── public
│ └── index.html
├── reducers
│ ├── reducerGreeting.js
│ └── reducers.js
├── server.js
├── src
│ ├── components
│ │ ├── App.js
│ │ ├── GoodbyeWorld.js
│ │ └── HelloWorld.js
│ ├── index.js
│ ├── root.js
│ └── routes.js
├── webpack.config.js
└── webpack.production.config.js
├── react, express, webpack, basic
├── .babelrc
├── .gitignore
├── index.js
├── package.json
├── public
│ └── index.html
├── server.js
└── webpack.config.js
├── react, express, webpack, development
├── .babelrc
├── .gitignore
├── index.js
├── package.json
├── public
│ └── index.html
├── server.js
├── server
│ └── compiler.js
└── webpack.config.js
├── react, express, webpack, production
├── .babelrc
├── .gitignore
├── index.js
├── npm-debug.log
├── package.json
├── public
│ └── index.html
├── server.js
├── server
│ └── compiler.js
├── webpack.config.js
└── webpack.production.config.js
└── react, redux, webpack, express, production
├── .gitignore
├── actions.js
├── index.js
├── package.json
├── public
└── index.html
├── reducers.js
├── root.js
├── server.js
├── server
└── compiler.js
├── webpack.config.js
└── webpack.production.config.js
/.gitignore:
--------------------------------------------------------------------------------
1 | node_modules
2 | bundle.js
3 | .DS_Store
4 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # Introduction
2 | As I was trying to learn the React ecosystem I had a hard time finding the barebones that I needed in order to start a project. My goal with this repo is to show the minimum that it takes to get a project off the ground in React using Webpack with a server. It has since grown to show a couple of examples--one in just React, and the other converted from just React to React and Redux.
3 |
4 | Read more about webpack here: https://medium.com/@colinlmcdonald/webpack-ec0dddea1195#.1z425oes1
5 |
6 | ## Installation
7 | In any of the folders run:
8 |
9 | npm install
10 |
11 | Please note that a for the production example, npm install will create a 'build' folder inside of 'public'. In order to have the webpack-dev-server running with Express, please delete that folder.
12 |
13 | The reason that it is there is because in package.json there is a 'postinstall' script that is run. This is for deploying to Heroku so that webpack bundles your project correctly before running npm start.
14 |
15 | # React, Webpack, Express, Basic
16 | ## To Start
17 |
18 | webpack OR webpack--watch
19 |
20 | node server.js
21 |
22 | The webpack command bundles your project and exports bundle.js to public/build folder. Webpack --watch does the same thing, but will re-bundle your project whenever you save. The express server serves index.html which points to your bundle.js.
23 |
24 | # React, Webpack, Express, Development
25 | ## To Start
26 |
27 | node server.js
28 |
29 | This example shows how to use an Express server alongside a webpack-dev-server. Webpack-dev-server itself creates a small Express server to serve your bundled static assets from memory. It does not create a physical copy of bundle.js. Your Express server, using http-proxy, proxies all requests to /build and sends it to your webpack-dev-server located on port 8080.
30 |
31 | # React, Webpack, Express, Production
32 | ## To Start
33 |
34 | node server.js
35 |
36 | This is the same as the development example with an added 'production' config for Webpack. It also has a 'postinstall' script that is ran after npm install is ran. The script will create a bundle.js in /public/build. If you want to use the development mode, delete the build folder or comment out the posinstall script in package.json before running npm install.
37 |
38 | The posinstall script is for when deploying to heroku so that a physical bundle.js is created. It also sets the NODE_ENV to production so that requests are not proxied to the webpack-dev-server.
39 |
40 | # React, Redux, Webpack, Express, Production
41 | ## To Start
42 |
43 | node server.js
44 |
45 | This is the same as the production example above but with a basic Redux layout. Redux manages the state of your application in something called a 'store'. Your components dispatch action objects to your store. A reducer then merges the previous state with what you want changed to create the new state.
46 |
47 | It also uses the react-redux module to implicitly pass down your store to your components so that they have access to the dispatch function, which is used to communicate with the store, in their props. See the comments in the files for more information on what is happening.
48 |
49 | # React Example
50 | ## To Start
51 |
52 | node server.js
53 |
54 | This example uses the 'development' webpack with express example mentioned previously. The example is a car shop which has different services. As you click on the different radio buttons, it updates the state with the chosen service. As you click 'add car' a car with that service is added to the corresponding queue. The UI/UX leaves much to be desired, but it shows some basic functionality with React, forms, and utilizing some of the cool things that are capable in ES2015. See the comments for more information.
55 |
56 | # Redux Example
57 | ## To Start
58 |
59 | node server.js
60 |
61 | This example also uses the 'development' webpack with express example. For this project I took the react-example and translated the state changes from the native methods of React into Redux. Redux does not ship with native async support for AJAX calls so you need to install redux-thunk. When the component will mount, an action creator is dispatched to the store. This action fetches the different car services from the server and then dispatches them to the store. See the comments for more information.
62 |
--------------------------------------------------------------------------------
/examples/react-example/.gitignore:
--------------------------------------------------------------------------------
1 | node_modules
--------------------------------------------------------------------------------
/examples/react-example/README.md:
--------------------------------------------------------------------------------
1 | # React Example
2 | ## To Start
3 | npm install
4 |
5 | node server.js
6 |
7 | ## Organization
8 | Carshop.js is the heart of the app. Selection.js contains the radio buttons. Carlist.js is for the different lists of cars that get added to the carshop. Car.js is for an individual car added to list.
9 |
--------------------------------------------------------------------------------
/examples/react-example/car.js:
--------------------------------------------------------------------------------
1 | import React from 'react'
2 |
3 | //See carlist for an explanation of a stateless functional component.
4 | const Car = ({car}) => (
5 |
{car}
6 | )
7 |
8 | export default Car
--------------------------------------------------------------------------------
/examples/react-example/carlist.js:
--------------------------------------------------------------------------------
1 | import React from 'react'
2 | import Car from './car'
3 |
4 | //This is an example of a stateless functional React component. It uses the fat arrow function from ES2015
5 | //to return a single node (in this case a
). Here JSX is used within the HTML to map over the different
6 | //cars that have been passed down from the parent component. Map returns our Car component, passing in an individual
7 | //car. React uses the 'key' property of a component for its diffing algorithm, so it is important to include it
8 | //for performance reasons.
9 | const CarList = ({cars, service}) => (
10 |
17 | )
18 |
19 | export default CarList
20 |
21 | //** Notice that this functional component is being passed {cars, service} as its argument. This is using
22 | //ES2015's object destructuring which takes the object that is passed in from the parent component and makes
23 | //its values available as cars & service.
--------------------------------------------------------------------------------
/examples/react-example/carshop.js:
--------------------------------------------------------------------------------
1 | import React, { Component } from 'react'
2 | import { render } from 'react-dom'
3 | import Service from './selection'
4 | import CarList from './carlist'
5 |
6 | //Constructor is a method on the class object in ES2015. Here we set our initial state and also bind our methods.
7 | class CarShop extends Component {
8 | constructor(props) {
9 | super(props)
10 | this.state = {
11 | services: ['Car Wash', 'Smog Check', 'Repair'],
12 | 'Car Wash': [],
13 | 'Smog Check': [],
14 | 'Repair': [],
15 | chosen: ''
16 | }
17 | this.handleChange = this.handleChange.bind(this)
18 | this.addCar = this.addCar.bind(this)
19 | }
20 |
21 | //setState triggers a re-render of the component and does not immediately mutate the data.
22 | addCar(e) {
23 | e.preventDefault()
24 | const { chosen } = this.state
25 | //in ES2015 you can set keys as shown below with [chosen]
26 | this.setState({ [chosen]: this.state[chosen].concat([chosen])})
27 | }
28 |
29 | handleChange(e, value) {
30 | this.setState({chosen: value})
31 | }
32 |
33 | //onSubmit we are running a function that grabs the event and passes it to handleChange(). The event is represented by 'e'.
34 | render() {
35 | return (
36 |
50 | )
51 | }
52 | }
53 |
54 | //We're literally mapping the state, which we receive from the store, to the props of this component.
55 | //The object that we return from this function will be available on this.props, i.e. this.props.services,
56 | //this.props.chosen, etc.
57 | function mapStateToProps(state) {
58 | const services = state.services
59 | const chosen = state.chosen
60 | if (services.length) {
61 | let serviceOptions
62 | services.forEach(service => {
63 | serviceOptions = Object.assign({}, serviceOptions, {
64 | [service]: state[service],
65 | services,
66 | chosen
67 | })
68 | })
69 | return serviceOptions
70 | } else {
71 | return {
72 | services,
73 | chosen
74 | }
75 | }
76 | }
77 |
78 | //Connect is a method from react-redux. This is what connects our different components to the store so
79 | //that they can see the new state that is returned.
80 | export default connect(mapStateToProps)(CarShop)
81 |
82 |
83 |
--------------------------------------------------------------------------------
/examples/redux-example/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "dependencies": {
3 | "isomorphic-fetch": "^2.2.1",
4 | "my1stnpmpackage": "^1.0.2",
5 | "react": "^0.14.7",
6 | "react-dom": "^0.14.7",
7 | "react-redux": "^4.4.1",
8 | "redux": "^3.3.1",
9 | "redux-thunk": "^2.1.0",
10 | "webpack": "^1.9.11"
11 | },
12 | "devDependencies": {
13 | "babel-core": "^6.3.15",
14 | "babel-loader": "^6.2.0",
15 | "babel-preset-es2015": "^6.3.13",
16 | "babel-preset-react": "^6.3.13",
17 | "express": "^4.13.3",
18 | "http-proxy": "^1.13.2",
19 | "react-hot-loader": "^1.3.0",
20 | "webpack-dev-middleware": "^1.2.0",
21 | "webpack-dev-server": "^1.14.1",
22 | "webpack-hot-middleware": "^2.9.1"
23 | }
24 | }
25 |
--------------------------------------------------------------------------------
/examples/redux-example/public/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
--------------------------------------------------------------------------------
/examples/redux-example/reducerCars.js:
--------------------------------------------------------------------------------
1 | //We import the different action types from our actions file
2 | import { HANDLE_CHOSEN, RECEIVE_SERVICES, ADD_CAR } from './actions'
3 |
4 | //On its first pass, Redux passes 'undefined' and an empty object into all the reducers that handle the store. In ES2015 if you pass undefined into a function, as Redux does, you can set that argument to something, in this case an object with two keys: services & chosen.
5 |
6 | export default function cars(state = {
7 | services: [],
8 | chosen: ''
9 | }, action) {
10 | switch(action.type) {
11 | case HANDLE_CHOSEN:
12 | return Object.assign({}, state, {
13 | chosen: action.service
14 | })
15 | case ADD_CAR:
16 | return Object.assign({}, state, {
17 | [state.chosen]: state[state.chosen].concat([state.chosen])
18 | })
19 | case RECEIVE_SERVICES:
20 | action.services.forEach(service => {
21 | state[service] = []
22 | })
23 | return Object.assign({}, state, {
24 | services: action.services
25 | })
26 | default:
27 | return state
28 | }
29 | }
30 |
31 | //Redux and React rely on immutable data. As I understand it, they do a comparison of the old state and the new state and check to see what is different. If you directly mutate the old state, there is nothing to compare.
32 |
33 | //Object.assign() merges object(s) to an empty object. By returning this new object we have not mutated the state.
--------------------------------------------------------------------------------
/examples/redux-example/root.js:
--------------------------------------------------------------------------------
1 | import React from 'react'
2 | import { render } from 'react-dom'
3 | import { createStore, applyMiddleware } from 'redux'
4 | import { Provider } from 'react-redux'
5 | import CarShop from './carshop'
6 | import cars from './reducerCars'
7 | import thunk from 'redux-thunk'
8 | import 'isomorphic-fetch'
9 |
10 |
11 | //Use createStore to create your redux store
12 | //In this case since we only have one reducer we pass it in. If you have multiple reducers,
13 | //use combineReducers instead.
14 | let store = createStore(cars, applyMiddleware(thunk))
15 |
16 | //The Provider tag passes the store down implicitly via context. This puts the dispatch method of the store
17 | //on the props of all the elements connected to the component, in this case CarShop, that it wraps.
18 | render(
19 |
20 |
21 | , document.getElementById('app')
22 | )
--------------------------------------------------------------------------------
/examples/redux-example/selection.js:
--------------------------------------------------------------------------------
1 | import React from 'react'
2 |
3 | //See the carlist file for more information about what is happening here. The only unique thing is that
4 | //we have passed a method from the parent component down (handleChange).
5 | const Service = ({service, handleChange, chosen}) => (
6 |
7 | handleChange(e, service)} /> {service}
8 |
9 | )
10 |
11 | export default Service
12 |
--------------------------------------------------------------------------------
/examples/redux-example/server.js:
--------------------------------------------------------------------------------
1 | var express = require('express');
2 | var path = require('path');
3 | var httpProxy = require('http-proxy');
4 | var publicPath = path.resolve(__dirname, 'public');
5 |
6 | var port = 3000;
7 |
8 | // We need to add a configuration to our proxy server,
9 | // as we are now proxying outside localhost
10 | var proxy = httpProxy.createProxyServer({
11 | changeOrigin: true
12 | });
13 |
14 | var app = express();
15 |
16 | //serving our index.html
17 | app.use(express.static(publicPath));
18 |
19 | //server/compiler.js runs webpack-dev-server which creates the bundle.js which index.html serves.
20 | //The compiler adds some console logs for some extra sugar.
21 | //Notice that you will not see a physical bundle.js because webpack-dev-server runs it from memory.
22 | var bundle = require('./server/compiler.js')
23 | bundle()
24 |
25 | //Express now proxies all requests to localhost:8080/build/*
26 | //app.all is a special routing method used for loading middleware functions
27 | app.all('/build/*', function (req, res) {
28 | proxy.web(req, res, {
29 | target: 'http://localhost:8080'
30 | })
31 | })
32 |
33 | proxy.on('error', function(e) {
34 | console.log('Could not connect to proxy, please try again...')
35 | });
36 |
37 | app.listen(port, function () {
38 | console.log('Server running on port ' + port)
39 | });
--------------------------------------------------------------------------------
/examples/redux-example/server/compiler.js:
--------------------------------------------------------------------------------
1 | var Webpack = require('webpack');
2 | var WebpackDevServer = require('webpack-dev-server');
3 | var WebpackConfig = require('./../webpack.config.js');
4 | var path = require('path');
5 | var fs = require('fs');
6 |
7 | module.exports = function () {
8 | // First we fire up Webpack an pass in the configuration we
9 | // created
10 | var bundleStart = null;
11 | var compiler = Webpack(WebpackConfig);
12 |
13 | // We give notice in the terminal when it starts bundling and
14 | // set the time it started
15 | compiler.plugin('compile', function() {
16 | console.log('Bundling...');
17 | bundleStart = Date.now();
18 | });
19 |
20 | // We also give notice when it is done compiling, including the
21 | // time it took. Nice to have
22 | compiler.plugin('done', function() {
23 | console.log('Bundled in ' + (Date.now() - bundleStart) + 'ms!');
24 | });
25 |
26 | var bundler = new WebpackDevServer(compiler, {
27 | // We need to tell Webpack to serve our bundled application
28 | // from the build path. When proxying:
29 | // http://localhost:3000/build -> http://localhost:8080/build
30 | publicPath: '/build/',
31 |
32 | // Configure hot replacement
33 | hot: true,
34 | quiet: false,
35 | noInfo: true,
36 | stats: {
37 | colors: true
38 | }
39 | });
40 |
41 | // We fire up the development server and give notice in the terminal
42 | // that we are starting the initial bundle
43 | bundler.listen(8080, function () {
44 | console.log('Bundling project, please wait...');
45 | });
46 |
47 | };
--------------------------------------------------------------------------------
/examples/redux-example/webpack.config.js:
--------------------------------------------------------------------------------
1 | var path = require('path')
2 | var webpack =require('webpack')
3 |
4 | module.exports = {
5 |
6 | //fastest rebuild and build speed
7 | devtool: 'eval',
8 | entry: [
9 | //for hot style updates
10 | 'webpack/hot/dev-server',
11 | //refreshes the browser when it can't hot update
12 | 'webpack-dev-server/client?http://localhost:8080',
13 | //our entry point
14 | './root.js'
15 | ],
16 | output: {
17 | path: path.join(__dirname, 'public', 'build'),
18 | filename: 'bundle.js',
19 | publicPath: '/build/' //the server will listen in on this path and then proxy Webpack
20 | },
21 |
22 | module: {
23 | loaders: [
24 | {
25 | test: /\.js$/,
26 | loader: 'babel-loader',
27 | query: {
28 | presets: ['es2015', 'react']
29 | },
30 | exclude: '/node_modules'
31 | },
32 | //This converts our .css into JS
33 | {
34 | test: /\.css$/,
35 | loader: 'css-loader'
36 | }
37 | ]
38 | },
39 | //Since we're running Webpack from our server, need to manually add the
40 | //Hot Replacement plugin
41 | plugins: [
42 | new webpack.HotModuleReplacementPlugin(),
43 | ]
44 | };
--------------------------------------------------------------------------------
/full-boilerplate/.gitignore:
--------------------------------------------------------------------------------
1 | node_modules
2 | .DS_Store
3 |
--------------------------------------------------------------------------------
/full-boilerplate/actions/actions.js:
--------------------------------------------------------------------------------
1 | export const GET_HELLO_WORLD = 'GET_HELLO_WORLD',
2 | GET_GOODBYE_WORLD = 'GET_GOODBYE_WORLD'
3 |
4 | export function getHelloWorld() {
5 | return {
6 | type: GET_HELLO_WORLD,
7 | }
8 | }
9 |
10 | export function getGoodbyeWorld() {
11 | return {
12 | type: GET_GOODBYE_WORLD,
13 | }
14 | }
--------------------------------------------------------------------------------
/full-boilerplate/compiler/compiler.js:
--------------------------------------------------------------------------------
1 | var Webpack = require('webpack');
2 | var WebpackDevServer = require('webpack-dev-server');
3 | var WebpackConfig = require('./../webpack.config.js');
4 | var path = require('path');
5 | var fs = require('fs');
6 |
7 | module.exports = function () {
8 | // First we fire up Webpack an pass in the configuration we
9 | // created
10 | var bundleStart = null;
11 | var compiler = Webpack(WebpackConfig);
12 |
13 | // We give notice in the terminal when it starts bundling and
14 | // set the time it started
15 | compiler.plugin('compile', function() {
16 | console.log('Bundling...');
17 | bundleStart = Date.now();
18 | });
19 |
20 | // We also give notice when it is done compiling, including the
21 | // time it took. Nice to have
22 | compiler.plugin('done', function() {
23 | console.log('Bundled in ' + (Date.now() - bundleStart) + 'ms!');
24 | });
25 |
26 | var bundler = new WebpackDevServer(compiler, {
27 | // We need to tell Webpack to serve our bundled application
28 | // from the build path. When proxying:
29 | // http://localhost:3000/build -> http://localhost:8080/build
30 | publicPath: '/build/',
31 |
32 | // Configure hot replacement
33 | hot: true,
34 | quiet: false,
35 | noInfo: true,
36 | stats: {
37 | colors: true
38 | }
39 | });
40 |
41 | // We fire up the development server and give notice in the terminal
42 | // that we are starting the initial bundle
43 | bundler.listen(8080, function () {
44 | console.log('Bundling project, please wait...');
45 | });
46 |
47 | };
--------------------------------------------------------------------------------
/full-boilerplate/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "engines": {
3 | "node": "4.4.0"
4 | },
5 | "main": "server.js",
6 | "scripts": {
7 | "start": "node server.js",
8 | "postinstall": "NODE_ENV=production webpack --config ./webpack.production.config.js --progress --colors"
9 | },
10 | "dependencies": {
11 | "react": "^0.14.7",
12 | "react-dom": "^0.14.7",
13 | "react-redux": "^4.4.1",
14 | "react-router": "^2.4.1",
15 | "redux": "^3.3.1",
16 | "redux-thunk": "^2.1.0",
17 | "webpack": "^1.9.11"
18 | },
19 | "devDependencies": {
20 | "babel-core": "^6.3.15",
21 | "babel-loader": "^6.2.0",
22 | "babel-preset-es2015": "^6.3.13",
23 | "babel-preset-react": "^6.3.13",
24 | "express": "^4.13.3",
25 | "http-proxy": "^1.13.2",
26 | "react-hot-loader": "^1.3.0",
27 | "webpack-dev-middleware": "^1.2.0",
28 | "webpack-dev-server": "^1.14.1",
29 | "webpack-hot-middleware": "^2.9.1"
30 | }
31 | }
32 |
--------------------------------------------------------------------------------
/full-boilerplate/public/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
--------------------------------------------------------------------------------
/full-boilerplate/reducers/reducerGreeting.js:
--------------------------------------------------------------------------------
1 | import { GET_HELLO_WORLD, GET_GOODBYE_WORLD } from '../actions/actions'
2 |
3 | export function greeting(state = {
4 | hello: '',
5 | goodbye: ''
6 | }, action) {
7 | switch(action.type) {
8 | case GET_HELLO_WORLD:
9 | return Object.assign({}, state, {
10 | hello: 'Hello World',
11 | })
12 | case GET_GOODBYE_WORLD:
13 | return Object.assign({}, state, {
14 | goodbye: 'Goodbye World',
15 | })
16 | default:
17 | return state
18 | }
19 | }
20 |
--------------------------------------------------------------------------------
/full-boilerplate/reducers/reducers.js:
--------------------------------------------------------------------------------
1 | import { combineReducers } from 'redux'
2 | import { greeting } from './reducerGreeting'
3 |
4 | export default combineReducers({
5 | greeting
6 | })
--------------------------------------------------------------------------------
/full-boilerplate/server.js:
--------------------------------------------------------------------------------
1 | var express = require('express');
2 | var path = require('path');
3 | var httpProxy = require('http-proxy');
4 | var publicPath = path.resolve(__dirname, 'public');
5 |
6 | // We need to add a configuration to our proxy server,
7 | // as we are now proxying outside localhost
8 | var isProduction = process.env.NODE_ENV === 'production';
9 | var port = isProduction ? process.env.PORT : 3000;
10 |
11 | var proxy = httpProxy.createProxyServer({
12 | changeOrigin: true
13 | });
14 | var app = express();
15 |
16 | app.use(express.static(publicPath));
17 |
18 | // If you only want this for development, you would of course
19 | // put it in the "if" block below
20 |
21 | if (!isProduction) {
22 | var bundle = require('./compiler/compiler.js')
23 | bundle()
24 | app.all('/build/*', function (req, res) {
25 | proxy.web(req, res, {
26 | target: 'http://localhost:8080'
27 | })
28 | })
29 | };
30 |
31 | proxy.on('error', function(e) {
32 | console.log('Could not connect to proxy, please try again...')
33 | });
34 |
35 | app.listen(port, function () {
36 | console.log('Server running on port ' + port)
37 | });
38 |
39 |
--------------------------------------------------------------------------------
/full-boilerplate/src/components/App.js:
--------------------------------------------------------------------------------
1 | import React, { Component } from 'react'
2 | import { render } from 'react-dom'
3 | import { Link } from 'react-router'
4 |
5 | import GoodbyeWorld from './GoodbyeWorld'
6 | import HelloWorld from './HelloWorld'
7 |
8 | export default class App extends Component {
9 | render() {
10 | return (
11 |