Kaj Białas
Daniel Capeletti
Artur Kudeł
Michał Przyszczypkowski
How SPA works?
A web application that fits on a single web page with the goal of providing a user experience similar to that of a desktop application. In an SPA, either all necessary code – HTML, JavaScript, and CSS – is retrieved with a single page load or the appropriate resources are dynamically loaded and added to the page as necessary, usually in response to user actions.
What problem this has?
Posible ways to solve problems?
Probably the best way to solve problems?
Server Side Rendering
Fully server side rendering
SPA
(without server side rendering)
Users happy
but sad bots...
Fully server side rendering
SPA
(without server side rendering)
Server Side Rendering
+
SPA
=
problem solved?
BROWSER
PROXY (RENDERING) SERVER
API SERVER
URL
API
requests
responses
Generated html
CDN
GET
STATIC
ASSETS
non-interactive
time
on server side:
But in our example will be
python-react
React.NET
react-php-v8js
and more...
switch (action.type) {
case PREPARE_NUMBER:
return Object.assign({}, state, { number: action.number * 10 });
default:
return state;
}export class App extends React.Component {
render() {
return (
<Switch>
<Route exact={true} path="/:id" component={StaticContainer} />
<Route component={NotFound} />
</Switch>
)
}
}constructor(props) {
super(props);
this.props.prepareNumber(this.props.match.params.id);
}
render() {
return (
<div>
<h1>Static number: {this.props.number} </h1>
</div>
)
}import Express from 'express';
import React from 'react';
import { renderToString } from 'react-dom/server';
import { createStore } from 'redux';
import { App } from './app/App';
import createReducer from './app/reducers/app.redux';
import { StaticRouter } from 'react-router';
import { Provider } from 'react-redux';
const app = Express();
const port = 3000;
app.use(Express.static('public'));
app.use(handleRender);
function handleRender(req, res) {
let store = createStore(createReducer());
const context = {};
renderToString(
<Provider store={store}>
<StaticRouter context={context} location={req.url}>
<App/>
</StaticRouter>
</Provider>
);
const preloadedState = store.getState();
store = createStore(createReducer(), preloadedState);
const html = renderToString(
<Provider store={store}>
<StaticRouter context={context} location={req.url}>
<App/>
</StaticRouter>
</Provider>
);
res.send(`
<!doctype html>
<html>
<header>
<title>Simple SSR in React</title>
</header>
<body>
<script>window.__PRELOADED_STATE__ = ${JSON.stringify(preloadedState).replace(/</g, '\\u003c')}</script>
<div id='app'>${html}</div>
<script src='bundle.js'></script>
</body>
</html>
`)
}
app.listen(port);import React from 'react';
import ReactDOM from 'react-dom';
import { BrowserRouter } from 'react-router-dom';
import { Provider } from 'react-redux';
import { createStore } from 'redux';
import {App} from './App';
import createReducer from './reducers/app.redux';
const preloadedState = window.__PRELOADED_STATE__;
delete window.__PRELOADED_STATE__;
const store = createStore(createReducer(), preloadedState);
export default class Main extends React.Component {
render() {
return (
<Provider store={store}>
<BrowserRouter>
<App/>
</BrowserRouter>
</Provider>
);
}
}
ReactDOM.render((
<Main/>
), document.getElementById('app'));
Fetching data
Different JS environment
React Lifecycle
Caching strategies
First things first
We need a NodeJS server
const express = require('express');
const app = express();
.
.
.
// Start your app.
app.listen(port, host, (err) => {
if (err) {
return logger.error(err.message);
}
// Connect to ngrok in dev mode
if (ngrok) {
ngrok.connect(port, (innerErr, url) => {
if (innerErr) {
return logger.error(innerErr);
}
logger.appStarted(port, prettyHost, url);
});
} else {
logger.appStarted(port, prettyHost);
}
});Let's handle a connection
//Somewhere in the server:
const handleSSR = require('./middlewares/handleSSR');
app.get('*', handleSSR);
/* --------- */
const app = express();
app.use(handleSSR);
/* --------- */
module.exports = function handleSSR(req, res) {
const options = {
assets,
webpackDllNames: extractWebpackDllNamesFromPackage(),
lang: req.acceptsLanguages(appLocales),
};
renderAppToStringAtLocation(req.url, options, (response) => {
if (response.error) {
res.status(500).send(response.error.message);
printError(response.error);
} else if (response.redirectLocation) {
res.redirect(302, response.redirectLocation);
} else if (response.notFound) {
res.status(404).send(response.html);
} else {
res.status(200).send(response.html);
}
});
};What do we need to render a react application on backend? (in a nutshell)
// In a nutshell
import { renderToStaticMarkup } from 'react-dom/server';
const doc = renderToStaticMarkup(
<HtmlDocument
appMarkup={appMarkup}
lang={state.locales.language}
state={state}
head={Helmet.rewind()}
assets={assets}
/>
);Well... we need a bit more than that
import 'babel-polyfill';
import React from 'react';
import { Provider } from 'react-redux';
import { renderToString, renderToStaticMarkup } from 'react-dom/server';
import { createMemoryHistory, match, RouterContext } from 'react-router';
import { syncHistoryWithStore } from 'react-router-redux';
import Helmet from 'react-helmet';
import configureStore from './modules/store';
import routes from './routes';
import HtmlDocument from './htmlDocument';
import { selectLocationState } from './modules/router/router.selectors';
function renderAppToString(store, renderProps) {
return renderToString(
<Provider store={store}>
<RouterContext {...renderProps} />
</Provider>
);
}
function renderAppToStringAtLocation(url, { webpackDllNames = [], assets, lang }, callback) {
const memoryHistory = createMemoryHistory(url);
const store = configureStore({}, memoryHistory);
syncHistoryWithStore(memoryHistory, store, {
selectLocationState: selectLocationState(),
});
match({ routes, location: url }, (error, redirectLocation, renderProps) => {
if (error) {
callback({ error });
} else if (redirectLocation) {
callback({ redirectLocation: redirectLocation.pathname + redirectLocation.search });
} else if (renderProps) {
store.rootSaga.done.then(() => {
const state = store.getState().toJS();
const appMarkup = renderAppToString(store, renderProps);
const doc = renderToStaticMarkup(
<HtmlDocument
appMarkup={appMarkup}
lang={state.locales.language}
state={state}
head={Helmet.rewind()}
assets={assets}
webpackDllNames={webpackDllNames}
/>
);
const html = `<!DOCTYPE html>\n${doc}`;
callback({ html });
}).catch((e) => {
callback({ e });
});
renderAppToString(store, renderProps);
store.close();
} else {
callback({ error: new Error('Unknown error') });
}
});
}
export {
renderAppToStringAtLocation,
};
Let's break this down a bit
React router match
function renderAppToStringAtLocation(url, { webpackDllNames = [], assets, lang }, callback) {
match({ routes, location: url }, (error, redirectLocation, renderProps) => {
if (error) {
callback({ error });
} else if (redirectLocation) {
callback({ redirectLocation: redirectLocation.pathname + redirectLocation.search });
} else if (renderProps) {
//Rest of code goes here This function is to be used for server-side rendering. It matches a set of routes to a location, without rendering, and calls a callback(error, redirectLocation, renderProps) when it's done.
http://knowbody.github.io/react-router-docs/api/match.html
After we match the route
.
.
} else if (renderProps) {
//Stuff going on here, will get back in a minute ;)
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
renderAppToString(store, renderProps);
store.close();We call renderApptoString
here
This will render the application, trigger the sagas, make the API calls and so on!
Next, we gonna stop the sagas
store.close = () => store.dispatch(END);Why do we stop sagas?
After we stop sagas
store.rootSaga = sagaMiddleware.run(rootSaga);
store.rootSaga.done.then(() => {
const state = store.getState().toJS();
const appMarkup = renderAppToString(store, renderProps);
const doc = renderToStaticMarkup(
<HtmlDocument
appMarkup={appMarkup}
lang={state.locales.language}
state={state}
head={Helmet.rewind()}
assets={assets}
webpackDllNames={webpackDllNames}
/>
);
const html = `<!DOCTYPE html>\n${doc}`;
callback({ html });
}).catch((e) => {
callback({ e });
});
renderAppToString(store, renderProps);
store.close();So, when our sagas finishes
We render the app again with new state
Render to static markup (html)
Respond our rendered html :)
Curious about HtmlDocument?
render() {
const { lang, head, appMarkup, state, assets, webpackDllNames } = this.props;
const attrs = head.htmlAttributes.toComponent();
return (
<html lang={lang} {...attrs}>
<head>
<meta charSet="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<meta name="mobile-web-app-capable" content="yes" />
{head.title.toComponent()}
{head.meta.toComponent()}
<link href="https://fonts.googleapis.com/css?family=Open+Sans:400,700" rel="stylesheet" />
<link href={assets.main.css} rel="stylesheet" />
</head>
<body>
<noscript>
If you are seeing this message, that means <strong>JavaScript has been disabled on your browser</strong>
, please <strong>enable JS</strong> to make this app work.
</noscript>
<div id="app">
<div dangerouslySetInnerHTML={{ __html: appMarkup }} />
</div>
<script dangerouslySetInnerHTML={{ __html: `APP_STATE = ${htmlescape(state)}` }} />
{(webpackDllNames || []).map((dllName) =>
<script data-dll key={dllName} src={`/${dllName}.dll.js`}></script>
)}
<script type="text/javascript" src={assets.main.js}></script>
</body>
</html>
);
}Webpack
module.exports = {
name: 'server',
target: 'web',
externals: [
nodeExternals(),
],
entry: [
path.join(process.cwd(), 'app/server.js'),
],
output: {
path: outputPath,
filename: 'server.js',
publicPath: '/',
libraryTarget: 'commonjs2',
},Webpack - Server side
new webpack.DefinePlugin({
'process.env': {
NODE_ENV: JSON.stringify(process.env.NODE_ENV),
},
'__SERVER__': false,
'__CLIENT__': true,
}), new webpack.DefinePlugin({
'process.env': {
NODE_ENV: JSON.stringify(process.env.NODE_ENV),
},
'__CLIENT__': false,
'__SERVER__': true,
}),Webpack - Frontend side
So you could..
if (__SERVER__) {
//do server-specific code
}
if (__CLIENT__) {
//do client-specific code
//i.e. window.location = 'http://google.com'
}Problems and Optimizations
We have a problem with routing, when using /en, /pt and so on
App dispatches the redux actions on Backend and Frontend
The code deserves a refactoring
Currently hosted at https://github.com/cpBurn/redux-saga-ssr