Refactor Blog: (2) SSR
March 26, 2021
<script>
tag. The js file is fetched then executed. The logic inside starts working. Then the app is created and attached to a specific HTML element.import * as Express from "express";
const APP_PORT = process.env.PORT || 3000;
const app = Express();
const renderer = () => {
app.get("*", (req: Express.Request, res: Express.Response) => {
res.send(`
<!DOCTYPE HTML>
<html>
<head>
<!-- insert metadata here -->
</head>
<body>
<div id="app-root"></div>
</body>
</html>
`);
});
};
app.use(renderer);
app.listen(APP_PORT, () => {
console.log(`Server is listening on ${APP_PORT}`);
});
ReactDOMServer.renderToString(element)
. Pass the app in JSX format as the element parameter.import * as React from "react";
import * as ReactDOMServer from "react-dom/server";
import { App } from "../client/App";
let htmlBody;
try {
htmlBody = ReactDOMServer.renderToString(<App />);
} catch (error) {
console.error(error);
}
Helmet.renderStatic()
. You have to call it after ReactDOMServer.renderToString
.import { Helmet } from "react-helmet";
let htmlBody;
let helmet;
try {
htmlBody = ReactDOMServer.renderToString(<App />);
helmet = Helmet.renderStatic();
} catch (error) {
console.error(error);
}
StaticRouter
.import { StaticRouter } from "react-router-dom";
let htmlBody;
let context;
app.get("*", (req: Express.Request, res: Express.Response) => {
try {
htmlBody = ReactDOMServer.renderToString(
<StaticRouter location={req.url} context={context}>
<App />
</StaticRouter>
);
} catch(error) {
console.error(error);
}
};
sheet.collectStyles
to wrap the app, then use sheet.getStyleTags
to collect all of the app's styles.import { ServerStyleSheet } from "styled-components";
const sheet = new ServerStyleSheet();
let htmlBody = "";
let styleTags = "";
try {
htmlBody = ReactDOMServer.renderToString(sheet.collectStyles(<App />));
styleTags = sheet.getStyleTags();
} catch (error) {
console.error(error);
} finally {
sheet.seal();
}
Provider
, and get the initialState
with the API Redux provided.import { createStore } from "redux";
import { Provider } from "react-redux";
import { rootReducer, RootState } from "../client/service/reducer";
app.get("*", (req: Express.Request, res: Express.Response) => {
// pass request because store is likely dependent on request URL
const yourStore = yourStoreInitiator(req);
const preloadedStore: RootState = {
// your store state
};
let htmlBody = "";
const store = createStore(rootReducer, preloadedState);
try {
htmlBody = ReactDOMServer.renderToString(
<Provider store={store}>
<App />
</Provider>
);
} catch (error) {
console.error(error);
}
// store.getState() returns object so we have to manually stringify it
const initialState = JSON.stringify(store.getState());
};
const getFullHTML = (
htmlBody: string,
styleTags: string,
initialState: string,
helmet?: HelmetData
) => {
return `
<!DOCTYPE html>
<html ${helmet?.htmlAttributes.toString()}>
<head>
<meta name="viewport" content="width=device-width, initial-scale=1">
${styleTags}
${helmet?.title.toString()}
${helmet?.meta.toString()}
${helmet?.link.toString()}
<style>
@import url('<https://fonts.googleapis.com/css2?family=Noto+Serif+JP:wght@400;700;900&display=swap>');
@import url('<https://fonts.googleapis.com/css2?family=Noto+Sans+JP:wght@100;400&display=swap>');
</style>
</head>
<body ${helmet?.bodyAttributes.toString()}>
<div id="app-root">${htmlBody}</div>
<script>window.__INITIAL_STATE__ = ${initialState}</script>
<script defer src="main.bundle.js" type="text/javascript" charset="utf-8"></script>
<script defer src="vendor.bundle.js" type="text/javascript" charset="utf-8"></script>
</body>
</html>
`;
};
import * as Express from "express";
import * as React from "react";
import * as ReactDOMServer from "react-dom/server";
import { Helmet, HelmetData } from "react-helmet";
import { Provider } from "react-redux";
import { StaticRouter } from "react-router-dom";
import { createStore } from "redux";
import { ServerStyleSheet } from "styled-components";
import { App } from "../client/App";
import { rootReducer, RootState } from "../client/service/reducer";
export const renderer = (
req: Express.Request,
res: Express.Response,
next: Express.NextFunction,
) => {
app.get("*", (req: Express.Request, res: Express.Response) => {
const yourStore = yourStoreInitiator(req);
const preloadedStore: RootState = { /* your state */ };
let htmlBody = "";
const context = {};
const sheet = new ServerStyleSheet();
let styleTags = "";
const store = createStore(rootReducer, preloadedState);
let helmet;
try {
htmlBody = ReactDOMServer.renderToString(
sheet.collectStyles(
<Provider store={store}>
<StaticRouter location={req.url} context={context}>
<App />
</StaticRouter>
</Provider>
)
);
helmet = Helmet.renderStatic();
styleTags = sheet.getStyleTags();
} catch (error) {
console.error(error);
} finally {
sheet.seal();
}
const initialState = JSON.stringify(store.getState());
const fullHTML = getFullHTML(htmlBody, styleTags, initialState, helmet);
res.send(fullHTML);
};
curl -X GET localhost:3000
to check if the express server is returning the whole app in the first easily.