📜 ⬆️ ⬇️

How to start developing universal applications with the Next.js library

We don't need no traffic building,
We don't need no SEO,
No link exchanges in your network,
Spammers! leave us all alone.

Anna Filina

A bit of history


Back in 2013, Spike Brehm from Airbnb published a programmatic article in which he analyzed the disadvantages of SPA-applications (Single Page Application), and alternatively proposed a model of isomorphic web applications. The term universal web application is now more often used (see discussion ).

In a universal web application, each page can be configured both by a web server and by means of JavaScript on the side of a web browser. At the same time, the source code of the programs that are executed by the web server and the web browser must be uniform (universal) in order to eliminate inconsistency and increased development costs.

The story of the author of the idea, Spike Brehm from Airbnb, has now ended with a complete victory, and recently, on December 7, 2017, on his Twitter account, he announced that the Airbnb website had switched to server-side rendering of SPA applications.
')

Criticism SPA-applications


What is wrong with SPA applications? And what problems arise when developing universal applications?

SPA-applications are criticized, first of all, for the low ranking in search engines (SEO), speed, accessibility. (This refers to accessibility as understood in https://www.w3.org/Translations/WCAG20-ru . There is evidence that React applications may not be available for screen readers.)

Partially the issue with SEO SPA-applications is solved by Prerender - a server with a “headless” web browser, which is implemented using chrome-remote-interface (previously used phantomjs). You can deploy your own server with Prerender or access a public service . In the latter case, access will be free with a limit on the number of pages. The process of page generation by means of Prerender is time-consuming - usually more than 3 seconds, which means that search engines will consider such a service not optimized for speed, and its rating will still be low.

Performance problems may not manifest themselves during the development process and become noticeable when working with low-speed Internet or on a low-power mobile device (for example, a phone or tablet with 1GB of RAM parameters and a 1.2GHz processor frequency). In this case, a page that “flies” may load unexpectedly long. For example, one minute. The reasons for such a slow boot are more than usually indicated. To begin, let's see how the application loads JavaScript. If there are a lot of scripts (which was typical when using require.js and amd-modules), then the load time increased due to the overhead of connecting to the server for each of the requested files. The solution was obvious: connect all the modules into one file (using rjs, webpack or another linker). This caused a new problem: for a web application with a rich interface and logic, when loading the first page, all the JavaScript code loaded in a single file was loaded. Therefore, the current trend is code spliting . We will return to this issue when we consider the necessary functionality for building universal web applications. The question is not that it is impossible or difficult to do. The question is that it is desirable to have the tools that do it optimally and without additional efforts on the part of the developer. And finally, when all the JavaScript code has been loaded and interpreted, the construction of the DOM document begins and ... finally, the loading of pictures begins.

Libraries for creating universal applications


On github.com now you can find a large number of projects that implement the idea of ​​universally web applications. However, all these projects have common drawbacks:

  1. small number of project contributors
  2. These are projects for quick start, not a library
  3. projects were not updated when new versions of react.js were released
  4. Only a part of the functionality necessary for developing a universal application is implemented in projects.

The first successful solution was the Next.js library, which, as of January 14, 2018, has 338 contributors and 21,137 “stars” on github.com. To evaluate the benefits of this library, consider what kind of functionality you need to provide for the operation of a universal web application.

Server rendering


Libraries such as react.js, vue.js, angular.js, riot.js, and others - support server-side rendering. Server rendering works, as a rule, synchronously. This means that asynchronous API requests in life cycle events will be launched for execution, but their result will be lost. (Limited support for asynchronous server rendering is provided by riot.js)

Asynchronous data loading


In order for the results of asynchronous requests to be received prior to the server rendering, Next.js implements a special type of the page component, which has an asynchronous static async getInitialProps ({req}) life cycle event.

Passing server component state to client


As a result of server-side rendering of the component, an HTML document is sent to the client, but the state of the component is lost. To transfer the status of a component, usually the web server generates a script for the web browser that writes the state of the server component to a global JavaScript variable.

Creating a component on the side of a web browser and linking it to an HTML document


The HTML document that is obtained as a result of server-side rendering of the component contains text and does not contain components (JavaScript objects). Components must be recreated in a web browser and “linked” to the document without being rendered. In react.js, the hydrate () method is executed for this. A method similar in function is in the library vue.js.

Routing


Routing on the server and on the client should also be universal. That is, the same definition of routing should work for both server and client code.

Code splitting


For each page, only the necessary JavaScript code should be loaded, not the entire application. When moving to the next page, the missing code should be reloaded - without reloading the same modules, and without unnecessary modules.

All these tasks are successfully solved by the Next.js library. This library is based on a very simple idea. It is proposed to introduce a new type of component - “page”, in which there is the async static method async getInitialProps ({req}). The “page” component is a normal React component. This type of component can be thought of as a new type in the series: “component”, “container”, “page”.

Working example


For work we need node.js and the package manager npm. If they are not already installed - the easiest way to do this is using nvm (Node Version Manager), which is installed from the command line and does not require sudo access:

curl -o- https://raw.githubusercontent.com/creationix/nvm/v0.33.8/install.sh | bash 

After installation, be sure to close and reopen the terminal in order to set the PATH environment variable. The list of all available versions displays the command:

 nvm ls-remote 

Download the required version of node.js and the compatible version of the npm package manager with the command:

 nvm install 8.9.4 

Create a new directory (folder) and execute the command in it:

 npm init 

As a result, the package.json file will be generated.
Download and add the necessary packages for the project dependencies:

 npm install --save axios next next-redux-wrapper react react-dom react-redux redux redux-logger 

In the project root directory, create the pages directory. This directory will contain components of the “page” type. The path to the files inside the pages directory corresponds to the url by which these components will be available. As usual, the “magic name” index.js is mapped to url / index and / . More complex rules for url with wildcard are also realizable.

Create the pages / index.js file :

 import React from 'react' export default class extends React.Component { static async getInitialProps({ req }) { const userAgent = req ? req.headers['user-agent'] : navigator.userAgent return { userAgent } } render() { return ( <div> Hello World {this.props.userAgent} </div> ) } } 

This simple component uses the main features of Next.js:


In the package.json file, add three commands to the “scripts” attribute:

 "scripts": { "dev": "next", "build": "next build", "start": "next start" } 

Start the developer server with the command:

 npm run dev 

To implement a transition to another page without loading the page from the server, the links are wrapped in a special Link component. Add a dependency to page / index.js :

 import Link from 'next/link' 

and Link component:

 <Link href="/time"> <a>Click me</a> </Link> 

When clicking on a link, a page with 404 errors will be displayed.

Copy the pages / index.js file to the pages / time.js file . In the new component time.js, we will display the current time received asynchronously from the server. In the meantime, change the link in this component so that it leads to the main page:

 <Link href="/"> <a>Back</a> </Link> 

Try several times to reload each of the pages from the server, and then go from one page to another and go back. In all cases, the download from the server will take place with the server rendering, and all subsequent transitions - by means of rendering on the side of the web browser.

On the pages / time.js page we place a timer that shows the current time received from the server. This will allow you to get acquainted with asynchronous data loading during server rendering - something that benefits Next.js from other libraries.

To store data in the store , we use redux . Asynchronous actions in redux are performed using middleware redux-thunk . Usually (but not always), one asynchronous action has three states: START, SUCCESS FAILURE . Therefore, the code for defining asynchronous actions often looks (at least for me) complicated. In one issue of the redux-thunk library, a simplified version of middleware was discussed, which allows defining all three states on a single line. Unfortunately, this option has not been designed in the library, so we will include it in our project as a module.

Create a new directory redux in the root directory of the application, and in it - the file redux / promisedMiddlewate.js :

 export default (...args) => ({ dispatch, getState }) => (next) => (action) => { const { promise, promised, types, ...rest } = action; if (!promised) { return next(action); } if (typeof promise !== 'undefined') { throw new Error('In promised middleware you mast not use "action"."promise"'); } if (typeof promised !== 'function') { throw new Error('In promised middleware type of "action"."promised" must be "function"'); } const [REQUEST, SUCCESS, FAILURE] = types; next({ ...rest, type: REQUEST }); action.promise = promised() .then( data => next({ ...rest, data, type: SUCCESS }), ).catch( error => next({ ...rest, error, type: FAILURE }) ); }; 

A few explanations for this feature. The midleware function in redux has a signature (store) => (next) => (action) . An indicator of the fact that an asynchronous action should be handled by this function is the promised property. If this property is not defined, then processing is completed and control is passed to the following middleware: return next (action) . The action.promise property retains a reference to the Promise object, which allows you to “hold” the asynchronous static async getInitialProps ({req, store}) function until the asynchronous action is completed.

All that is connected with the data storage we put in the file redux / store.js :

 import { createStore, applyMiddleware } from 'redux'; import logger from 'redux-logger'; import axios from 'axios'; import promisedMiddleware from './promisedMiddleware'; const promised = promisedMiddleware(axios); export const initStore = (initialState = {}) => { const store = createStore(reducer, {...initialState}, applyMiddleware(promised, logger)); store.dispatchPromised = function(action) { this.dispatch(action); return action.promise; } return store; } export function getTime(){ return { promised: () => axios.get('http://time.jsontest.com/'), types: ['START', 'SUCCESS', 'FAILURE'], }; } export const reducer = (state = {}, action) => { switch (action.type) { case 'START': return state case 'SUCCESS': return {...state, ...action.data.data} case 'FAILURE': return Object.assign({}, state, {error: true} ) default: return state } } 

The getTime () action will be processed promisedMiddleware () . To do this, the promised property has a function that returns Promise , and the types property has an array of three elements containing the constants 'START', 'SUCCESS', and 'FAILURE' . The values ​​of the constants can be arbitrary, their order in the list is important.

It now remains to apply these actions in the pages / time.js component :

 import React from 'react'; import {bindActionCreators} from 'redux'; import Link from 'next/link'; import { initStore, getTime } from '../redux/store'; import withRedux from 'next-redux-wrapper'; function mapStateToProps(state) { return state; } function mapDispatchToProps(dispatch) { return { getTime: bindActionCreators(getTime, dispatch), }; } class Page extends React.Component { static async getInitialProps({ req, store }) { await store.dispatchPromised(getTime()); return; } componentDidMount() { this.intervalHandle = setInterval(() => this.props.getTime(), 3000); } componentWillUnmount() { clearInterval(this.intervalHandle); } render() { return ( <div> <div>{this.props.time}</div> <div> <Link href="/"> <a>Return</a> </Link> </div> </div> ) } } export default withRedux(initStore, mapStateToProps, mapDispatchToProps)(Page); 

I pay attention that, here the method withRedux () from the library next-redux-wrapper is used . All other libraries are common to react.js and do not require adaptation to Next.js.

When I first got acquainted with the Next.js library, it didn’t impress me very much due to the rather primitive out-of-the-box routing. It seemed to me that the applicability of this library is not above business cards. Now I don’t think so, and planned in the same article to talk about the next-routes library, which significantly expands the possibilities of routing. But now I understand that this material is better to put in a separate post. And there are plans to talk about the react-i18next library , which is attention! - has no direct relation to Next.js, but it is very well suited for sharing.

apapacy@gmail.com
January 14, 2018

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


All Articles