├── .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 |
33 | -
34 | Homepage
35 |
36 | -
37 | About
38 |
39 | -
40 | Contact
41 |
42 |
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 |
--------------------------------------------------------------------------------