├── .babelrc
├── .gitignore
├── README.md
├── client
├── App.jsx
└── client.jsx
├── package.json
├── public
└── index.html
├── server
├── database.js
└── server.jsx
├── shared
└── utility.js
└── webpack.config.js
/.babelrc:
--------------------------------------------------------------------------------
1 | {
2 | "presets": [
3 | "@babel/preset-env",
4 | "@babel/preset-react"
5 | ]
6 | }
7 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | # Logs
2 | logs
3 | *.log
4 | npm-debug.log*
5 | yarn-debug.log*
6 | yarn-error.log*
7 | lerna-debug.log*
8 |
9 | # Diagnostic reports (https://nodejs.org/api/report.html)
10 | report.[0-9]*.[0-9]*.[0-9]*.[0-9]*.json
11 |
12 | # Runtime data
13 | pids
14 | *.pid
15 | *.seed
16 | *.pid.lock
17 |
18 | # Directory for instrumented libs generated by jscoverage/JSCover
19 | lib-cov
20 |
21 | # Coverage directory used by tools like istanbul
22 | coverage
23 | *.lcov
24 |
25 | # nyc test coverage
26 | .nyc_output
27 |
28 | # Grunt intermediate storage (https://gruntjs.com/creating-plugins#storing-task-files)
29 | .grunt
30 |
31 | # Bower dependency directory (https://bower.io/)
32 | bower_components
33 |
34 | # node-waf configuration
35 | .lock-wscript
36 |
37 | # Compiled binary addons (https://nodejs.org/api/addons.html)
38 | build/Release
39 |
40 | # Dependency directories
41 | node_modules/
42 | jspm_packages/
43 |
44 | # TypeScript v1 declaration files
45 | typings/
46 |
47 | # TypeScript cache
48 | *.tsbuildinfo
49 |
50 | # Optional npm cache directory
51 | .npm
52 |
53 | # Optional eslint cache
54 | .eslintcache
55 |
56 | # Microbundle cache
57 | .rpt2_cache/
58 | .rts2_cache_cjs/
59 | .rts2_cache_es/
60 | .rts2_cache_umd/
61 |
62 | # Optional REPL history
63 | .node_repl_history
64 |
65 | # Output of 'npm pack'
66 | *.tgz
67 |
68 | # Yarn Integrity file
69 | .yarn-integrity
70 |
71 | # dotenv environment variables file
72 | .env
73 | .env.test
74 |
75 | # parcel-bundler cache (https://parceljs.org/)
76 | .cache
77 |
78 | # Next.js build output
79 | .next
80 |
81 | # Nuxt.js build / generate output
82 | .nuxt
83 | dist
84 |
85 | # Gatsby files
86 | .cache/
87 | # Comment in the public line in if your project uses Gatsby and not Next.js
88 | # https://nextjs.org/blog/next-9-1#public-directory-support
89 | # public
90 |
91 | # vuepress build output
92 | .vuepress/dist
93 |
94 | # Serverless directories
95 | .serverless/
96 |
97 | # FuseBox cache
98 | .fusebox/
99 |
100 | # DynamoDB Local files
101 | .dynamodb/
102 |
103 | # TernJS port file
104 | .tern-port
105 |
106 | # Stores VSCode versions used for testing VSCode extensions
107 | .vscode-test
108 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # Server Rendered React Application
2 |
3 | ## Overview
4 | This simple application serves a server-rendered React application to the client. The application is made up of only a single component, but has full interactivity.
5 |
6 | ## Usage
7 | To preview the app, first install the necessary dependencies:
8 |
9 | `npm install`
10 |
11 | Then, build the client code and start the server with one step using
12 |
13 | `npm start`
14 |
15 | The application should now be visible at `http:localhost:7777`.
--------------------------------------------------------------------------------
/client/App.jsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 |
3 | export const App = ({questions, answers, handleVote})=>(
4 |
5 |
6 |
7 | Dev Team Decision Tool
8 |
9 |
10 |
11 | {questions.map(({questionId, content})=>(
12 |
13 |
14 |
15 |
16 |
17 | {content}
18 |
19 |
20 |
21 |
22 | {answers.filter(answer => answer.questionId === questionId).sort((a,b) => b.upvotes - a.upvotes).map(({
23 |
24 | content,
25 | upvotes,
26 | answerId
27 |
28 | })=>(
29 |
30 |
31 |
32 |
33 |
34 |
35 | {content} - {upvotes}
36 |
37 |
38 |
39 |
40 |
41 |
42 |
43 |
44 |
45 |
46 |
47 |
48 |
49 |
50 |
51 | ))}
52 |
53 |
54 |
55 |
56 |
57 | ))}
58 |
59 |
60 | )
--------------------------------------------------------------------------------
/client/client.jsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import ReactDOM from 'react-dom';
3 |
4 | import { App } from './App';
5 | import { copyAnswersWithModifiedUpvotes } from '../shared/utility';
6 |
7 | let state = undefined;
8 |
9 | console.info("Client:: Fetching data from server");
10 |
11 | fetch("http://localhost:7777/data")
12 | .then(data => data.json())
13 | .then(json => {
14 |
15 | state = json;
16 | render();
17 |
18 | });
19 |
20 | function handleVote(answerId, increment){
21 |
22 | state.answers = copyAnswersWithModifiedUpvotes(state.answers, answerId, increment);
23 |
24 | fetch(`vote/${answerId}?increment=${increment}`);
25 |
26 | render();
27 |
28 | };
29 |
30 | function render(){
31 |
32 | console.info("Client:: Rendering application with remote data", state);
33 | ReactDOM.hydrate(, document.querySelector("#Container"));
34 |
35 | }
36 |
37 |
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "react-server-rendering-demo-2020",
3 | "version": "1.0.0",
4 | "description": "",
5 | "main": "index.js",
6 | "scripts": {
7 | "build": "webpack",
8 | "server": "babel-node server/server",
9 | "start": "npm run build && npm run server"
10 | },
11 | "keywords": [],
12 | "author": "",
13 | "license": "ISC",
14 | "dependencies": {
15 | "compression": "^1.7.4",
16 | "express": "^4.17.1",
17 | "react": "^16.12.0",
18 | "react-dom": "^16.12.0"
19 | },
20 | "devDependencies": {
21 | "@babel/core": "^7.7.7",
22 | "@babel/node": "^7.7.7",
23 | "@babel/preset-env": "^7.7.7",
24 | "@babel/preset-react": "^7.7.4",
25 | "babel-loader": "^8.0.6",
26 | "webpack": "^4.41.5",
27 | "webpack-cli": "^3.3.10",
28 | "webpack-dev-server": "^3.10.1"
29 | }
30 | }
31 |
--------------------------------------------------------------------------------
/public/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
--------------------------------------------------------------------------------
/server/database.js:
--------------------------------------------------------------------------------
1 | /** A blackbox wrapper containing a dummy data set and database.
2 | * Can be modified to use production data without affecting the rest of the application. */
3 |
4 | import { copyAnswersWithModifiedUpvotes } from '../shared/utility';
5 |
6 | const data = {
7 |
8 | questions:[{
9 |
10 | questionId:"Q1",
11 | content:"Which back end solution should we use for our application?"
12 |
13 | },{
14 |
15 | questionId:"Q2",
16 | content:"What percentage of developer time should be devoted to end-to-end testing?"
17 |
18 | }],
19 | answers:[{
20 |
21 | answerId:"A1",
22 | questionId:1,
23 | upvotes:2,
24 | content: "Apache"
25 |
26 | },{
27 |
28 | answerId:"A2",
29 | questionId:"Q1",
30 | upvotes:0,
31 | content:"Java"
32 |
33 | },{
34 |
35 | answerId:"A3",
36 | questionId:"Q1",
37 | upvotes:4,
38 | content:"Node.js"
39 |
40 | },{
41 |
42 | answerId:"A4",
43 | questionId:"Q2",
44 | upvotes:2,
45 | content:"25%"
46 |
47 | },{
48 |
49 | answerId:"A5",
50 | questionId:"Q2",
51 | upvotes:1,
52 | content:"50%"
53 |
54 | },{
55 |
56 | answerId:"A6",
57 | questionId:"Q2",
58 | upvotes:1,
59 | content:"75%"
60 |
61 | }]
62 |
63 | }
64 |
65 | export async function getData() {
66 |
67 | return data;
68 |
69 | }
70 |
71 | export function modifyAnswerUpvotes(answerId, increment) {
72 |
73 | console.log("Modifying data", answerId, increment);
74 |
75 | data.answers = copyAnswersWithModifiedUpvotes(data.answers, answerId, increment);
76 |
77 | }
--------------------------------------------------------------------------------
/server/server.jsx:
--------------------------------------------------------------------------------
1 | import express from 'express';
2 | import React from 'react';
3 | import compression from 'compression';
4 | import { renderToString } from 'react-dom/server';
5 | import { readFileSync } from 'fs';
6 |
7 | import { App } from '../client/App';
8 | import { getData, modifyAnswerUpvotes } from './database';
9 |
10 | const app = new express();
11 | const port = 7777;
12 |
13 | app.use(compression());
14 | app.use(express.static("dist"));
15 |
16 | app.get("/data", async (_req, res) => {
17 |
18 | res.json(await getData());
19 |
20 | });
21 |
22 | app.get("/vote/:answerId", (req, res) => {
23 |
24 | const { query, params } = req;
25 | modifyAnswerUpvotes(params.answerId, parseInt(query.increment));
26 |
27 | });
28 |
29 | app.get("/", async ( _req, res )=>{
30 |
31 | const { questions, answers } = await getData();
32 |
33 | const rendered = renderToString();
34 |
35 | const index = readFileSync(`public/index.html`, `utf8`);
36 |
37 | res.send(index.replace("{{rendered}}", rendered));
38 |
39 | });
40 |
41 | app.listen(port);
42 | console.info(`App listening on port ${port}`);
--------------------------------------------------------------------------------
/shared/utility.js:
--------------------------------------------------------------------------------
1 | export function copyAnswersWithModifiedUpvotes (answers, answerId, increment) {
2 |
3 | return answers.map(answer => {
4 |
5 | if (answer.answerId === answerId) {
6 |
7 | return {
8 |
9 | ... answer,
10 | upvotes: answer.upvotes + increment
11 | }
12 |
13 |
14 | } else {
15 |
16 | return answer;
17 |
18 | }
19 |
20 | });
21 |
22 | }
--------------------------------------------------------------------------------
/webpack.config.js:
--------------------------------------------------------------------------------
1 | const {resolve} = require('path');
2 |
3 | module.exports = {
4 | mode: "development",
5 | entry:{
6 | client: "./client/client.jsx",
7 | },
8 | output: {
9 | filename: '[name].js'
10 | },
11 | resolve: {
12 | extensions: ['.js', '.jsx'],
13 | },
14 | /** can module be omitted for a simple project? No, it cannot. */
15 | module: {
16 | rules: [
17 | {
18 | test: /\.jsx?$/,
19 | exclude: /(node_modules)/,
20 | use: {
21 | loader: 'babel-loader',
22 | options: {
23 | presets: [
24 | '@babel/preset-env',
25 | '@babel/preset-react'
26 | ]
27 | }
28 | }
29 | }
30 | ]
31 | }
32 | }
--------------------------------------------------------------------------------