├── .babelrc ├── .gitignore ├── README.md ├── express ├── development.js └── production.js ├── package.json ├── src ├── action │ └── types.js ├── app │ ├── App.js │ └── template.js ├── client.js ├── index.html ├── reducers │ ├── countries.js │ ├── country.js │ └── index.js └── server.js ├── webpack.config.babel.js └── webpack ├── webpack.development.config.js └── webpack.production.config.js /.babelrc: -------------------------------------------------------------------------------- 1 | { 2 | "presets": [ 3 | "@babel/preset-env", 4 | "@babel/react" 5 | ], 6 | "plugins": [ 7 | "react-hot-loader/babel", 8 | "transform-class-properties", 9 | "dynamic-import-node" 10 | ] 11 | } 12 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .idea 2 | node_modules 3 | dist 4 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # React-SSR-boilerplate 2 | 3 | a SSR poilerplate for react functional with hooks ( react version +16.8 ,jss , webpack , express ) 4 | 5 | #### features added 6 | 7 | - ssr provider supporting 8 | - hot reloading 9 | - jss support 10 | 11 | 12 | #### step 1 13 | 14 | ``` 15 | git clone https://github.com/iran-react-community/React-SSR-boilerplate.git 16 | ``` 17 | 18 | #### step 2 19 | 20 | ``` 21 | yarn install && yarn global add webpack 22 | ``` 23 | #### step 3 24 | 25 | run production environment 26 | 27 | ``` 28 | yarn build:prod && yarn start:prod 29 | ``` 30 | 31 | or you can use it as development environment 32 | 33 | ``` 34 | yarn build:dev 35 | ``` 36 | 37 | 38 | 39 | -------------------------------------------------------------------------------- /express/development.js: -------------------------------------------------------------------------------- 1 | const express = require('express'); 2 | const app = express(); 3 | const webpack = require('webpack'); 4 | const config = require('./../webpack/webpack.development.config.js'); 5 | const compiler = webpack(config); 6 | const webpackDevMiddleware = require('webpack-dev-middleware'); 7 | const webpackHotMiddleware = require('webpack-hot-middleware'); 8 | const webpackHotServerMiddleware = require('webpack-hot-server-middleware'); 9 | 10 | app.use(webpackDevMiddleware(compiler, { 11 | serverSideRender: true, 12 | publicPath: "/dist/", 13 | })); 14 | app.use(webpackHotMiddleware(compiler.compilers.find(compiler => compiler.name === 'client'))); 15 | app.use(webpackHotServerMiddleware(compiler)); 16 | 17 | const PORT = process.env.PORT || 3000; 18 | 19 | app.listen(PORT, error => { 20 | if (error) { 21 | 22 | return console.error(error); 23 | 24 | } else { 25 | 26 | console.log(`Development Express server running at http://localhost:${PORT}`); 27 | } 28 | }); 29 | -------------------------------------------------------------------------------- /express/production.js: -------------------------------------------------------------------------------- 1 | const express = require('express'); 2 | const path = require('path'); 3 | const app = express(); 4 | const ClientStatsPath = path.join(__dirname, './../dist/stats.json'); 5 | const ServerRendererPath = path.join(__dirname, './../dist/server.js'); 6 | const ServerRenderer = require(ServerRendererPath).default; 7 | const Stats = require(ClientStatsPath); 8 | 9 | app.use('/dist', express.static(path.join(__dirname, '../dist'))); 10 | app.use(ServerRenderer(Stats)); 11 | 12 | const PORT = process.env.PORT || 3050; 13 | 14 | app.listen(PORT, error => { 15 | if (error) { 16 | 17 | return console.error(error); 18 | 19 | } else { 20 | 21 | console.log(`Production Express server running at http://localhost:${PORT}`); 22 | } 23 | }); 24 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "reactzero-boilerplate", 3 | "version": "1.0.0", 4 | "description": "a poilerplate for react functional with hooks ( react version +16.8 ) ", 5 | "main": "index.js", 6 | "scripts": { 7 | "build": "webpack", 8 | "start:prod": "NODE_ENV=production node ./express/production.js", 9 | "start:dev": "babel-node ./node_modules/webpack-dev-server/bin/webpack-dev-server --open", 10 | "build:dev": "NODE_ENV=development node ./express/development.js", 11 | "build:prod": "NODE_ENV=production webpack -p --config ./webpack/webpack.production.config.js --progress --profile --colors", 12 | "webpack": "babel-node ./node_modules/webpack/bin/webpack" 13 | }, 14 | "repository": { 15 | "type": "git", 16 | "url": "https://github.com/nimahkh/reactzero-boilerplate.git" 17 | }, 18 | "author": "nima habibkhoda", 19 | "license": "MIT", 20 | "dependencies": { 21 | "@hot-loader/react-dom": "^16.8.6", 22 | "react": "^16.8.6", 23 | "react-dom": "^16.8.6", 24 | "react-helmet": "^5.2.1", 25 | "react-hot-loader": "^4.9.0", 26 | "react-jss": "^8.6.1", 27 | "react-redux": "^7.0.3", 28 | "react-router-dom": "^5.0.0" 29 | }, 30 | "devDependencies": { 31 | "@babel/cli": "^7.0.0", 32 | "@babel/core": "^7.4.5", 33 | "@babel/node": "^7.4.5", 34 | "@babel/preset-env": "^7.4.4", 35 | "@babel/preset-es2015": "^7.0.0-beta.53", 36 | "@babel/preset-react": "^7.0.0", 37 | "autoprefixer": "^9.5.1", 38 | "babel-loader": "^8.0.0-beta.6", 39 | "babel-plugin-dynamic-import-node": "^2.2.0", 40 | "babel-plugin-transform-class-properties": "^6.24.1", 41 | "babel-preset-env": "^1.7.0", 42 | "babel-preset-react": "^6.24.1", 43 | "clean-webpack-plugin": "^2.0.2", 44 | "css-loader": "^2.1.1", 45 | "css-mqpacker": "^7.0.0", 46 | "express": "^4.16.4", 47 | "extract-text-webpack-plugin": "^4.0.0-beta.0", 48 | "html-webpack-plugin": "^3.2.0", 49 | "isomorphic-style-loader": "^5.0.1", 50 | "optimize-css-assets-webpack-plugin": "^5.0.1", 51 | "redux": "^4.0.1", 52 | "stats-webpack-plugin": "^0.7.0", 53 | "style-loader": "^0.23.1", 54 | "uglifyjs-webpack-plugin": "^2.1.2", 55 | "webpack": "^4.31.0", 56 | "webpack-cli": "^3.3.2", 57 | "webpack-dev-middleware": "^3.6.2", 58 | "webpack-dev-server": "^3.7.0", 59 | "webpack-hot-middleware": "^2.24.4", 60 | "webpack-hot-server-middleware": "^0.6.0" 61 | } 62 | } 63 | -------------------------------------------------------------------------------- /src/action/types.js: -------------------------------------------------------------------------------- 1 | export const REQUEST_COUNTRIES = 'REQUEST_COUNTRIES'; 2 | export const RECEIVE_COUNTRIES = 'RECEIVE_COUNTRIES'; 3 | 4 | 5 | export const REQUEST_COUNTRY = 'REQUEST_COUNTRY'; 6 | export const RECEIVE_COUNTRY = 'RECEIVE_COUNTRY'; 7 | -------------------------------------------------------------------------------- /src/app/App.js: -------------------------------------------------------------------------------- 1 | import React, {useState} from 'react'; 2 | import Helmet from "react-helmet"; 3 | import {Switch, Route} from 'react-router-dom'; 4 | import {NavLink} from 'react-router-dom'; 5 | import injectSheet from 'react-jss' 6 | 7 | const styles = { 8 | active: { 9 | color: 'green', 10 | margin: { 11 | // jss-expand gives more readable syntax 12 | top: 5, // jss-default-unit makes this 5px 13 | right: 0, 14 | bottom: 0, 15 | left: '1rem' 16 | }, 17 | }, 18 | componentBody: { 19 | fontStyle: 'italic' 20 | }, 21 | clickMe:{ 22 | padding:10, 23 | backgroundColor:'#333', 24 | color:'#fff' 25 | } 26 | }; 27 | 28 | const Menu=injectSheet(styles)((props) =>{ 29 | const {classes}=props; 30 | return ( 31 |
32 | 43 |
44 | ); 45 | }) 46 | 47 | 48 | const Homepage=injectSheet(styles)((props) =>{ 49 | const {classes}=props; 50 | 51 | return ( 52 |
53 | 54 | 55 |

Homepage

56 |
57 | ); 58 | }) 59 | 60 | function About() { 61 | return ( 62 |
63 | 64 | 65 |

About

66 |
67 | ); 68 | } 69 | 70 | const Contact=injectSheet(styles)((props) =>{ 71 | const [counter,setCounter]=useState(0); 72 | const {classes}=props; 73 | 74 | function handleThis(){ 75 | setCounter(counter+1) 76 | } 77 | 78 | return ( 79 |
80 | 81 | 82 |

Contact

83 |
handleThis()}>Click on me to count this :{counter}
84 |
85 | ); 86 | }) 87 | 88 | function App () { 89 | return ( 90 |
91 | 101 | 102 | 103 | 104 | 105 | 106 |
107 | ); 108 | } 109 | 110 | export default injectSheet(styles)(App) 111 | -------------------------------------------------------------------------------- /src/app/template.js: -------------------------------------------------------------------------------- 1 | export default ({markup, helmet,sheets}) => { 2 | return ` 3 | 4 | 5 | ${helmet.title.toString()} 6 | ${helmet.meta.toString()} 7 | ${helmet.link.toString()} 8 | 9 | 10 | 11 |
${markup}
12 | 13 | 14 | `; 15 | }; 16 | -------------------------------------------------------------------------------- /src/client.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import {hydrate} from 'react-dom'; 3 | import {BrowserRouter} from 'react-router-dom'; 4 | import App from './app/App'; 5 | import {createStore} from 'redux' 6 | import {Provider} from 'react-redux' 7 | import counterApp from './reducers' 8 | 9 | const preloadedState = window.__PRELOADED_STATE__ 10 | 11 | delete window.__PRELOADED_STATE__ 12 | 13 | const store = createStore(counterApp, preloadedState) 14 | 15 | hydrate(( 16 | 17 | 18 | 19 | 20 | 21 | ), document.getElementById('root')); 22 | -------------------------------------------------------------------------------- /src/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | React SSR 7 | 8 | 9 |
10 | 11 | 12 | 13 | 14 | -------------------------------------------------------------------------------- /src/reducers/countries.js: -------------------------------------------------------------------------------- 1 | 2 | import { 3 | REQUEST_COUNTRIES, 4 | RECEIVE_COUNTRIES 5 | } from "../action/types"; 6 | 7 | const INITIAL_STATE = { 8 | data: [], 9 | isFetching: false, 10 | lastUpdate: Date.now() 11 | }; 12 | 13 | export default (state = INITIAL_STATE, action) => { 14 | switch (action.type) { 15 | case REQUEST_COUNTRIES: { 16 | return { ...state, isFetching: true }; 17 | } 18 | case RECEIVE_COUNTRIES: { 19 | return { ...state, isFetching: false, data: action.payload }; 20 | } 21 | default: 22 | return state; 23 | } 24 | }; 25 | -------------------------------------------------------------------------------- /src/reducers/country.js: -------------------------------------------------------------------------------- 1 | import { 2 | REQUEST_COUNTRY, 3 | RECEIVE_COUNTRY 4 | } from "../action/types"; 5 | 6 | const INITIAL_STATE = { 7 | name: '', 8 | nativeName: '', 9 | flag: '', 10 | capital: '', 11 | region: '', 12 | population: '', 13 | languages: [], 14 | isFetching: false, 15 | lastUpdate: Date.now() 16 | }; 17 | 18 | export default(state = INITIAL_STATE, action) => { 19 | switch(action.type) { 20 | case REQUEST_COUNTRY: { 21 | return { ...state, isFetching: true }; 22 | } 23 | case RECEIVE_COUNTRY: { 24 | return { ...state, isFetching: false, ...action.payload }; 25 | } 26 | default: return state; 27 | } 28 | }; 29 | -------------------------------------------------------------------------------- /src/reducers/index.js: -------------------------------------------------------------------------------- 1 | import { combineReducers } from 'redux'; 2 | import Countries from "./countries"; 3 | import Country from "./country"; 4 | 5 | export default combineReducers({ 6 | countries: Countries, 7 | country: Country, 8 | }); 9 | -------------------------------------------------------------------------------- /src/server.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import ReactDOMServer from 'react-dom/server'; 3 | import {StaticRouter} from 'react-router-dom'; 4 | import {Helmet} from "react-helmet"; 5 | import {createStore} from 'redux' 6 | import counterApp from './reducers' 7 | import {Provider} from 'react-redux' 8 | import Template from './app/template'; 9 | import App from './app/App'; 10 | import {JssProvider, SheetsRegistry} from 'react-jss' 11 | 12 | export default function serverRenderer({clientStats, serverStats}) { 13 | return (req, res, next) => { 14 | 15 | const sheets = new SheetsRegistry() 16 | const store = createStore(counterApp) 17 | const context = {}; 18 | const markup = ReactDOMServer.renderToString( 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | ); 27 | const preloadedState = store.getState() 28 | 29 | const helmet = Helmet.renderStatic(); 30 | 31 | res.status(200).send(renderFullPage(markup, helmet, sheets, preloadedState)); 32 | }; 33 | 34 | function renderFullPage(markup, helmet, sheets, preloadedState) { 35 | return ` 36 | 37 | ${helmet.title.toString()} 38 | ${helmet.meta.toString()} 39 | ${helmet.link.toString()} 40 | 41 | 49 | 50 | 51 |
${markup}
52 | 53 | 54 | 55 | ` 56 | } 57 | } 58 | -------------------------------------------------------------------------------- /webpack.config.babel.js: -------------------------------------------------------------------------------- 1 | /* eslint-disable */ 2 | const path = require('path'); 3 | const webpack = require('webpack'); 4 | const HtmlWebpackPlugin = require('html-webpack-plugin'); 5 | 6 | module.exports = { 7 | entry: ['webpack/hot/dev-server', './src/client.js'], 8 | output: { 9 | path: __dirname + '/dist/', 10 | publicPath: "/", 11 | filename: 'bundle.js', 12 | }, 13 | module: { 14 | rules: [ 15 | { 16 | test: /\.js$/, 17 | use: 'babel-loader', 18 | exclude: [/node_modules/, /dist/] 19 | }, 20 | ], 21 | }, 22 | plugins: [ 23 | new HtmlWebpackPlugin({ 24 | template: path.join(__dirname, 'src', 'index.html') 25 | }), 26 | new webpack.NamedModulesPlugin(), 27 | new webpack.HotModuleReplacementPlugin()], 28 | }; 29 | -------------------------------------------------------------------------------- /webpack/webpack.development.config.js: -------------------------------------------------------------------------------- 1 | const path = require('path'); 2 | const ExtractTextPlugin = require('extract-text-webpack-plugin'); 3 | 4 | const distDir = path.join(__dirname, '../dist'); 5 | const srcDir = path.join(__dirname, '../src'); 6 | 7 | module.exports = [ 8 | { 9 | mode : 'development', 10 | name: 'client', 11 | target: 'web', 12 | entry: `${srcDir}/client.js`, 13 | output: { 14 | path: path.join(__dirname, 'dist'), 15 | filename: 'client.js', 16 | publicPath: '/dist/', 17 | }, 18 | resolve: { 19 | extensions: ['.js', '.jsx'], 20 | alias: { 21 | 'react-dom': '@hot-loader/react-dom' 22 | } 23 | }, 24 | devtool: 'source-map', 25 | module: { 26 | rules: [ 27 | { 28 | test: /\.(js|jsx)$/, 29 | exclude: /(node_modules[\\\/])/, 30 | use: [ 31 | { 32 | loader: 'babel-loader', 33 | } 34 | ] 35 | }, 36 | { 37 | test: /\.pcss$/, 38 | use: ExtractTextPlugin.extract({ 39 | fallback: 'style-loader', 40 | use: [ 41 | { 42 | loader: 'css-loader', 43 | options: { 44 | modules: true, 45 | importLoaders: 1, 46 | localIdentName: '[local]', 47 | sourceMap: true, 48 | } 49 | }, 50 | { 51 | loader: 'postcss-loader', 52 | options: { 53 | config: { 54 | path: `${__dirname}/../postcss/postcss.config.js`, 55 | } 56 | } 57 | } 58 | ] 59 | }) 60 | }, 61 | ], 62 | }, 63 | plugins: [ 64 | new ExtractTextPlugin({ 65 | filename: 'styles.css', 66 | allChunks: true 67 | }) 68 | ] 69 | }, 70 | { 71 | name: 'server', 72 | target: 'node', 73 | entry: `${srcDir}/server.js`, 74 | output: { 75 | path: path.join(__dirname, 'dist'), 76 | filename: 'server.js', 77 | libraryTarget: 'commonjs2', 78 | publicPath: '/dist/', 79 | }, 80 | resolve: { 81 | extensions: ['.js', '.jsx'] 82 | }, 83 | module: { 84 | rules: [ 85 | { 86 | test: /\.(js|jsx)$/, 87 | exclude: /(node_modules[\\\/])/, 88 | use: [ 89 | { 90 | loader: 'babel-loader', 91 | } 92 | ] 93 | }, 94 | { 95 | test: /\.pcss$/, 96 | use: [ 97 | { 98 | loader: 'isomorphic-style-loader', 99 | }, 100 | { 101 | loader: 'css-loader', 102 | options: { 103 | modules: true, 104 | importLoaders: 1, 105 | localIdentName: '[local]', 106 | sourceMap: false 107 | } 108 | }, 109 | { 110 | loader: 'postcss-loader', 111 | options: { 112 | config: { 113 | path: `${__dirname}/../postcss/postcss.config.js`, 114 | } 115 | } 116 | } 117 | ] 118 | } 119 | ], 120 | }, 121 | } 122 | ]; 123 | -------------------------------------------------------------------------------- /webpack/webpack.production.config.js: -------------------------------------------------------------------------------- 1 | const path = require('path'); 2 | const webpack = require('webpack'); 3 | const ExtractTextPlugin = require('extract-text-webpack-plugin'); 4 | const StatsPlugin = require('stats-webpack-plugin'); 5 | const OptimizeCssAssetsPlugin = require('optimize-css-assets-webpack-plugin'); 6 | const CleanWebpackPlugin = require('clean-webpack-plugin'); 7 | const UglifyJsPlugin = require('uglifyjs-webpack-plugin'); 8 | 9 | const distDir = path.join(__dirname, '../dist'); 10 | const srcDir = path.join(__dirname, '../src'); 11 | 12 | module.exports = [ 13 | { 14 | name: 'client', 15 | target: 'web', 16 | entry: `${srcDir}/client.js`, 17 | output: { 18 | path: distDir, 19 | filename: 'client.js', 20 | publicPath: distDir, 21 | }, 22 | resolve: { 23 | extensions: ['.js', '.jsx'] 24 | }, 25 | module: { 26 | rules: [ 27 | { 28 | test: /\.(js|jsx)$/, 29 | exclude: /(node_modules[\\\/])/, 30 | use: [ 31 | { 32 | loader: 'babel-loader', 33 | } 34 | ] 35 | }, 36 | { 37 | test: /\.pcss$/, 38 | use: ExtractTextPlugin.extract({ 39 | fallback: 'style-loader', 40 | use: [ 41 | { 42 | loader: 'css-loader', 43 | options: { 44 | modules: true, 45 | importLoaders: 1, 46 | localIdentName: '[hash:base64:10]', 47 | sourceMap: false, 48 | } 49 | }, 50 | { 51 | loader: 'postcss-loader', 52 | options: { 53 | config: { 54 | path: `${__dirname}/../postcss/postcss.config.js`, 55 | } 56 | } 57 | } 58 | ] 59 | }) 60 | } 61 | ], 62 | }, 63 | optimization: { 64 | minimizer: [ 65 | // we specify a custom UglifyJsPlugin here to get source maps in production 66 | new UglifyJsPlugin({ 67 | cache: true, 68 | parallel: true, 69 | uglifyOptions: { 70 | compress: false, 71 | ecma: 6, 72 | mangle: true 73 | }, 74 | sourceMap: true 75 | }) 76 | ] 77 | }, 78 | plugins: [ 79 | new ExtractTextPlugin({ 80 | filename: 'styles.css', 81 | allChunks: true 82 | }), 83 | new webpack.DefinePlugin({ 84 | 'process.env': { 85 | NODE_ENV: '"production"' 86 | } 87 | }), 88 | new CleanWebpackPlugin(), 89 | new webpack.optimize.OccurrenceOrderPlugin(), 90 | ] 91 | }, 92 | { 93 | name: 'server', 94 | target: 'node', 95 | entry: `${srcDir}/server.js`, 96 | output: { 97 | path: distDir, 98 | filename: 'server.js', 99 | libraryTarget: 'commonjs2', 100 | publicPath: distDir, 101 | }, 102 | resolve: { 103 | extensions: ['.js', '.jsx'] 104 | }, 105 | module: { 106 | rules: [ 107 | { 108 | test: /\.(js|jsx)$/, 109 | exclude: /(node_modules[\\\/])/, 110 | use: [ 111 | { 112 | loader: 'babel-loader', 113 | } 114 | ] 115 | }, 116 | { 117 | test: /\.pcss$/, 118 | use: [ 119 | { 120 | loader: 'isomorphic-style-loader', 121 | }, 122 | { 123 | loader: 'css-loader', 124 | options: { 125 | modules: true, 126 | importLoaders: 1, 127 | localIdentName: '[hash:base64:10]', 128 | sourceMap: false 129 | } 130 | }, 131 | { 132 | loader: 'postcss-loader', 133 | options: { 134 | config: { 135 | path: `${__dirname}/../postcss/postcss.config.js`, 136 | } 137 | } 138 | } 139 | ] 140 | } 141 | ], 142 | }, 143 | plugins: [ 144 | new OptimizeCssAssetsPlugin({ 145 | cssProcessorOptions: {discardComments: {removeAll: true}} 146 | }), 147 | new StatsPlugin('stats.json', { 148 | chunkModules: true, 149 | modules: true, 150 | chunks: true, 151 | exclude: [/node_modules[\\\/]react/], 152 | }), 153 | ] 154 | } 155 | ]; 156 | --------------------------------------------------------------------------------