📜 ⬆️ ⬇️

Create an isomorphic / universal application on Next.JS + Redux

This is the second article on Server Side Rendering and Isomorphic / Universal React applications. The first one titled “ Simplifying the universal / isomorphic application on React + Router + Redux + Express ” was more about a custom solution, the same article is aimed more at those who do not want to bother, but want a ready-made solution, with the community, and generally less headaches with setup, debugging, selection of libraries, etc.


+


In this article we will consider Next.JS , which has advantages in the form of lack of configuration, server rendering and a ready ecosystem.


Out of the box, Next.JS does not know how to work with Redux, so in the process of writing a trial project, I selected the resulting common code into a separate repository next-redux-wrapper , with which in this article we will build an example application on Next.JS + Redux.


What is it all about


For newcomers, I remind you that the essence of server rendering is quite simple: on the server we need to determine, based on the rules of the router, which component will be shown on the page, find out what data it needs to work, request this data, render HTML, and send this HTML along with data to the client.


Nowadays, solutions such as the Create React App , which postulate the "configuration zero" approach, are rapidly gaining popularity, everything is set up by one team, it works right out of the box, but contains a lot of restrictions, and it does not have server rendering at all. You can read my article on this topic: What to take as a basis React applications .


For this there are alternatives like Next.JS and Electrode . Why not take them and forget about the torment? Or do not forget? Maybe things will only get worse. But in fact, it all depends on the task, and in order to quickly slap the application there is usually enough flexibility, but here are some limitations to keep in mind when starting work with Next.JS:


  1. It does not have a React Router, routing out of the box is very stupid, and even the path with substitutions of the form /path/:id/:foo does not know how (there are separate solutions for this), but at least there is support for the query .
  2. Does not support importing CSS / LESS / SASS, etc., instead there is CSS in JSX, but you can add styles directly to the document, but then their Hot Reload will not work.
  3. The configuration of the Webpack (which is used internally), although it can be changed manually, but the addition of Loaders is not strongly recommended.

Next.js life cycle


In the process of rendering pages, the mini-router takes the corresponding file from the ./pages directory, takes default export from it and uses the static getInitialProps method from the exported component in order to throw these props into it. The method can be asynchronous, i.e. return Promise . This method is called both on the server and on the client, the only difference is in the number of arguments received, for example, there is a req on the server (which is a NodeJS Request). Both the client and server get the normalized pathname and query .


The code for the page looks like this:


 export default class Page extends Component { getInitialProps({pathname, query}) { return {custom: 'custom'}; //     props   } render() { return ( <div> <div>Prop from getInitialProps {this.props.custom}</div> </div> ) } } 

Or in a functional style:


 const Page = ({custom}) => ( <div> <div>Prop from getInitialProps {this.props.custom}</div> </div> ); Page.getInitialProps = ({pathname, query}) => ({ custom: 'custom' }); export default Page; 

The static getInitialProps method getInitialProps ideally suited as a place where we can otdatcatch some actions to bring the Redux Store in the right state, so that then during the rendering the page can read all the necessary data from there.


 getInitialProps({store, pathname, query}) { // component will read it from store's state when rendered store.dispatch({type: 'FOO', payload: 'foo'}); // pass some custom props to component return {custom: 'custom'}; } 

But in order for this to work, the Store needs to be created somewhere, both on the server and on the client, and on the Store client, as a rule, always one, and on the server it must be created for each request separately.


In addition to the getInitialProps argument getInitialProps the same Store should be provided to Redux Provider so that all the components nested in it can access the Store.


The Higher Order Components concept is best suited for this purpose. In two words, it is a function that takes a component as an argument and returns its wrapped version, which is also a component, for example, React Router withRouter(Cmp) . Sometimes a function can take arguments and return another function that the component already accepts; this is called currying, for example React Redux connect(mapStateToProps, mapDispathToProps)(Cmp) . The wrapper that we are going to use must be applied to all pages in order to be sure that all the initial conditions are always the same.


Create application


First, put all the packages:


 npm install next-redux-wrapper next@^2.0.0-beta redux react-redux --save 

Create the things needed for Redux:


 import React, {Component} from "react"; import {createStore} from "redux"; const reducer = (state = {foo: ''}, action) => { switch (action.type) { case 'FOO': return {...state, foo: action.payload}; default: return state } }; const makeStore = (initialState) => { return createStore(reducer, initialState); }; 

Now wrap the page in next-redux-wrapper :


 import withRedux from "next-redux-wrapper"; const Page = ({foo, custom}) => ( <div> <div>Prop from Redux {foo}</div> <div>Prop from getInitialProps {custom}</div> </div> ); Page.getInitialProps = ({store, isServer, pathname, query}) => { store.dispatch({type: 'FOO', payload: 'foo'}); return {custom: 'custom'}; }; //    makeStore     Page = withRedux(makeStore, (state) => ({foo: state.foo}))(Page); export default Page; 

You can also dispute Promise , in which case you will need to wait for its completion and even then return the initial props.


That's all. All the magic happens inside the wrapper, and from the outside we see a clean and beautiful implementation. You can see the full example in the Next.js repository: https://github.com/zeit/next.js/blob/master/examples/with-redux/README.md or look at the example in the wrapper repository: https: // github .com / kirill-konshin / next-redux-wrapper / blob / master / pages / index.js .


To start, just write in the console:


 node_modules/.bin/next 

PS


I found one unpleasant moment, if you want to use pages/_document.js (which allows you to build a template for the entire page) and dispatch actions from its getInitialProps , as well as from the destination page, then race condition may occur. The order of calling these functions is not particularly controlled and it may happen that they will work in parallel, therefore at the moments when the page template is rendered and the page itself the state may be different.


The wrapper itself supports this script, because it stores the Store in the request itself, and also ensures that there is always one on the Store client. However, the authors of Next.js say that it is a bad practice to have disputes in _document.js . They propose instead to write another HOC, already a custom one, and do everything there, but this remains outside the scope of the article.


')

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


All Articles