Loading...

Angular universal ngrx/store transfer state from server to browser

What is SSR

The new generation frameworks, rather it's react or angular, are universal, meaning there code can run both on the browser and by a node engine.
This means that if we know what we are doing, we can build our code to run on both ends as well, which means our users will get a fully rendered HTML page before there browser starts to kick in.
The result of setting your app to be rendered in the server side means fast initial loading time as well as superior SEO.
This article will guide you on how to create a server side to run your angular using ExpressJS and then install ngrx/store to use redux in our app.
Our goal is to manage to transfer our redux state from the server side to the client side.

New project

I will be using @angular/cli version 6, so let's start by creating a new project and a server for the project.

        
            > new new ssr-redux
            > g g universal second-app-universal --clientProject ssr-redux
        
    

Angular doesn't make assumptions regarding your server side and it leaves the main.server.ts file as the file you will fill the blanks and will run your server.
So let's modify that file to work with ExpressJs.

Start ExpressJS app

Angular doesn't know which server side you will run, so in this case we want to use express so we will install the package that connects express to angular.
The package is called: @nguniversal/express-engine.
At the time of writing @nguniversal released server side adaptors for: express, aspnetcore, and hapi.
Install the express engine package with npm:

        
            npm install @nguniversal/express-engine --save
        
    

Let's start by installing express and starting a new express app

        
            > npm install express --save
        
    

The express-engine package contains ngExpressEngine which is a templating engine, that will add a render method to the response and when given a template will replace the angular selectors with the actual HTML of the component.
We will also need to use the static middleware to serve the static files from the client build folder. So your server code might look like this

        
            import 'zone.js/dist/zone-node';
            import 'reflect-metadata';
            import { enableProdMode } from '@angular/core';
            import { environment } from './environments/environment';
            import {AppServerModule} from './app/app.server.module';
            import {ngExpressEngine} from '@nguniversal/express-engine';
            import * as express from 'express';
            import * as path from 'path';

            if (environment.production) {
              enableProdMode();
            }

            const app = express();

            app.engine('html', ngExpressEngine({
              bootstrap: AppServerModule
            }));

            app.set('view engine', 'html');
            const clientDistFolder = path.resolve(__dirname, '../second-app');
            app.set('views', clientDistFolder);

            app.get('*.*', express.static(clientDistFolder));

            app.get('*', function(req, res) {
              res.render('index', {
                req
              });
            });

            app.listen(3000, function() {
              console.log('our server is now listening on port 3000');
            });
        
    

So our code sets the template engine as ngExpressEngine which is set to bootstrap our server module.
This will expose a render method in the response and just call it pointing to the index.html we built with the client project, passing along the request as well.

Building our server

to build our server you can run:

        
            > ng run second-app:server
        
    

you will also need to build the client app to produce the index.html file as well as all the static files needed.
So you will need to run also:

        
            > ng build
        
    

Try and run your server main.js file with the following command:

        
            > node dist/second-app-universal/main.js
        
    

This should run your server on port 3000 and you should be able to view your app on the browser.

Passing the state from server to client

This use case will almost always repeat in a server rendered angular app using redux.
Let's think of way it could happen:
Your code will first run on the server, if for example you are querying your server for a certain resource and saving that in the state, there is no need to do the same request in the client.
We will load a hell of a lot faster if the browser will already have the same state as the server.
For this case it is very common to want to pass that state to the browser.
But let's first start by adding @ngrx/store

@ngrx/store

Install @ngrx/store with npm:

        
            npm install @ngrx/store --save
        
    

Let's first create our reducer.
our state will have a simple counter, initiated by default to 0, our server will initiate it to 1 and we will expect to see 1 in the browser and not the initial state.
In the app folder create a folder called redux and in it create a file called reducer.ts with the following code:

        
            import { Action } from '@ngrx/store'

            export const INCREMENT = 'INCREMENT';

            const initialState = 0;

            export function countReducer(state: number = initialState, action: Action) {
              switch (action.type) {
                case INCREMENT:
                  return state + 1;
                default:
                  return state
              }
            }
        
    

Let's create our store, modify the app.module.ts

        
            import { import { BrowserModule } from '@angular/platform-browser';
            import { NgModule } from '@angular/core';

            import { AppComponent } from './app.component';
            import {StoreModule} from "@ngrx/store";
            import {countReducer} from "./redux/reducer";

            @NgModule({
              declarations: [
                AppComponent
              ],
              imports: [
                BrowserModule.withServerTransition({ appId: 'serverApp' }),
                StoreModule.forRoot({count: countReducer})
              ],
              providers: [],
              bootstrap: [AppComponent]
            })
            export class AppModule { }
        
    

notice that we added the StoreModule.forRoot to the imports array, the second argument can contain the initial state but we left it empty for the time being.
Let's display the counter on the screen. In the app.component.ts let's grab the counter from the state and display it

        
            import { Component } from '@angular/core';
            import {select, Store} from "@ngrx/store";
            import {Observable} from "rxjs/index";

            @Component({
              selector: 'app-root',
              templateUrl: './app.component.html',
              styleUrls: ['./app.component.css']
            })
            export class AppComponent {
              title = 'app';
              public count$: Observable<number>;

              constructor(private store: Store<any>) {
                this.count$ = store.pipe(select('count'));
              }
            }
        
    

You can display the count in the template by modifying the app.component.html to:

        
            The count is: {{count$ | async}}
        
    
Transfer state

So let's say our app component has OnInit function, where we call our rest server for data. We will mimic the call with a set timeout that increases the counter, our init will only be on the server side.
so our server side will increase the counter and we expect the client side to have a counter increased and not zero. Modify the app.component.ts

        
            import {Component, Inject, OnInit, PLATFORM_ID} from '@angular/core';
            import {select, Store} from "@ngrx/store";
            import {Observable} from "rxjs/index";
            import {isPlatformServer} from "@angular/common";
            import {INCREMENT} from "./redux/reducer";

            @Component({
              selector: 'app-root',
              templateUrl: './app.component.html',
              styleUrls: ['./app.component.css']
            })
            export class AppComponent implements OnInit{
              title = 'app';
              public count$: Observable;

              constructor(private store: Store, @Inject(PLATFORM_ID) private _platformId) {
                this.count$ = store.pipe(select('count'));
              }

              ngOnInit() {
                if (isPlatformServer(this._platformId)) {
                  setTimeout(() => {
                    this.store.dispatch({type: INCREMENT});
                  }, 1000);
                }
              }
            }
        
    

We are increasing the count in the state only in the server side, if you launch your app you will see in the browser the count is still zero.
If you run the app with the SSR you can examine the view page source to see what the server is sending as the HTML and you will see the server is sending a count of 1

So how can we transfer the state to the browser?
The most fitting way here is to place a script tag in the beginning of the body tag and initiate the window with global variable with the state.
to do this we will have to grab the HTML before sending it to the browser and add our script.
To modify the HTML string like jquery in the server side we can install cheerio

        
            < npm install cheerio --save
        
    

and in your main.server.ts modify the following

        
            import 'zone.js/dist/zone-node';
            import 'reflect-metadata';
            import { enableProdMode, InjectionToken } from '@angular/core';
            import { environment } from './environments/environment';
            import {AppServerModule} from './app/app.server.module';
            import {ngExpressEngine} from '@nguniversal/express-engine';
            import * as express from 'express';
            import * as path from 'path';
            import {STATE_CB} from "./app/tokens";
            import * as cheerio from 'cheerio';

            if (environment.production) {
              enableProdMode();
            }

            const app = express();

            app.engine('html', ngExpressEngine({
              bootstrap: AppServerModule
            }));

            app.set('view engine', 'html');
            const clientDistFolder = path.resolve(__dirname, '../second-app');
            app.set('views', clientDistFolder);

            app.get('*.*', express.static(clientDistFolder));

            app.get('*', function(req, res) {
              let saveState;
              const cb = (state) => {
                saveState = state;
              }

              res.render('index', {
                req,
                providers: [
                  {provide: STATE_CB, useValue: cb}
                ]
              }, (err: Error, html: string) => {
                const $ = cheerio.load(html);
                $('body').prepend('<script>window.__STATE__ = ' + JSON.stringify(saveState)</script>');
                res.status($.html() ? 200 : 500).send($.html() || err.message);
              });
            });

            app.listen(3000, function() {
              console.log('our server is now listening on port 3000');
            });
        
    

We are doing something intresting here, sincewe have access to the full HTML only in the express part, we can implement the hook of the render method that get's called with the HTML created and we are using cheerio to place the state after the body.
How do we know the state? Well we are providing a service which is a callback that will be injected and called in the angular part with the new state.
How will our angular app.component.ts will look like?

        
            import {Component, Inject, OnInit, Optional, PLATFORM_ID} from '@angular/core';
            import {select, Store} from "@ngrx/store";
            import {Observable} from "rxjs/index";
            import {isPlatformServer} from "@angular/common";
            import {INCREMENT} from "./redux/reducer";
            import {STATE_CB} from "./tokens";

            @Component({
              selector: 'app-root',
              templateUrl: './app.component.html',
              styleUrls: ['./app.component.css']
            })
            export class AppComponent implements OnInit{
              title = 'app';
              public count$: Observable<number>;

              constructor(
                private store: Store<any>,
                @Inject(PLATFORM_ID) private _platformId,
                @Optional() @Inject(STATE_CB) private _stateCb) {
                this.count$ = store.pipe(select('count'));
              }

              ngOnInit() {
                if (isPlatformServer(this._platformId)) {
                  setTimeout(() => {
                    this.store.dispatch({type: INCREMENT});
                  }, 1000);

                  this.store.subscribe((state) => {
                    this._stateCb(state);
                  })
                }
              }
            }
        
    

We can subscribe to state change and this means that all the state changes will modify the service we pass from express.
Notice also that we are injecting the service from express with the optional decorator, we are doing this cause this service won't be available for us in the browser.

we need to modify the app.module.ts to initialize with the the state on the browser

        
            import { BrowserModule } from '@angular/platform-browser';
            import { NgModule } from '@angular/core';

            import { AppComponent } from './app.component';
            import {StoreModule} from "@ngrx/store";
            import {countReducer} from "./redux/reducer";
            import {getInitialState} from "./tokens";

            @NgModule({
              declarations: [
                AppComponent
              ],
              imports: [
                BrowserModule.withServerTransition({ appId: 'serverApp' }),
                StoreModule.forRoot({count: countReducer}, {
                  initialState: getInitialState
                })
              ],
              providers: [],
              bootstrap: [AppComponent]
            })
            export class AppModule { }
        
    

create a file in the app folder called tokens.ts with the token to inject the provider from our express to the angular universal and also to create the function that creates the initial state.

        
            import { InjectionToken } from '@angular/core';

            declare var window: any;

            export const STATE_CB = new InjectionToken('to provide the state cb');

            export function getInitialState() {
              if (typeof window !== 'undefined') {
                return window.__STATE__;
              } else {
                return {counter: 0};
              }
            }
        
    

try and build the client and the universal app, if all went well you should see the browser count is 1 just like the server

Summary

Don't initiate your state from the server and the browser, you will double the time it will take you to do the initalization process, just transfer the state from the server to the browser using the window.