├── .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 |
11 |
{{rendered}}
12 |
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 | } --------------------------------------------------------------------------------