How To Create A Universal Applications On React + Express?
We suggest that you get acquainted with the current state of the “ecosystem” of React.js, as for today all that Next.js does, and even more, can be done with the help of relatively simple tricks on React.js.
There are, of course, ready-made blanks for projects. For example, we really like this project, which, unfortunately, is based on the non-actuator version of the router. And very relevant, although not such a “well-deserved” project.
Use ready-made projects with a lot of poorly documented features a bit scary, because do not know where to stumble, and most importantly – how to develop the project. Therefore, for those who want to understand the current state of the issue (and for myself), we have prepared the project with explanations. There will not be any personal exclusive code in it. Just a compilation of examples of documentation and a large number of articles.
Here is a list of tasks that a universal application should solve.
1. Asynchronous preloading of data on the server (React.js like most similar library implements only synchronous rendering) and the formation of the component state.
2. Server-side rendering of the component.
3. Pass the state of the component to the client.
4. Recreating the component on the client with the state transmitted from the server.
5. “Joining” the component (hydrarte (…)) to the markup received from the server (analog render (…)).
6. Splitting the code into the optimal number of fragments (code splitting).
And, of course, there should not be any differences in the code of the server part and the client side of the application front-end. The same component should work the same both for server-side and client-side rendering.
Let’s start with routing. In the React documentation for the implementation of universal routing, it is proposed to configure routes based on a simple object.
For example:
// routes.js module.exports = [ { path: '/', exact: true, // component: Home, componentName: 'home' }, { path: '/users', exact: true, // component: UsersList, componentName: 'components/usersList', }, { path: '/users/:id', exact: true, // component: User, componentName: 'components/user', }, ];
This form of routing description allows:
- to form a server and client router based on a single source;
- on the server to do preloading of data before creating an instance of the component;
- organize splitting the code into the optimal number of fragments (code splitting).
The server router code is very simple:
</pre> import React from 'react'; import { Switch, Route } from 'react-router'; import routes from './routes'; import Layout from './components/layout' export default (data) => ( <Layout> <Switch> { routes.map(props => { props.component = require('./' + props.componentName); if (props.component.default) { props.component = props.component.default; } return <Route key={ props.path } {...props}/> }) } </Switch> </Layout> ); <pre>
Lack of the possibility to use the full common Layout /> in Next.js just served as a starting point for writing this article.
The client router code is a little more complicated:
import React from 'react'; import { Router, Route, Switch} from 'react-router'; import routes from './routes'; import Loadable from 'react-loadable'; import Layout from './components/layout'; export default (data) => ( <Layout> <Switch> { routes.map(props => { props.component = Loadable({ loader: () => import('./' + props.componentName), loading: () => null, delay: () => 0, timeout: 10000, }); return <Route key={ props.path } {...props}/>; }) } </Switch> </Layout> );
The most interesting part is in the code snippet () => import (‘./’ + props.componentName). The import () function gives the web pack command to implement code splitting. If the page had a normal import or require () construct, then web pack would include the component code in one resulting file. And so the code will be loaded when switching to a router from a separate code fragment.
Consider the main entry point of the client part of the frontend:
'use strict' import React from 'react'; import { hydrate } from 'react-dom'; import { Provider } from 'react-redux'; import {BrowserRouter} from 'react-router-dom'; import Layout from './react/components/layout'; import AppRouter from './react/clientRouter'; import routes from './react/routes'; import createStore from './redux/store'; const preloadedState = window.__PRELOADED_STATE__; delete window.__PRELOADED_STATE__; const store = createStore(preloadedState); const component = hydrate( <Provider store={store}> <BrowserRouter> <AppRouter /> </BrowserRouter> </Provider>, document.getElementById('app') );
Everything is usually enough and is described in the React documentation. The state of the component is recreated from the server and the component is “attached” to the finished markup. We draw your attention that not all libraries allow such an operation to be done in one line of code, as it can be done in React.js.
And the same component in the server version:
import { matchPath } from 'react-router-dom'; import routes from './react/routes'; import AppRouter from './react/serverRouter'; import stats from '../dist/stats.generated'; ... app.use('/', async function(req, res, next) { const store = createStore(); const promises = []; const componentNames = []; routes.forEach(route => { const match = matchPath(req.path, route); if (match) { let component = require('./react/' + route.componentName); if (component.default) { component = component.default; } componentNames.push(route.componentName); if (typeof component.getInitialProps == 'function') { promises.push(component.getInitialProps({req, res, next, match, store})); } } return match; }) Promise.all(promises).then(data => { const context = {data}; const html = ReactDOMServer.renderToString( <Provider store={store}> <StaticRouter location={req.url} context={context}> <AppRouter/> </StaticRouter> </Provider> ); if (context.url) { res.writeHead(301, { Location: context.url }) res.end() } else { res.write(` <!doctype html> <script> // WARNING: See the following for security issues around embedding JSON in HTML: // http://redux.js.org/docs/recipes/ServerRendering.html#security-considerations window.__PRELOADED_STATE__ = ${JSON.stringify(store.getState()).replace(/</g, '\\u003c')} </script> <div id="app">${html}</div> <script src='${assets(stats.common)}'></script> ${componentNames.map(componentName => `<script src='${assets(stats[componentName])}'></script>` )} `) res.end() } }) });
The most significant part is the definition of the routing of the required component:
routes.forEach(route => { const match = matchPath(req.path, route); if (match) { let component = require('./react/' + route.componentName); if (component.default) { component = component.default; } componentNames.push(route.componentName); if (typeof component.getInitialProps == 'function') { promises.push(component.getInitialProps({req, res, next, match, store})); } } return match; })
After we find the component, we call its asynchronous static method component.getInitialProps ({req, res, next, match, store}). Static – because the instance of the component on the server has not yet been created. This method is named by analogy with Next.js.
Here’s how this method might look in the component:
class Home extends React.PureComponent { static async getInitialProps({ req, match, store, dispatch }) { const userAgent = req ? req.headers['user-agent'] : navigator.userAgent const action = userActions.login({name: 'John', userAgent}); if (req) { await store.dispatch(action); } else { dispatch(action); } return; }
To store the state of the object redux is used, which in this case significantly facilitates access to the state on the server. Without redux, this would be not only difficult but very difficult.
For the convenience of development, it is necessary to ensure the compilation of client and server code components on the fly and update the browser. About this and also about the configurations of the web pack for the work of the project, We plan to talk about it in the next article.