📜 ⬆️ ⬇️

Universal React + Express Applications

In the last article, we looked at the Next.js library, which allows you to develop universal applications out of the box. In the discussion of the article, significant shortcomings of this library were voiced. Judging by the fact that https://github.com/zeit/next.js/issues/88 has been actively discussed since October 2016, there will be no solution to the problem in the near future.

Therefore, I propose to get acquainted with the current state of the “ecosystem” React.js, because today everything that Next.js does, and even more, can be done with the help of relatively simple tricks. There are, of course, finished projects. For example, I really like the project , which, unfortunately, is based on the irrelevant version of the router. And very relevant, although not such a “deserved” project .

Using ready-made projects with a mass of poorly documented features is a bit scary, because you do not know where you will stumble, and most importantly - how to develop the project. Therefore, for those who want to understand the current state of the issue (and for themselves), I made a draft of the project with explanations. It will not be some kind of my personal exclusive code. Just a compilation of examples of documentation and a large number of articles.

In the last article , the tasks that the universal application should solve were listed.
')
1. Asynchronous preloading of data on the server (React.js, like most of these libraries, implements only synchronous rendering) and the formation of the state of the component.
2. Server rendering component.
3. Transfer the status of the component to the client.
4. Recreation of the component on the client with the status transmitted from the server.
5. "Attaching" the component (hydrarte (...)) to the markup received from the server (analog of render (...)).
6. Code splitting into the optimal number of fragments (code splitting).

And, of course, there should be no differences in the code of the server part and the client part of the application frontend. The same component should work the same in both server and client rendering.

Let's start with the routing. In the React documentation for the implementation of universal routing, it is proposed to form 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 route description form allows you to:

1) create a server and client router based on a single source;
2) on the server to preload data before creating an instance of the component;
3) organize code splitting into the optimal number of fragments (code splitting).

The server router code is very simple:

 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> ); 

The lack of the ability to use the full-fledged overall <Layout/> in Next.js was the starting point for writing this article.

The client router code is a bit 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 the code snippet () => import('./' + props.componentName) . The import () function gives the webpack command to implement code splitting. If the page had the usual import or require () construction, then the webpack would include the component code in one resulting file. And so the code will be loaded when switching to a route from a separate code fragment.

Consider the main entry point of the front end of the client:

 '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 fairly common and is described in the React documentation. The state of the component from the server is recreated and the component “joins” to the ready markup. I draw your attention to the fact that not all libraries allow you to do such an operation in one line of code, as can be done in React.js.

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 determination by route of the necessary 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 a component, we call its asynchronous static method component.getInitialProps({req, res, next, match, store}) . Static - because a component instance on the server has not yet been created. This method is named by analogy with Next.js. Here is how this method might look like in a 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 greatly facilitates access to the state on the server Without redux, this would be not only difficult but very difficult.

For ease of development, you need to ensure that the client and server code of the components are compiled on the fly and the browser is updated. I plan to talk about this as well as the webpack configurations for the project in the next article.
https://github.com/apapacy/uni-react

apapacy@gmail.com
February 14, 2018

Source: https://habr.com/ru/post/349064/


All Articles