Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Going Step By Step to have SSR #1

Open
wants to merge 26 commits into
base: recro-airmeet-session
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
26 commits
Select commit Hold shift + click to select a range
e7bffd6
Set up express
ankushdharkar May 15, 2020
25e2e22
JSX causes error
ankushdharkar May 15, 2020
8e09099
Run webpack babel for server and use build
ankushdharkar May 15, 2020
83c3089
Nodemon and watch commands
ankushdharkar May 15, 2020
6bc5eea
Module imports in Node, Thanks Webpack
ankushdharkar May 15, 2020
31eaaaf
Why does click not work
ankushdharkar May 15, 2020
1efb324
Client bundle generation
ankushdharkar May 15, 2020
00dbf77
Renderer file, client js bundle
ankushdharkar May 15, 2020
495d016
Use hydrate
ankushdharkar May 15, 2020
6a3ed15
Run all commands together. Multiple colons are confusing to npm-run-all
ankushdharkar May 15, 2020
d82daa2
Added externals ignore in s-build
ankushdharkar May 15, 2020
f70f294
Client Router
ankushdharkar May 15, 2020
43ff052
Add static router
ankushdharkar May 15, 2020
cc1081d
Try another route for confirmation
ankushdharkar May 15, 2020
f8c6c59
Set up redux with a fetch all concerts
ankushdharkar May 15, 2020
df6776e
Concerts List Page created that loads data from data fetching
ankushdharkar May 15, 2020
57d55db
Routes in object format
ankushdharkar May 15, 2020
5a93919
Waiting available for loadData calls, but it disappears on client
ankushdharkar May 15, 2020
61edfe5
Rehydrate Redux State on client
ankushdharkar May 15, 2020
519f23d
Prevent XSS
ankushdharkar May 15, 2020
31ce627
App super component that renders on every page
ankushdharkar May 15, 2020
3462d10
Implement a 404 page for good UX
ankushdharkar May 15, 2020
50d3f4a
Not Found page should return a 404 code
ankushdharkar May 15, 2020
99ed36b
SEO using helmet and dynamic title
ankushdharkar May 15, 2020
d7b500d
Helmet on home page
ankushdharkar May 15, 2020
2b62f14
Added PR details in the README
ankushdharkar Aug 15, 2020
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -1,5 +1,8 @@
# See https://help.github.com/articles/ignoring-files/ for more about ignoring files.

c-bundle.js
s-bundle.js

# dependencies
/node_modules
/.pnp
Expand Down
5 changes: 5 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,3 +1,8 @@
# SSR

Please check out the sequence on how to convert a basic React app into it's SSR Equivalent, by going through the commits of this PR one-by-one (Use 'n' and 'p' keyboard shortcuts to go next and to previous commit): https://github.com/ankushdharkar/SSR-with-React-step-by-step/pull/1


This project was bootstrapped with [Create React App](https://github.com/facebook/create-react-app).

## Available Scripts
Expand Down
35 changes: 35 additions & 0 deletions api/all-concerts.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
const allConcerts = [
{
id: 1001,
name: 'Ricky Martin',
level: 'vip'
},
{
id: 1003,
name: 'AC/DC',
level: 'vip'
},
{
id: 5009,
name: 'Taylor Swift',
level: 'supervip'
},
{
id: 5009,
name: 'Shankar Mahadevan',
level: 'incountry'
},
{
id: 9800,
name: 'Ricky Martin',
level: 'vip'
}
];

export default () => {
return new Promise( (resolve, reject) => {
setTimeout( () => {
resolve(allConcerts);
}, 5000);
})
}
Empty file added api/concert.js
Empty file.
Empty file added api/trending.js
Empty file.
24 changes: 24 additions & 0 deletions config/webpack.client.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
const path = require('path');

module.exports = {
entry: './src/client.js',
output : {
filename: 'c-bundle.js',
path: path.resolve(__dirname, '../public')
},
module: {
rules: [
{
test:/\.js?$/,
loader: 'babel-loader',
exclude: /node_modules/,
options: {
presets: [
'@babel/react',
['@babel/env', { targets: { browsers: ['last 2 versions'] } } ]
]
}
}
]
}
}
27 changes: 27 additions & 0 deletions config/webpack.server.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
const path = require('path');
const webpackNodeExternals = require('webpack-node-externals');

module.exports = {
target: 'node', // Server side. Not for client browser
entry: './server/index.js',
output : {
filename: 's-bundle.js',
path: path.resolve(__dirname, '../build')
},
module: {
rules: [
{
test:/\.js?$/,
loader: 'babel-loader',
exclude: /node_modules/,
options: {
presets: [
'@babel/react',
['@babel/env', { targets: { browsers: ['last 2 versions'] } } ]
]
}
}
]
},
externals: [webpackNodeExternals()]
}
28 changes: 26 additions & 2 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -6,15 +6,28 @@
"@testing-library/jest-dom": "^4.2.4",
"@testing-library/react": "^9.3.2",
"@testing-library/user-event": "^7.1.2",
"babel-polyfill": "^6.26.0",
"express": "^4.17.1",
"react": "^16.13.1",
"react-dom": "^16.13.1",
"react-scripts": "3.4.1"
"react-helmet": "^6.0.0",
"react-redux": "^7.2.0",
"react-router-config": "^5.1.1",
"react-router-dom": "^5.2.0",
"react-scripts": "3.4.1",
"redux": "^4.0.5",
"redux-thunk": "^2.3.0",
"serialize-javascript": "^3.0.0"
},
"scripts": {
"start": "react-scripts start",
"build": "react-scripts build",
"test": "react-scripts test",
"eject": "react-scripts eject"
"eject": "react-scripts eject",
"dev": "npm-run-all --parallel dev:*",
"dev:build-server-watch": "webpack --config config/webpack.server.js --watch",
"dev:server": "nodemon --watch build --exec \"node build/s-bundle.js\"",
"dev:build-client-watch": "webpack --config config/webpack.client.js --watch"
},
"eslintConfig": {
"extends": "react-app"
Expand All @@ -30,5 +43,16 @@
"last 1 firefox version",
"last 1 safari version"
]
},
"devDependencies": {
"@babel/core": "^7.9.6",
"@babel/preset-env": "^7.9.6",
"@babel/preset-react": "^7.9.4",
"babel-loader": "^8.1.0",
"babel-preset-react": "^6.24.1",
"nodemon": "^2.0.4",
"npm-run-all": "^4.1.5",
"webpack-cli": "^3.3.11",
"webpack-node-externals": "^1.7.2"
}
}
File renamed without changes.
36 changes: 36 additions & 0 deletions server/index.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
import express from 'express';
import { generatedHTML } from './renderer';
import serverCreateStore from './store';

import { matchRoutes } from 'react-router-config';
import Routes from './../src/routes';

const app = express();

app.use(express.static('public'));

app.get('*', (req, res) => {
const { path } = req;
const store = serverCreateStore();

const allMatchedRoutesArr = matchRoutes(Routes, path); // List of components to render for the path, before sending html

const allLoadPromises = allMatchedRoutesArr.map( ({ route }) => {
const { loadData } = route;
return loadData && loadData(store);
});

Promise.all(allLoadPromises).then(() => {
const context = {};
const contentRendered = generatedHTML(path, store, context);

if (context.notFound) {
res.status(404);
}
res.send(contentRendered);
});
});

app.listen(4000, () => {
console.log('Listening to Port 4000');
});
63 changes: 63 additions & 0 deletions server/renderer.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,63 @@
import React from 'react';
import { renderToString } from 'react-dom/server';
import { StaticRouter } from 'react-router-dom';
import { Provider } from 'react-redux';
import { renderRoutes } from 'react-router-config';
import serialize from 'serialize-javascript';
import { Helmet } from 'react-helmet';

import Routes from "./../src/routes";


function getContent(path, store, context) {
const routeContent = (
<Provider store={store}>
<StaticRouter location={path} context={context}>
{ renderRoutes(Routes) }
</StaticRouter>
</Provider>
)
const content = renderToString(routeContent);
return content;
}

function getHeadTags () {
const headTagsData = Helmet.renderStatic();
const { title: titleTagData, meta: metaTagsData } = headTagsData;
const titleTagStr = titleTagData.toString();
const metaTagsStr = metaTagsData.toString();
return `
${titleTagStr}
${metaTagsStr}
`
}


function getScripts(store) {
const stateString = serialize(store.getState());
return `
<script>window.INITIAL_STORE_STATE = ${stateString}</script>
<script src="c-bundle.js"></script>
`;
}

function generatedHTML(path, store, context) {
const content = getContent(path, store, context);
const scripts = getScripts(store);
const headTags = getHeadTags();

return `
<html>
<head>
${headTags}
</head>
<body>
<div id="root">
${content}
${scripts}
</div>
</body>
</html>`
}

export { generatedHTML }
8 changes: 8 additions & 0 deletions server/store.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
import { createStore, applyMiddleware } from 'redux';
import thunk from 'redux-thunk';
import reducers from './../src/reducers';

export default () => {
const store = createStore(reducers, {}, applyMiddleware(thunk));
return store;
}
22 changes: 18 additions & 4 deletions src/App.js
Original file line number Diff line number Diff line change
@@ -1,12 +1,26 @@
import React from 'react';
import './App.css';
import { Link } from 'react-router-dom';
import { renderRoutes } from 'react-router-config';

function App() {
function App({ route }) {
return (
<div className="App">
App.js
<br />

<Link to="/">
Home
</Link>
{'\t\t'}
<Link to="/all-concerts">
All Concerts
</Link>

<br/>
<br/>

{ renderRoutes(route.routes) }
</div>
);
}

export default App;
export default { component: App };
11 changes: 11 additions & 0 deletions src/actions/all-concerts.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
import 'babel-polyfill';
import getAllConcerts from "./../../api/all-concerts";
import { FETCH_ALL_CONCERTS_TYPE } from "../constants/action-types";

export default () => async dispatch => {
const res = await getAllConcerts();
dispatch({
type: FETCH_ALL_CONCERTS_TYPE,
payload: res
})
}
29 changes: 29 additions & 0 deletions src/client.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
import React from 'react';
import ReactDOM from 'react-dom';
// import './index.css';
// import * as serviceWorker from './serviceWorker';
import { BrowserRouter } from 'react-router-dom';
import { createStore, applyMiddleware } from 'redux';
import thunk from 'redux-thunk';
import { Provider } from 'react-redux';
import { renderRoutes } from 'react-router-config';
import Routes from './routes';
import reducers from './reducers'

const { INITIAL_STORE_STATE: initialStoreState = {} } = window;
const store = createStore(reducers, initialStoreState, applyMiddleware(thunk));

ReactDOM.hydrate(
<Provider store={store}>
<BrowserRouter>
{ renderRoutes(Routes) }
</BrowserRouter>
</Provider>
,
document.getElementById('root')
);

// If you want your app to work offline and load faster, you can change
// unregister() to register() below. Note this comes with some pitfalls.
// Learn more about service workers: https://bit.ly/CRA-PWA
// serviceWorker.unregister();
1 change: 1 addition & 0 deletions src/constants/action-types.js
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export const FETCH_ALL_CONCERTS_TYPE = 'FETCH_ALL_CONCERTS_TYPE';
17 changes: 0 additions & 17 deletions src/index.js

This file was deleted.

Loading