Look at the following two code snippets showing how to render the same top-level React component on the client (in a browser) and on the server (in Node.js app):
In order to render the App
component on the client, you write:
import ReactDOM from 'react-dom'; import App from './components/App'; ReactDOM.hydrate(<App />, document.getElementById('app'));
In order to render the same component on the server (in Node.js app), you write:
import ReactDOM from 'react-dom/server'; import App from './components/App'; const html = ReactDOM.renderToString(<App />);
Both methods will try to build an in-memory representation of the UI tree (aka virtual DOM) of the App
component. The first one will compare that virtual DOM with the actual DOM inside the <div id="app"></div>
HTML element and will modify the actual DOM to make it match the virtual DOM exactly. The second method will just convert the in-memory representation of the UI tree into HTML, which then can be sent to a client.
Now, let's see how a complete example for the client-side and server-side application code looks like. Go ahead and create client.js
file with the following content:
import 'babel-core/register'; import React from 'react'; import ReactDOM from 'react-dom'; import App from './components/App'; function run() { ReactDOM.hydrate(<App />, document.getElementById('app')); } const loadedStates = ['complete', 'loaded', 'interactive']; if (loadedStates.includes(document.readyState) && document.body) { run(); } else { window.addEventListener('DOMContentLoaded', run, false); }
This ensures that the React application is only mounted when the HTML page was fully loaded into the browsers.
For the server-side app, let's create server.js
file with the following content:
import express from 'express'; import React from 'react'; import ReactDOM from 'react-dom/server'; import App from './components/App'; const server = express(); const port = process.env.PORT || 3000; server.get('*', (req, res) => { const title = 'Sample Application'; const app = ReactDOM.renderToString(<App />); res.send(`<!doctype html> <html> <head> <title>${title}</title> <src script="client.js"></script> </head> <body> <div id="app">${app}</div> </body> </html>`); }); server.listen(port, () => { console.log(`App is listening at http://localhost:${port}/`); });
It renders the App component to an HTML string, wraps it into a valid HTML5 document with <head>
and <body>
sections, and sends it to a client on all HTTP requests to the server.
We can go further and replace that ES7 string literal above with a React-based template in order not to worry about escaping HTML fragments. In order to do so, create components/Html.js
file with the following content:
import React from 'react'; const Html = (props) => <html> <head> <meta charSet="utf-8" /> <meta httpEquiv="x-ua-compatible" content="ie=edge" /> <title>{props.title || ''}</title> <meta name="description" content={props.description || ''} /> <meta name="viewport" content="width=device-width, initial-scale=1" /> <script src="client.js" /> </head> <body> <div id="app" dangerouslySetInnerHTML={{__html: props.children}} /> </body> </html>; export default Html;
Since this component doesn't use state, we don't need to extend it from React.Component
, but instead, we can use a regular function which accepts a collection of properties as an argument and returns a React component. Make sure that all the tags in the mark-up used in your React components are properly closed and you use valid JSX attributes. For example, instead of <meta charset="utf-8">
, you should write <meta charSet="utf-8" />
and so on.
Note
If you're new to JSX syntax, visit the following two pages to get more information about it:https://facebook.github.io/react/docs/jsx-in-depthhttps://facebook.github.io/react/docs/jsx-gotchas.
Another addition which we can make to the server.js
file is to add Express.js
static middleware to make it serve static files, such as robots.txt
from the /public
folder. Now, the final server.js
file should look like this:
import path from 'path'; import express from 'express'; import React from 'react'; import ReactDOM from 'react-dom/server'; import Html from './components/Html'; import App from './components/App'; const server = express(); const port = process.env.PORT || 3000; server.use(express.static(path.join(__dirname, 'public'))); server.get('*', (req, res) => { const body = ReactDOM.renderToString(<App />); const html = ReactDOM.renderToStaticMarkup(<Html title="My App" description="Isomorphic web application sample" body={body} />); res.send('<!doctype html>\n' + html); }); server.listen(port, () => console.log( `Node.js server is listening at http://localhost:${port}/` ));