├── .gitignore
├── src
├── public
│ └── js
│ │ ├── lib
│ │ ├── requests
│ │ │ ├── getRequest.js
│ │ │ └── createRequest.js
│ │ └── api
│ │ │ └── products.js
│ │ ├── components
│ │ ├── footer
│ │ │ └── Footer.js
│ │ ├── home
│ │ │ ├── components
│ │ │ │ └── topProduct
│ │ │ │ │ └── TopProduct.js
│ │ │ └── Home.js
│ │ ├── header
│ │ │ └── Header.js
│ │ ├── app
│ │ │ ├── ClientRouter.js
│ │ │ ├── ServerRouter.js
│ │ │ └── App.js
│ │ └── product
│ │ │ └── Product.js
│ │ └── main.js
├── lib
│ └── services
│ │ ├── index.js
│ │ └── productService
│ │ ├── productData.js
│ │ └── ProductService.js
├── views
│ ├── index.js
│ └── home.html
└── app.js
├── .vscode
└── settings.json
├── server.js
├── package.json
├── README.md
└── webpack.config.js
/.gitignore:
--------------------------------------------------------------------------------
1 | node_modules/
2 |
--------------------------------------------------------------------------------
/src/public/js/lib/requests/getRequest.js:
--------------------------------------------------------------------------------
1 | const createRequest = require("./createRequest");
2 |
3 | module.exports = createRequest("GET");
4 |
--------------------------------------------------------------------------------
/src/lib/services/index.js:
--------------------------------------------------------------------------------
1 | const ProductService = require("./productService/ProductService");
2 |
3 | module.exports = {
4 | productService: new ProductService()
5 | };
6 |
--------------------------------------------------------------------------------
/src/public/js/components/footer/Footer.js:
--------------------------------------------------------------------------------
1 | const React = require("react");
2 |
3 | const Footer = () => {
4 | return ;
5 | };
6 |
7 | module.exports = Footer;
8 |
--------------------------------------------------------------------------------
/.vscode/settings.json:
--------------------------------------------------------------------------------
1 | {
2 | "spellright.language": [
3 | "en"
4 | ],
5 | "spellright.documentTypes": [
6 | "markdown",
7 | "latex",
8 | "plaintext"
9 | ]
10 | }
--------------------------------------------------------------------------------
/server.js:
--------------------------------------------------------------------------------
1 | const app = require("./src/app");
2 |
3 | const port = process.env.PORT || 3000;
4 |
5 | app.listen(port, () => {
6 | console.log("Running server on port:", port);
7 | console.log("--------------------------");
8 | });
9 |
--------------------------------------------------------------------------------
/src/public/js/lib/requests/createRequest.js:
--------------------------------------------------------------------------------
1 | module.exports = method => data => {
2 | return {
3 | method,
4 | headers: {
5 | "Content-Type": "application/json"
6 | },
7 | body: JSON.stringify(data)
8 | };
9 | };
10 |
--------------------------------------------------------------------------------
/src/lib/services/productService/productData.js:
--------------------------------------------------------------------------------
1 | module.exports = {
2 | 1: {
3 | id: 1,
4 | name: "foo"
5 | },
6 | 2: {
7 | id: 2,
8 | name: "bar"
9 | },
10 | 3: {
11 | id: 3,
12 | name: "baz"
13 | }
14 | };
15 |
--------------------------------------------------------------------------------
/src/public/js/main.js:
--------------------------------------------------------------------------------
1 | const React = require("react");
2 | const ReactDOM = require("react-dom");
3 | const Router = require("./components/app/ClientRouter");
4 |
5 | ReactDOM.render(
6 | ,
7 | document.getElementById("root")
8 | );
9 |
--------------------------------------------------------------------------------
/src/public/js/components/home/components/topProduct/TopProduct.js:
--------------------------------------------------------------------------------
1 | const React = require("react");
2 | const { Link } = require("react-router-dom");
3 |
4 | module.exports = ({ product }) => {
5 | return (
6 |
7 | {product.name}
8 |
9 | );
10 | };
11 |
--------------------------------------------------------------------------------
/src/public/js/components/header/Header.js:
--------------------------------------------------------------------------------
1 | const React = require("react");
2 | const { Link } = require("react-router-dom");
3 |
4 | const Header = () => {
5 | return (
6 |
7 |
10 |
11 | );
12 | };
13 |
14 | module.exports = Header;
15 |
--------------------------------------------------------------------------------
/src/lib/services/productService/ProductService.js:
--------------------------------------------------------------------------------
1 | const productData = require("./productData");
2 |
3 | class ProductService {
4 | async getProduct(productId) {
5 | return productData[productId];
6 | }
7 |
8 | async getTopProducts() {
9 | return Object.values(productData);
10 | }
11 | }
12 |
13 | module.exports = ProductService;
14 |
--------------------------------------------------------------------------------
/src/public/js/components/app/ClientRouter.js:
--------------------------------------------------------------------------------
1 | const React = require("react");
2 | const App = require("./App");
3 | const { BrowserRouter } = require("react-router-dom");
4 |
5 | const ClientRouter = ({ state }) => {
6 | return (
7 |
8 |
9 |
10 | );
11 | };
12 |
13 | module.exports = ClientRouter;
14 |
--------------------------------------------------------------------------------
/src/public/js/components/app/ServerRouter.js:
--------------------------------------------------------------------------------
1 | const React = require("react");
2 | const App = require("./App");
3 | const { StaticRouter } = require("react-router-dom");
4 |
5 | const ServerApp = ({ url, state }) => {
6 | return (
7 |
8 |
9 |
10 | );
11 | };
12 |
13 | module.exports = ServerApp;
14 |
--------------------------------------------------------------------------------
/src/public/js/lib/api/products.js:
--------------------------------------------------------------------------------
1 | const getRequest = require("../requests/getRequest");
2 |
3 | const getTopProducts = async () => {
4 | const res = await fetch("/api/v1/products", getRequest());
5 | return res.json();
6 | };
7 |
8 | const getProduct = async productId => {
9 | const res = await fetch(`/api/v1/products/${productId}`, getRequest());
10 | return res.json();
11 | };
12 |
13 | module.exports = {
14 | getTopProducts,
15 | getProduct
16 | };
17 |
--------------------------------------------------------------------------------
/src/views/index.js:
--------------------------------------------------------------------------------
1 | const html = (jsx, state = {}) => {
2 | return `
3 |
4 |
5 |
6 |
7 |
8 |
9 | Document
10 |
11 |
12 |
13 |
14 | ${jsx}
15 |
16 |
17 |
18 |
19 | `;
20 | };
21 |
22 | module.exports = html;
23 |
--------------------------------------------------------------------------------
/src/public/js/components/product/Product.js:
--------------------------------------------------------------------------------
1 | const React = require("react");
2 | const { getProduct } = require("../../lib/api/products");
3 |
4 | class Product extends React.Component {
5 | constructor(props) {
6 | super(props);
7 | this.state = { product: props.product };
8 | }
9 |
10 | async componentDidMount() {
11 | if (this.props.product.id != this.props.match.params.productId) {
12 | const product = await getProduct(this.props.match.params.productId);
13 | this.setState({ product });
14 | }
15 | }
16 |
17 | render() {
18 | return (
19 |
20 | {this.state.product.name}
21 |
22 | );
23 | }
24 | }
25 |
26 | module.exports = Product;
27 |
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "ssr-react",
3 | "version": "1.0.0",
4 | "description": "## What we will cover",
5 | "main": "index.js",
6 | "scripts": {
7 | "start": "nodemon ./bundle.server.js",
8 | "webpack": "npx webpack --watch",
9 | "test": "echo \"Error: no test specified\" && exit 1"
10 | },
11 | "author": "",
12 | "license": "ISC",
13 | "dependencies": {
14 | "express": "4.16.4",
15 | "react": "16.6.3",
16 | "react-dom": "16.6.3",
17 | "react-router-dom": "4.3.1"
18 | },
19 | "devDependencies": {
20 | "@babel/core": "7.1.6",
21 | "@babel/preset-react": "7.0.0",
22 | "babel-loader": "8.0.4",
23 | "nodemon": "1.18.6",
24 | "webpack": "4.26.0",
25 | "webpack-cli": "3.1.2"
26 | }
27 | }
28 |
--------------------------------------------------------------------------------
/src/public/js/components/app/App.js:
--------------------------------------------------------------------------------
1 | const React = require("react");
2 | const Home = require("../home/Home");
3 | const Product = require("../product/Product");
4 | const Header = require("../header/Header");
5 | const Footer = require("../footer/Footer");
6 | const { Route, Switch } = require("react-router-dom");
7 |
8 | const initState = {
9 | products: [],
10 | selectedProduct: {}
11 | }
12 |
13 | const App = ({state = initState}) => {
14 | return (
15 | <>
16 |
17 |
18 | } />
19 | } />
20 | } />
21 |
22 |
23 | >
24 | );
25 | };
26 |
27 | module.exports = App;
28 |
--------------------------------------------------------------------------------
/src/public/js/components/home/Home.js:
--------------------------------------------------------------------------------
1 | const React = require("react");
2 | const TopProduct = require("./components/topProduct/TopProduct");
3 | const { getTopProducts } = require("../../lib/api/products");
4 |
5 | class Home extends React.Component {
6 | constructor(props) {
7 | super(props);
8 | this.state = { products: props.products };
9 | }
10 |
11 | async componentDidMount() {
12 | /**
13 | * Benefit, we can save on network calls if the user
14 | * has refreshed the page
15 | */
16 | if (this.props.products.length <= 0) {
17 | const products = await getTopProducts();
18 | this.setState({ products });
19 | }
20 | }
21 |
22 | render() {
23 | return (
24 |
25 | {this.state.products.map(product => {
26 | return ;
27 | })}
28 |
29 | );
30 | }
31 | }
32 |
33 | module.exports = Home;
34 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # ssr-react
2 |
3 | ## What we will cover
4 |
5 | SSR - server side rendering
6 |
7 | * What is React SSR?
8 | * How does it work?
9 | * What are the benefits of doing it?
10 | * What drawbacks does it bring?
11 | * What alternatives do we have?
12 |
13 | ## Notes
14 |
15 | SSR is when the server renders out a html file so that the browser can immediately show the content to the user.
16 |
17 | This is different from how SPA applications usually work since they depend on JavaScript and that is not immediately available when the browser loads the page.
18 |
19 | SSR has the benefit of allowing us to provide a experience that feels faster for the user since there is visible content as soon as the html document is loaded in the browser.
20 |
21 | However using SSR with React has a few drawbacks we need to consider as well.
22 |
23 | Sometimes it is simpler to be without SSR and a good alternative is to use placeholder content to improve the users experience.
24 |
--------------------------------------------------------------------------------
/src/views/home.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 | Document
8 |
26 |
27 |
28 |
29 |
30 |
31 |
34 |
35 |
40 |
41 |
42 |
43 |
44 |
45 |
--------------------------------------------------------------------------------
/webpack.config.js:
--------------------------------------------------------------------------------
1 | const path = require("path");
2 |
3 | const Config = (entry, name, target, path) => {
4 | return {
5 | entry,
6 | target,
7 | output: {
8 | path,
9 | filename: `bundle.${name}.js`
10 | },
11 | mode: "development",
12 | module: {
13 | rules: [
14 | {
15 | test: /\.jsx?/,
16 | exclude: /node_modules/,
17 | loader: "babel-loader",
18 | options: {
19 | presets: ["@babel/preset-react"]
20 | }
21 | }
22 | ]
23 | }
24 | };
25 | };
26 |
27 | const clientEntry = path.resolve(__dirname, "src", "public", "js", "main.js");
28 | const clientPath = path.resolve(__dirname, "dist");
29 | const clientConfig = Config(clientEntry, "main", "web", clientPath);
30 |
31 | const serverEntry = path.resolve(__dirname, "server.js");
32 | const serverPath = __dirname;
33 | const serverConfig = Config(serverEntry, "server", "node", serverPath);
34 |
35 | /**
36 | * First drawback, we need to transpile our server code when we could
37 | * have kept it as standard JavaScript.
38 | */
39 | module.exports = [serverConfig, clientConfig];
40 |
--------------------------------------------------------------------------------
/src/app.js:
--------------------------------------------------------------------------------
1 | const express = require("express");
2 | const React = require("react");
3 | const ReactDOMServer = require("react-dom/server");
4 | const path = require("path");
5 | const Router = require("./public/js/components/app/ServerRouter");
6 | const html = require("./views");
7 | const { productService } = require("./lib/services");
8 | const app = express();
9 |
10 | app.use(express.json());
11 | app.use(express.static("./dist"));
12 |
13 | app.get("/api/v1/products", async (req, res) => {
14 | const products = await productService.getTopProducts();
15 | res.json(products);
16 | });
17 |
18 | app.get("/api/v1/products/:productId", async (req, res) => {
19 | const { productId } = req.params;
20 | const product = await productService.getProduct(productId);
21 | res.json(product);
22 | });
23 |
24 | /**
25 | * Second drawback, now we need to keep 2 sets of endpoints in sync
26 | * and make sure that the server rendered state is the same as the
27 | * client state.
28 | */
29 |
30 | app.get("/", async (req, res) => {
31 | const products = await productService.getTopProducts();
32 | const state = { products };
33 | const jsx = ReactDOMServer.renderToStaticMarkup(
34 |
35 | );
36 | res.end(html(jsx, state));
37 | });
38 |
39 | app.get("/home", async (req, res) => {
40 | res.sendFile(path.resolve("src/views/home.html"));
41 | });
42 |
43 | app.get("/products/:productId", async (req, res) => {
44 | const { productId } = req.params;
45 | const product = await productService.getProduct(productId);
46 | const state = { selectedProduct: product };
47 | const jsx = ReactDOMServer.renderToStaticMarkup(
48 |
49 | );
50 | res.end(html(jsx, state));
51 | });
52 |
53 | app.get("/test", (req, res) => {
54 | res.end(html());
55 | });
56 |
57 | app.get("/*", (req, res) => {
58 | const jsx = ReactDOMServer.renderToStaticMarkup( );
59 | res.end(html(jsx));
60 | });
61 |
62 | module.exports = app;
63 |
--------------------------------------------------------------------------------