Nextjs is a popular SSR (server Side render) framework in the React ecosystem. It takes only a few steps to build a project that supports SSR (see next.js for a quick setup). The sample code for this article comes from the front-end standard template project.

The server organizes data

Nextjs

GetInitialProps () provides a convenient and powerful server-side rendering function that makes it easy to process asynchronous request data for both the server and the front end:

const load = async () =>{
    return new Promise((res, rej)=>{
        res('Success')
    })
}
class Simple extends React.Component{
    static async getInitialProps({req, query}) {
        const data = await load();
        return {data}
    }
    render() {
        return(<p>{this.props.data}</p>)
    }
}
Copy the code

One of the strengths of Next was that in just a few lines of code, it solved the most troublesome aspect of SSR’s asynchronous data assembly, the front and back end. No matter how complex the asynchronous data assembly process is, you can put Promise objects in your code.

Pages and inside pages

There are two concepts that need to be reinforced before continuing with this article — inside pages and pages.

Content retrieved by entering an address in the browser is called a page.

In a single page application, there will also be content switching effects controlled by the navigation bar or menu, which we call inner pages. In a single page application, a page is opened first, and then the effect of page switching is simulated by adding, deleting and modifying Dom.

Limitations of SSR rendering in Nextjs

The getInitialProps() method is powerful, but there is one problem right now — it can only be used in “inside pages.”

Nextjs_ specifies all files placed in./pages (usually *.js_ files, which can also be imported

.ts* files) are treated as an inside page, and the React component exported from these files can be accessed directly from the input address. For example, there is now [

./pages/about.js

] (github.com/palmg/websi…

Nextjs

After browsing http://localhost:3000/about you can see the component, and [

./pages/async/simple.js

] (github.com/palmg/websi…

But components in other paths (such as./component) cannot use the getInitialProps() method. At first glance this may not seem like a problem, but some applications require these components not to be placed in./pages exposed to _urL_ and need to load data asynchronously. Look at the following example.

Examples of loading menus on demand

As shown above. In enterprise-class applications (such as OA systems), SSR implementation is usually not necessary. In this case, menus can be loaded asynchronously in the component’s componentDidMount() method based on role permissions, but in some cases (such as a menu-configurable content site, Or server side cache for enterprise-class applications) there will also be the need for asynchronous menu loading and SSR implementation, which needs to be extended on the basis of _Nextjs_ framework.

Looking at this, you might think you could put the menu assembly in the getInitialProps() method of each inner page as follows:

const Comp = props =>(<div><Menus menus={props.menus}/><div>{props.pageData}</div></div>);
Comp.getInitialProps = async ({req})=>{
    //load Menu Promise
    const menus = await getMenus();
    //load Page Data Promise
    const pageData = await getPageData();
    return {menus, pageData}
}
Copy the code

This is implementationally fine, but architecturally bad. Here are three reasons:

  1. React has been described in various ways, such as one-way data flow, componentization, and so on. But his core idea is thatDivide and conquer. Back when Jquery “ruled” you could use _selector_(e.g$('#id')Easy access to any element on the page. If a project is not well standardized management (long-term manual standardized management needs to invest a lot of costs), over time, it will find that the coupling between various plates is becoming stronger and more and more pits (code decay). However, the one-way data flow of React makes there is no direct communication between components, so the standardization is strengthened from the technical level, which leads to _Redux_ and _Flux_ controlling the communication between modules in accordance with the “sub-total sub-point” mode (actually a message bus mode). So it doesn’t make sense to have pages and menus that don’t have strong business logic dependencies in one place.
  2. The vast majority of projects are not developed by one person, and an architect needs to take into account the uneven level of future developers involved in the project. If the frame-level structure is exposed to the business developer, there is no guarantee that some business developer has overlooked or modified some code to cause a frame-level pit.
  3. Following the code above, virtually every inside page is required to remainconst menus = await getMenus();,<Menus menus={props.menus}/>This type of code (copy and paste each inside page). This is called “boilerplate code” in architecture, and the architect should try to “layer” the code into one place.

So it makes sense to implement asynchronous SSR data loading for components other than./pages of _Nextjs_.

Component SSR asynchronous data implementation

In order to implement the requirements of this article for all components to implement methods similar to getInitialProps(), we first need to sort out the rendering process for the _Nextjs_ front and back ends.

Rendering process

_Nextjs_ provides the user with./pages/_app.js and./pages/_document.js to perform certain tasks before the inner page processing, which is used to build the structure of the entire HTML. And./pages/_document.js will only be executed on the server. This article calls the developer-implemented inner page _page, and now there are three types of builds for _Nextjs_ — _

document

, _

App_ and _component

, each build can contain static getInitialProps(), constructor(), and Render () methods, which are executed as follows.

The server side executes the procedure

  1. _document getInitialProps()
  2. _app getInitialProps()
  3. _page getInitialProps()
  4. _app constructor()
  5. _app render()
  6. _page constructor()
  7. _page render()
  8. _document constructor()
  9. _document render()

The above process is decomposed as follows:

  1. Assemble asynchronous data (1 to 3) : The server will start the static method _document.getInitialprops (), which will execute _app.getInitialprops () and iterate over all the _Page.getInitialprops () until all the asynchronous data is assembled.

  2. Render React component (4~7) : Once you have the data, start rendering the page, which uses the ReactDOMServer execution to generate an HTML string.

  3. Build static HTML (8~9) : With the strings generated by the ReactDOMServer, all that remains is to assemble them into a standard HTML document and return it to the client.

The client executes the procedure

When initializing the page (opening the page for the first time) :

  1. _app constructor()
  2. _app render()
  3. _page constructor()
  4. _page render()

When the client first opens the page (or refreshes the page), the server already provides a complete HTML document that can be displayed immediately. The React component still performs a virtual Dom rendering, so all components will do it. Then _Nextjs_ prevents the virtual Dom from rendering the real Dom using a mechanism similar to _checksum_ rendered by the _React_ server.

When the inside page jumps (throughnext/linkJump) :

  1. _app getInitialProps()
  2. _page getInitialProps()
  3. _app render()
  4. _page constructor()
  5. _page render()

The client jumping to a new inner page has nothing to do with the server rendering. GetInitialProps () for __app and _page_ assembles the data, and passes the assembled data to the component to render via props. Note that the _app constructor is not executed when the inner page jumps because it is only instantiated once when the entire page is rendered.

implementation

Once you know the _Nextjs_ solution execution, it’s easy to implement the requirements — to assemble the data using the getInitialProps() method of _document or _app, and then pass the data to the corresponding component. Of course, according to the idea of divide-and-conquer, business can not be done directly in the framework. It is necessary to provide a registration interface for components and then _document or _app use the registration method to build business data.

Data loading method registration

The first thing we need to do is provide an interface for our component to register asynchronously loading data. The component can use this interface to register asynchronously loading data methods so that the framework can uniformly execute getInitialProps(). . / util/serverInitProps. Js provides this function:

const FooDict = {}; Export const registerAsyncFoo = (key, foo, params = {}) => {FooDict[key] = {foo, params}; // Register method export const registerAsyncFoo = (key, foo, params = {}) => {FooDict[key] = {foo, params}; }; Export const executeAsyncFoo = async () => {const valueDict = {}; const keys = Object.keys(FooDict); for (let key of keys) { const dict = FooDict[key]; valueDict[key] = await dict.foo(dict.params); } return valueDict; };Copy the code

Then we register the method to get data asynchronously in the Menu component:

registerAsyncFoo('menus', getMenus);
Copy the code

GetMenus simulates the process of getting data asynchronously:

import {Menus} from ".. /.. /.. /.. /data/menuData"; Export const getMenus = () => {return new promise ((resolve, const getMenus = ()) => { reject) => { resolve(Menus) }) };Copy the code

After registration, perform asynchronous loading in _app:

import {executeAsyncFoo} from ".. /util/serverInitProps"; class ExpressApp extends App { static async getInitialProps({Component, router, ctx}) { info('Execute _App getInitialProps()! ', 'executeReport'); /** * the app's getInitialProps is called once on the server and every time the front end switches pages. */ let pageProps = {}, appProps = {}; if (Component.getInitialProps) { pageProps = await Component.getInitialProps(ctx); } if (ctx && ! Ctx.req) {// The client is performing appProps = window.__next_data__. } else {// The server is performing appProps = await executeAsyncFoo(); } return {pageProps, appProps} } //other function }Copy the code

_Nextjs_ writes the data to the HTML window.__next_data__ object, from which the client can retrieve the data that has been loaded on the server. React Context is used to pass data. Components that need to use the React Context can retrieve the data from the ApplicationContext:

//_app import ApplicationContext from '.. /components/app/applicationContext' class ExpressApp extends App { //other function render() { info('Execute _App render()! ', 'executeReport'); const {Component, pageProps, appProps} = this.props; return ( <ApplicationContext.Provider value={appProps}> <Application> <Component {... pageProps} /> </Application> </ApplicationContext.Provider> ) } //other function } //menu import ApplicationContext from '.. /applicationContext' const Menu = props => { return ( <ApplicationContext.Consumer> {appProps => { const {menus} = appProps; return menus.map(menu => ( <Link href={menu.href}> <a>{menu.name}</a> </Link> )) }} </ApplicationContext.Consumer> ); };Copy the code

. / util/serverInitProps. Js can be used in any component, _app can be carried by method to get the data in the manner of kev – the value set to the ApplicationContext, All any component has to do is get the target data from the ApplicationContext.

Of course, the way to transfer data is not limited to the React Context feature. Redux or global data management methods are also possible.

Originally reprinted from @sunflower walking in the wind