├── .babelrc ├── .eslintignore ├── .eslintrc.json ├── .gitignore ├── .npmignore ├── LICENSE ├── README.MD ├── client ├── res │ ├── images │ │ └── logo.png │ └── scss │ │ ├── home.scss │ │ ├── main.scss │ │ ├── palette.scss │ │ └── view_item.scss └── src │ ├── actions │ └── list_actions.js │ ├── app.js │ ├── components │ ├── header.js │ ├── home.js │ ├── list_item_preview.js │ ├── list_item_view.js │ └── list_items.js │ ├── consts │ ├── action_types.js │ ├── default_state.js │ └── list_items.js │ ├── containers │ ├── list_item_preview.js │ ├── list_item_view.js │ └── list_items.js │ ├── index.js │ └── reducers │ ├── index.js │ └── list.js ├── package-lock.json ├── package.json ├── server ├── routes │ ├── index.js │ └── ssr.js └── views │ ├── index.dev.ejs │ └── index.ejs └── webpack ├── client.dev.js ├── client.prod.js └── server.prod.js /.babelrc: -------------------------------------------------------------------------------- 1 | { 2 | "presets": ["@babel/react","@babel/preset-env"] 3 | } 4 | -------------------------------------------------------------------------------- /.eslintignore: -------------------------------------------------------------------------------- 1 | **/webpack/* 2 | node_modules 3 | **/public/* 4 | **/bin/* -------------------------------------------------------------------------------- /.eslintrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "airbnb", 3 | "rules": { 4 | "react/jsx-filename-extension": [1, { 5 | "extensions": [".js", ".jsx"] 6 | }], 7 | "react/forbid-prop-types": 0, 8 | "function-paren-newline": ["error", "consistent"], 9 | "jsx-a11y/anchor-is-valid": [ "error", { 10 | "components": [ "Link" ], 11 | "specialLink": [ "to" ] 12 | }] 13 | }, 14 | "env": { 15 | "es6": true, 16 | "node": true, 17 | "browser": true 18 | } 19 | } -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | 3 | **/bin 4 | **/public -------------------------------------------------------------------------------- /.npmignore: -------------------------------------------------------------------------------- 1 | client 2 | webpack 3 | .babelrc 4 | .eslintrc.json 5 | package-lock.json 6 | server/routes -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright 2017 Tahnik Mustasin 2 | 3 | Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: 4 | 5 | The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. 6 | 7 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. -------------------------------------------------------------------------------- /README.MD: -------------------------------------------------------------------------------- 1 | # Behold the ReactJS + ExpressJS Boilerplate 2 | 3 | I wrote an [article] about this repo which explains the structure. 4 | 5 | This is a boilerplate to use ReactJS, ExpressJS, Redux and React Router v4 in a project. 6 | 7 | - [ReactJS] - The state based framework for your Views 8 | - [React Router v4] - For routing to different paths 9 | - [Redux] - Redux manages your state 10 | - [Babel] - The compiler to compile your JS files with es6, es7, JSX syntax to regular javascript 11 | - [Webpack] - The module binder which takes all your JS files from different directories and compiles them into a single app.bundle.js (you can change the filename of course) so you can include it in a HTML page 12 | - [ExpressJS] - The node framework to serve your views to the world when they hit the server at example.com or example.com/awesome.html 13 | 14 | 15 | # Installation 16 | 17 | Node Version: v9.0.0^ 18 | 19 | Just clone this repo or download the zip file. `cd` into the directory and run 20 | 21 | npm install 22 | 23 | ## Developing App with [Hot Reload] 24 | To develop your own react application, you can take advantage of React Hot Loader and Webpack Dev Server. To develop app with hot reload: 25 | 26 | npm run dev 27 | 28 | Now you can access your react application on http://localhost:8080 29 | 30 | ## Production build and Deploy 31 | To make a production build of your project, run the following commands 32 | 33 | npm run build 34 | 35 | This will create create two files: `index.js` in `server/public/js` and `server.js` in `server/bin`. 36 | 37 | `server.js` will be used for serving the application on port 3000 and `index.js` is the actual react app itself. 38 | 39 | Finally run 40 | 41 | npm start 42 | 43 | The you will be able to access this app from http://localhost:3000. 44 | 45 | To get a distributable tarball of your application, run this command 46 | 47 | npm pack 48 | 49 | Remember that you have to run `npm run build` before doing this. This will create a tar.gz file in your root folder. The contents in this file is deployable. All you need to do is copy the contents inside package folder inside this tar.gz file to your server and run the app with something like [pm2]. 50 | 51 | 52 | [ReactJS]: 53 | [Babel]: 54 | [Webpack]: 55 | [React Router v4]: 56 | [Hot Reload]: 57 | [ExpressJS]: 58 | [Redux]: 59 | [pm2]: 60 | [article]: 61 | -------------------------------------------------------------------------------- /client/res/images/logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tahnik/react-expressjs/663d05e626ee86f0ebfe71df76e28991623abf87/client/res/images/logo.png -------------------------------------------------------------------------------- /client/res/scss/home.scss: -------------------------------------------------------------------------------- 1 | .home { 2 | display: flex; 3 | .list_items { 4 | flex: 1; 5 | padding: 1rem; 6 | } 7 | .preview { 8 | flex: 2; 9 | padding: 1rem; 10 | display: flex; 11 | flex-direction: column; 12 | align-items: center; 13 | justify-content: center; 14 | h2 { 15 | text-transform: capitalize; 16 | margin-top: 0; 17 | } 18 | p { 19 | text-align: center; 20 | } 21 | button { 22 | font-size: 1rem; 23 | } 24 | } 25 | ul { 26 | list-style-type: none; 27 | padding: 0; 28 | li { 29 | margin: 0.5rem 0; 30 | button { 31 | width: 100%; 32 | font-size: 1.5rem; 33 | transition: all 0.3s; 34 | text-transform: capitalize; 35 | &:hover { 36 | cursor: pointer; 37 | } 38 | } 39 | } 40 | } 41 | } -------------------------------------------------------------------------------- /client/res/scss/main.scss: -------------------------------------------------------------------------------- 1 | @import './palette.scss'; 2 | @import './home.scss'; 3 | @import './view_item.scss'; 4 | 5 | @import url('https://fonts.googleapis.com/css?family=Raleway:400,600|Roboto:100,300'); 6 | 7 | .reactbody { 8 | background-color: $color5; 9 | } 10 | 11 | h1, h2, h3, h4, h5, h6 { 12 | font-family: 'Roboto', sans-serif; 13 | } 14 | 15 | h1 { 16 | font-weight: 300; 17 | } 18 | 19 | button { 20 | font-family: 'Roboto', sans-serif; 21 | background-color: $color1; 22 | font-weight: 300; 23 | border: none; 24 | outline:none; 25 | padding: 0.5rem 1rem; 26 | border-radius: 3px; 27 | color: white; 28 | transition: all 0.3s; 29 | &:hover { 30 | cursor: pointer; 31 | background-color: $color2; 32 | } 33 | } 34 | 35 | div, p, span { 36 | font-family: 'Raleway', sans-serif; 37 | } 38 | 39 | .header { 40 | display: flex; 41 | flex-direction: column; 42 | align-items: center; 43 | h1 { 44 | text-align: center; 45 | } 46 | border-bottom: 2px solid gray; 47 | } 48 | -------------------------------------------------------------------------------- /client/res/scss/palette.scss: -------------------------------------------------------------------------------- 1 | /* Coolors Exported Palette - coolors.co/03b5aa-037971-023436-00bfb3-049a8f */ 2 | 3 | /* HSL */ 4 | $color1: hsla(176%, 97%, 36%, 1); 5 | $color2: hsla(176%, 95%, 24%, 1); 6 | $color3: hsla(182%, 93%, 11%, 1); 7 | $color4: hsla(176%, 100%, 37%, 1); 8 | $color5: hsla(176%, 95%, 31%, 1); 9 | 10 | /* RGB */ 11 | $color1: rgba(3, 181, 170, 1); 12 | $color2: rgba(3, 121, 113, 1); 13 | $color3: rgba(2, 52, 54, 1); 14 | $color4: rgba(0, 191, 179, 1); 15 | $color5: rgba(4, 154, 143, 1); -------------------------------------------------------------------------------- /client/res/scss/view_item.scss: -------------------------------------------------------------------------------- 1 | .view_item { 2 | display: flex; 3 | flex-direction: column; 4 | align-items: center; 5 | padding: 2rem; 6 | h2 { 7 | margin-top: 3rem; 8 | text-transform: capitalize; 9 | } 10 | p { 11 | text-align: center; 12 | } 13 | button { 14 | font-size: 1rem; 15 | } 16 | } -------------------------------------------------------------------------------- /client/src/actions/list_actions.js: -------------------------------------------------------------------------------- 1 | import { LIST_ACTIONS } from '../consts/action_types'; 2 | 3 | export const previewItem = name => ({ 4 | type: LIST_ACTIONS.ITEM_PREVIEW, 5 | name, // shorthand for name: name 6 | }); 7 | 8 | export const viewItem = name => ({ 9 | type: LIST_ACTIONS.ITEM_VIEW, 10 | name, 11 | }); 12 | 13 | export const addItem = item => ({ 14 | type: LIST_ACTIONS.ITEM_ADD, 15 | item, // shorthand for item: item 16 | }); 17 | 18 | export const clearItem = () => ({ 19 | type: LIST_ACTIONS.ITEM_CLEAR, 20 | }); 21 | -------------------------------------------------------------------------------- /client/src/app.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { Route } from 'react-router-dom'; 3 | import Header from './components/header'; 4 | import Home from './components/home'; 5 | import ItemView from './containers/list_item_view'; 6 | 7 | const App = () => ( 8 |
9 |
10 | 11 | 12 |
13 | ); 14 | 15 | export default App; 16 | -------------------------------------------------------------------------------- /client/src/components/header.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import logo from '../../res/images/logo.png'; 3 | 4 | const Header = () => ( 5 |
6 |
7 | react logo 8 |
9 |

React Redux Router

10 |
11 | ); 12 | 13 | export default Header; 14 | -------------------------------------------------------------------------------- /client/src/components/home.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import ListItems from '../containers/list_items'; 3 | import ListItemPreview from '../containers/list_item_preview'; 4 | 5 | const Home = () => ( 6 |
7 | 8 | 9 |
10 | ); 11 | 12 | export default Home; 13 | -------------------------------------------------------------------------------- /client/src/components/list_item_preview.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import PropTypes from 'prop-types'; 3 | import { Link } from 'react-router-dom'; 4 | 5 | const ListItemPreview = ({ item }) => { 6 | if (!item) { 7 | return ( 8 |
9 |

Select an item

10 |

Description will appear here

11 |
12 | ); 13 | } 14 | return ( 15 |
16 |

{ item.name }

17 |

{ item.description }

18 | 19 | 20 | 21 |
22 | ); 23 | }; 24 | 25 | ListItemPreview.propTypes = { 26 | item: PropTypes.object, 27 | }; 28 | 29 | ListItemPreview.defaultProps = { 30 | item: null, 31 | }; 32 | 33 | export default ListItemPreview; 34 | -------------------------------------------------------------------------------- /client/src/components/list_item_view.js: -------------------------------------------------------------------------------- 1 | import React, { Component } from 'react'; 2 | import PropTypes from 'prop-types'; 3 | import { Link } from 'react-router-dom'; 4 | 5 | class ListItemView extends Component { 6 | componentDidMount() { 7 | const { viewItem, match } = this.props; 8 | viewItem(match.params.name); 9 | } 10 | render() { 11 | const { item } = this.props; 12 | if (!item) { 13 | return (
Loading...
); 14 | } 15 | 16 | return ( 17 |
18 | 19 | 20 | 21 |

{ item.name }

22 |

{ item.description }

23 |
24 | ); 25 | } 26 | } 27 | 28 | ListItemView.propTypes = { 29 | viewItem: PropTypes.func.isRequired, 30 | match: PropTypes.object.isRequired, 31 | item: PropTypes.object, 32 | }; 33 | 34 | ListItemView.defaultProps = { 35 | item: null, 36 | }; 37 | 38 | export default ListItemView; 39 | -------------------------------------------------------------------------------- /client/src/components/list_items.js: -------------------------------------------------------------------------------- 1 | import React, { Component } from 'react'; 2 | import PropTypes from 'prop-types'; 3 | 4 | class ListView extends Component { 5 | renderList() { 6 | const { listItems, previewItem } = this.props; 7 | return Object.keys(listItems).map((key) => { 8 | const item = listItems[key]; 9 | return ( 10 |
  • 13 | 14 |
  • 15 | ); 16 | }); 17 | } 18 | render() { 19 | return ( 20 |
    21 |
      22 | { this.renderList() } 23 |
    24 |
    25 | ); 26 | } 27 | } 28 | 29 | ListView.propTypes = { 30 | listItems: PropTypes.object.isRequired, 31 | previewItem: PropTypes.func.isRequired, 32 | }; 33 | 34 | export default ListView; 35 | -------------------------------------------------------------------------------- /client/src/consts/action_types.js: -------------------------------------------------------------------------------- 1 | // eslint-disable-next-line 2 | export const LIST_ACTIONS = { 3 | ITEM_PREVIEW: 'ITEM_PREVIEW', 4 | ITEM_VIEW: 'ITEM_VIEW', 5 | ITEM_ADD: 'ITEM_ADD', 6 | ITEM_CLEAR: 'ITEM_CLEAR', 7 | }; 8 | -------------------------------------------------------------------------------- /client/src/consts/default_state.js: -------------------------------------------------------------------------------- 1 | import LIST_ITEMS from './list_items'; 2 | 3 | // eslint-disable-next-line 4 | export const LISTS = { items: LIST_ITEMS, itemPreview: null, itemView: null }; -------------------------------------------------------------------------------- /client/src/consts/list_items.js: -------------------------------------------------------------------------------- 1 | export default { 2 | ACTIONS: { 3 | name: 'actions', 4 | description: 'Actions are payloads of information that send data from your application to your store. They are the only source of information for the store.', 5 | }, 6 | STORE: { 7 | name: 'store', 8 | description: 'The state of your whole application is stored in an object tree within a single store.', 9 | }, 10 | REDUCERS: { 11 | name: 'reducers', 12 | description: 'Actions describe the fact that something happened, but don\'t specify how the application\'s state changes in response. This is the job of reducers.', 13 | }, 14 | }; 15 | -------------------------------------------------------------------------------- /client/src/containers/list_item_preview.js: -------------------------------------------------------------------------------- 1 | import { connect } from 'react-redux'; 2 | import ListItemPreview from '../components/list_item_preview'; 3 | 4 | /* 5 | This is a redux specific function. 6 | What is does is: It gets the state specified in here from the global redux state. 7 | For example, here we are retrieving the list of items from the redux store. 8 | Whenever this list changes, any component that is using this list of item will re-render. 9 | */ 10 | function mapStateToProps(state) { 11 | return { 12 | item: state.list.itemPreview, 13 | }; 14 | } 15 | 16 | /* 17 | Here we are creating a Higher order component 18 | https://facebook.github.io/react/docs/higher-order-components.html 19 | */ 20 | export default connect(mapStateToProps)(ListItemPreview); 21 | -------------------------------------------------------------------------------- /client/src/containers/list_item_view.js: -------------------------------------------------------------------------------- 1 | import { connect } from 'react-redux'; 2 | import { viewItem } from '../actions/list_actions'; 3 | import ItemView from '../components/list_item_view'; 4 | 5 | const mapStateToProps = state => ({ 6 | item: state.list.itemView, 7 | }); 8 | 9 | const mapDispatchToProps = dispatch => ({ 10 | viewItem: (name) => { 11 | dispatch(viewItem(name)); 12 | }, 13 | }); 14 | 15 | /* 16 | Here we are creating a Higher order component 17 | https://facebook.github.io/react/docs/higher-order-components.html 18 | */ 19 | export default connect(mapStateToProps, mapDispatchToProps)(ItemView); 20 | -------------------------------------------------------------------------------- /client/src/containers/list_items.js: -------------------------------------------------------------------------------- 1 | import { connect } from 'react-redux'; 2 | import { previewItem } from '../actions/list_actions'; 3 | import ListItems from '../components/list_items'; 4 | 5 | /* 6 | This is a redux specific function. 7 | What is does is: It gets the state specified in here from the global redux state. 8 | For example, here we are retrieving the list of items from the redux store. 9 | Whenever this list changes, any component that is using this list of item will re-render. 10 | */ 11 | const mapStateToProps = state => ({ 12 | listItems: state.list.items, 13 | }); 14 | 15 | /* 16 | This is a redux specific function. 17 | http://redux.js.org/docs/api/bindActionCreators.html 18 | */ 19 | const mapDispatchToProps = dispatch => ({ 20 | previewItem: (name) => { 21 | dispatch(previewItem(name)); 22 | }, 23 | }); 24 | 25 | 26 | /* 27 | Here we are creating a Higher order component 28 | https://facebook.github.io/react/docs/higher-order-components.html 29 | */ 30 | export default connect(mapStateToProps, mapDispatchToProps)(ListItems); 31 | -------------------------------------------------------------------------------- /client/src/index.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import ReactDOM from 'react-dom'; 3 | import { Provider } from 'react-redux'; 4 | import { createStore } from 'redux'; 5 | import { BrowserRouter } from 'react-router-dom'; 6 | import { AppContainer } from 'react-hot-loader'; 7 | import reducers from './reducers/index'; 8 | import App from './app'; 9 | 10 | 11 | /* 12 | Here we are getting the initial state injected by the server. See routes/index.js for more details 13 | */ 14 | const initialState = window.__INITIAL_STATE__; // eslint-disable-line 15 | 16 | const store = createStore(reducers, initialState); 17 | 18 | /* 19 | While creating a store, we will inject the initial state we received from the server to our app. 20 | */ 21 | const render = (Component) => { 22 | ReactDOM.render( 23 | 24 | 25 | 26 | 27 | 28 | 29 | , 30 | document.getElementById('reactbody'), 31 | ); 32 | }; 33 | 34 | render(App); 35 | 36 | if (module.hot) { 37 | module.hot.accept('./app', () => { 38 | // eslint-disable-next-line 39 | const nextApp = require('./app').default; 40 | render(nextApp); 41 | }); 42 | } 43 | 44 | // module.hot.accept('./reducers', () => { 45 | // // eslint-disable-next-line 46 | // const nextRootReducer = require('./reducers/index'); 47 | // store.replaceReducer(nextRootReducer); 48 | // }); 49 | -------------------------------------------------------------------------------- /client/src/reducers/index.js: -------------------------------------------------------------------------------- 1 | import { combineReducers } from 'redux'; 2 | import list from './list'; 3 | 4 | const rootReducer = combineReducers({ 5 | list, // shorthand for lists: lists 6 | }); 7 | 8 | export default rootReducer; 9 | -------------------------------------------------------------------------------- /client/src/reducers/list.js: -------------------------------------------------------------------------------- 1 | import { LIST_ACTIONS } from '../consts/action_types'; 2 | import { LISTS } from '../consts/default_state'; 3 | 4 | export default (state = LISTS, action) => { 5 | switch (action.type) { 6 | case LIST_ACTIONS.ITEM_PREVIEW: 7 | return { ...state, itemPreview: state.items[action.name.toUpperCase()] }; 8 | case LIST_ACTIONS.ITEM_VIEW: 9 | return { ...state, itemView: state.items[action.name.toUpperCase()] }; 10 | case LIST_ACTIONS.ITEM_CLEAR: 11 | return { ...state, itemView: null }; 12 | case LIST_ACTIONS.ITEM_ADD: { 13 | const nextItems = { ...state.items }; 14 | const itemToAdd = action.item; 15 | nextItems[itemToAdd.name.toUpperCase()] = itemToAdd; 16 | const returnVal = { ...state, items: nextItems }; 17 | return returnVal; 18 | } 19 | default: 20 | return state; 21 | } 22 | }; 23 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "react-expressjs", 3 | "version": "4.0.0", 4 | "description": "Boilerplate for ReactJS project with ExpressJS server", 5 | "main": "server/bin/server.js", 6 | "scripts": { 7 | "dev": "webpack-dev-server --config webpack/client.dev.js", 8 | "build": "npm run build:client && npm run build:server", 9 | "build:server": "cross-env NODE_ENV=production webpack --config webpack/server.prod.js --progress", 10 | "build:client": "cross-env NODE_ENV=production webpack --config webpack/client.prod.js --progress", 11 | "start": "cd server && node bin/server.js", 12 | "lint": "eslint --ext .jsx,.js ./" 13 | }, 14 | "repository": { 15 | "type": "git", 16 | "url": "git+https://github.com/tahnik/react-express-webpack-babel" 17 | }, 18 | "keywords": [ 19 | "react", 20 | "express", 21 | "webpack", 22 | "babel", 23 | "expressjs", 24 | "react router v4", 25 | "redux" 26 | ], 27 | "author": "Tahnik Mustasin", 28 | "license": "MIT", 29 | "bugs": { 30 | "url": "https://github.com/tahnik/react-express-webpack-babel/issues" 31 | }, 32 | "homepage": "https://github.com/tahnik/react-express-webpack-babel#readme", 33 | "dependencies": { 34 | "cross-env": "^5.2.0", 35 | "ejs": "^2.6.1", 36 | "express": "^4.17.1", 37 | "npm": "^6.9.0", 38 | "npm-upgrade": "^1.4.1", 39 | "prop-types": "^15.7.2", 40 | "react": "^16.8.6", 41 | "react-dom": "^16.8.6", 42 | "react-hot-loader": "^4.9.0", 43 | "react-redux": "^7.0.3", 44 | "react-router": "^5.0.1", 45 | "react-router-dom": "^5.0.1", 46 | "redux": "^4.0.1" 47 | }, 48 | "devDependencies": { 49 | "@babel/core": "^7.4.5", 50 | "@babel/preset-env": "^7.4.5", 51 | "@babel/preset-react": "^7.0.0", 52 | "babel-loader": "^8.0.6", 53 | "babel-preset-es2015": "^6.24.1", 54 | "babel-preset-stage-1": "^6.24.1", 55 | "css-loader": "^2.1.1", 56 | "eslint": "^5.16.0", 57 | "eslint-config-airbnb": "^17.1.0", 58 | "eslint-plugin-import": "^2.17.3", 59 | "eslint-plugin-jsx-a11y": "^6.2.1", 60 | "eslint-plugin-react": "^7.13.0", 61 | "extract-text-webpack-plugin": "^4.0.0-beta.0", 62 | "file-loader": "^4.0.0", 63 | "html-webpack-plugin": "^3.2.0", 64 | "node-sass": "^4.12.0", 65 | "sass-loader": "^7.1.0", 66 | "style-loader": "^0.23.1", 67 | "uglifyjs-webpack-plugin": "^2.1.3", 68 | "webpack": "^4.33.0", 69 | "webpack-cli": "^3.3.2", 70 | "webpack-dev-server": "^3.7.0", 71 | "webpack-node-externals": "^1.7.2" 72 | }, 73 | "bundledDependencies": [ 74 | "cross-env", 75 | "ejs", 76 | "express", 77 | "prop-types", 78 | "react", 79 | "react-dom", 80 | "react-redux", 81 | "react-router", 82 | "react-router-dom", 83 | "redux" 84 | ] 85 | } 86 | -------------------------------------------------------------------------------- /server/routes/index.js: -------------------------------------------------------------------------------- 1 | import express from 'express'; 2 | import ssr from './ssr'; 3 | 4 | const app = express(); 5 | 6 | app.set('view engine', 'ejs'); 7 | 8 | app.use(express.static('public')); 9 | 10 | app.use('/*', ssr); 11 | 12 | app.listen(3000, () => { 13 | console.log('Hello World listening on port 3000!'); 14 | }); 15 | -------------------------------------------------------------------------------- /server/routes/ssr.js: -------------------------------------------------------------------------------- 1 | import express from 'express'; 2 | import React from 'react'; 3 | import ReactDOMServer from 'react-dom/server'; 4 | import { createStore } from 'redux'; 5 | import { Provider } from 'react-redux'; 6 | import { StaticRouter } from 'react-router'; 7 | import reducers from '../../client/src/reducers/index'; 8 | import { LIST_ACTIONS } from '../../client/src/consts/action_types'; 9 | import App from '../../client/src/app'; 10 | 11 | const router = express.Router(); 12 | 13 | router.get('/', (req, res) => { 14 | /* 15 | http://redux.js.org/docs/recipes/ServerRendering.html 16 | */ 17 | const store = createStore(reducers); 18 | 19 | /* 20 | We can dispatch actions from server side as well. This can be very useful if you want 21 | to inject some initial data into the app. For example, if you have some articles that 22 | you have fetched from database and you want to load immediately after the user has loaded 23 | the webpage, you can do so in here. 24 | 25 | Here we are inject an list item into our app. Normally once the user has loaded the webpage 26 | we would make a request to the server and get the latest item list. But in the server we have 27 | instant connection to a database (for example, if you have a mongoDB or MySQL database 28 | installed in the server which contains all you items). 29 | So you can quickly fetch and inject it into the webpage. 30 | 31 | This will help SEO as well. If you load the webpage and make a request to the server to get 32 | all the latest items/articles, by the time Google Search Engine may not see all the updated 33 | items/articles. 34 | 35 | But if you inject the latest items/articles before it reaches the user, the Search Engine 36 | will see the item/article immediately. 37 | */ 38 | store.dispatch({ 39 | type: LIST_ACTIONS.ITEM_ADD, 40 | item: { 41 | name: 'middleware', 42 | description: `Redux middleware solves different problems than Express or Koa middleware, but in a conceptually similar way. 43 | It provides a third-party extension point between dispatching an action, and the moment it reaches the reducer.`, 44 | }, 45 | }); 46 | 47 | const context = {}; 48 | 49 | const html = ReactDOMServer.renderToString( 50 | 51 | 55 | 56 | 57 | , 58 | ); 59 | 60 | const finalState = store.getState(); 61 | 62 | if (context.url) { 63 | res.writeHead(301, { 64 | Location: context.url, 65 | }); 66 | res.end(); 67 | } else { 68 | res.status(200).render('../views/index.ejs', { 69 | html, 70 | script: JSON.stringify(finalState), 71 | }); 72 | } 73 | }); 74 | 75 | 76 | export default router; 77 | -------------------------------------------------------------------------------- /server/views/index.dev.ejs: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | React ExpressJS 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 |
    27 |
    28 | 29 | 30 | 31 | -------------------------------------------------------------------------------- /server/views/index.ejs: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | React ExpressJS 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 |
    27 |
    28 | <%- html %> 29 |
    30 |
    31 | 34 | 35 | 36 | 37 | -------------------------------------------------------------------------------- /webpack/client.dev.js: -------------------------------------------------------------------------------- 1 | const path = require('path'); 2 | const webpack = require('webpack'); 3 | var HtmlWebpackPlugin = require('html-webpack-plugin'); 4 | const ExtractTextPlugin = require('extract-text-webpack-plugin'); 5 | 6 | module.exports = { 7 | context: path.join(__dirname, '../client'), 8 | devtool: 'inline-source-map', 9 | entry: [ 10 | 'react-hot-loader/patch', 11 | 'webpack-dev-server/client?http://localhost:8080', 12 | 'webpack/hot/only-dev-server', 13 | './src/index.js', 14 | './res/scss/main.scss', 15 | ], 16 | mode: 'development', 17 | output: { 18 | path: path.join(__dirname, '../server/public'), 19 | filename: './js/index.js', 20 | publicPath: '/', 21 | }, 22 | devServer: { 23 | hot: true, 24 | publicPath: '/', 25 | historyApiFallback: true 26 | }, 27 | module: { 28 | rules: [{ 29 | test: /\.js$/, 30 | exclude: /node_modules/, 31 | use: { 32 | loader: 'babel-loader', 33 | options: { 34 | presets: ['@babel/react'], 35 | }, 36 | }, 37 | }, 38 | { 39 | test: /\.scss$/, 40 | use: ['style-loader', 'css-loader', 'sass-loader'] 41 | }, 42 | { 43 | test: /\.(png|jpg|gif)$/, 44 | use: [ 45 | { 46 | loader: 'file-loader', 47 | options: {} 48 | } 49 | ] 50 | }, 51 | ], 52 | }, 53 | plugins: [ 54 | new webpack.HotModuleReplacementPlugin(), 55 | new webpack.NamedModulesPlugin(), 56 | new HtmlWebpackPlugin({ 57 | filename: 'index.html', 58 | template: path.join(__dirname, '../server/views/index.dev.ejs'), 59 | inject: false, 60 | }), 61 | ], 62 | }; 63 | -------------------------------------------------------------------------------- /webpack/client.prod.js: -------------------------------------------------------------------------------- 1 | const path = require('path'); 2 | const webpack = require('webpack'); 3 | const UglifyJSPlugin = require('uglifyjs-webpack-plugin'); 4 | const ExtractTextPlugin = require('extract-text-webpack-plugin'); 5 | 6 | module.exports = { 7 | context: path.join(__dirname, '../client'), 8 | devtool: 'source-map', 9 | entry: [ 10 | './src/index.js', 11 | './res/scss/main.scss', 12 | ], 13 | mode: 'production', 14 | output: { 15 | path: path.join(__dirname, '../server/public'), 16 | filename: './js/index.js', 17 | publicPath: '/', 18 | }, 19 | module: { 20 | rules: [{ 21 | test: /\.js$/, 22 | exclude: /node_modules/, 23 | use: { 24 | loader: 'babel-loader', 25 | options: { 26 | presets: ['@babel/react'], 27 | }, 28 | }, 29 | }, 30 | { 31 | test: /\.scss$/, 32 | use: ExtractTextPlugin.extract({ 33 | fallback: 'style-loader', 34 | use: ['css-loader', 'sass-loader'], 35 | }), 36 | }, 37 | { 38 | test: /\.(png|jpg|gif)$/, 39 | use: [{ 40 | loader: 'file-loader', 41 | options: { 42 | outputPath: 'images/', 43 | } 44 | }] 45 | }, 46 | ], 47 | }, 48 | plugins: [ 49 | new webpack.DefinePlugin({ 50 | 'process.env': { 51 | 'NODE_ENV': JSON.stringify('production') 52 | } 53 | }), 54 | new UglifyJSPlugin({ 55 | sourceMap: true 56 | }), 57 | new ExtractTextPlugin('css/main.css'), 58 | ], 59 | }; 60 | -------------------------------------------------------------------------------- /webpack/server.prod.js: -------------------------------------------------------------------------------- 1 | const path = require('path'); 2 | const webpack = require('webpack'); 3 | const UglifyJSPlugin = require('uglifyjs-webpack-plugin'); 4 | const nodeExternals = require('webpack-node-externals'); 5 | 6 | module.exports = { 7 | context: path.join(__dirname, '../server'), 8 | devtool: 'source-map', 9 | entry: [ 10 | './routes/index.js', 11 | ], 12 | mode: 'production', 13 | target: 'node', 14 | output: { 15 | path: path.join(__dirname, '../server/bin'), 16 | filename: './server.js', 17 | }, 18 | externals: [nodeExternals()], 19 | module: { 20 | rules: [ 21 | { 22 | test: /\.js$/, 23 | exclude: /node_modules/, 24 | use: { 25 | loader: 'babel-loader', 26 | options: { 27 | presets: ['es2015', 'stage-1'], 28 | }, 29 | }, 30 | }, 31 | { 32 | test: /\.(png|jpg|gif)$/, 33 | use: [ 34 | { 35 | loader: 'file-loader', 36 | options: { 37 | outputPath: 'images/', 38 | emitFile: false, 39 | } 40 | } 41 | ] 42 | }, 43 | ], 44 | }, 45 | plugins: [ 46 | new webpack.DefinePlugin({ 47 | 'process.env': { 48 | 'NODE_ENV': JSON.stringify('production') 49 | } 50 | }), 51 | new UglifyJSPlugin({ 52 | sourceMap: true 53 | }), 54 | ] 55 | }; 56 | --------------------------------------------------------------------------------