Synchronize Vuex store with server side in Nuxt.js

3.2k Views Asked by At

Problem

Below Nuxt middleware

const inspectAuthentication: Middleware = async (): Promise<void> => {
  await AuthenticationService.getInstance().inspectAuthentication();
};

is being executed on the server side before return each page's HTML and checks has been user authenticated. If has been, it stores the CurrentAuthenticatedUser in Vuex module:

import {
  VuexModule,
  getModule as getVuexModule,
  Module as VuexModuleConfiguration,
  VuexAction,
  VuexMutation
} from "nuxt-property-decorator";


@VuexModuleConfiguration({
  name: "AuthenticationService",
  store,
  namespaced: true,
  stateFactory: true,
  dynamic: true
})
export default class AuthenticationService extends VuexModule {

  public static getInstance(): AuthenticationService {
    return getVuexModule(AuthenticationService);
  }


  private _currentAuthenticatedUser: CurrentAuthenticatedUser | null = null;

  public get currentAuthenticatedUser(): CurrentAuthenticatedUser | null {
    return this._currentAuthenticatedUser;
  }


  @VuexAction({ rawError: true })
  public async inspectAuthentication(): Promise<boolean> {

    // This condition is always falsy after page reloading
    if (this.isAuthenticationInspectionSuccessfullyComplete) {
      return isNotNull(this._currentAuthenticatedUser);
    }


    this.onAuthenticationInspectionStarted();

    // The is no local storage on server side; use @nuxtjs/universal-storage instead
    const accessToken: string | null = DependenciesInjector.universalStorageService.
        getItem(AuthenticationService.ACCESS_TOKEN_KEY_IN_LOCAL_STORAGE);

    if (isNull(accessToken)) {
      this.completeAuthenticationInspection();
      return false;
    }


    let currentAuthenticatedUser: CurrentAuthenticatedUser | null;

    try {

      currentAuthenticatedUser = await DependenciesInjector.gateways.authentication.getCurrentAuthenticatedUser(accessToken);

    } catch (error: unknown) {

      this.onAuthenticationInspectionFailed();
      // error wrapping / rethrowing
    }


    if (isNull(currentAuthenticatedUser)) {
      this.completeAuthenticationInspection();
      return false;
    }


    this.completeAuthenticationInspection(currentAuthenticatedUser);

    return true;
  }

  @VuexMutation
  private completeAuthenticationInspection(currentAuthenticatedUser?: CurrentAuthenticatedUser): void {

    if (isNotUndefined(currentAuthenticatedUser)) {
      this._currentAuthenticatedUser = currentAuthenticatedUser;
      DependenciesInjector.universalStorageService.setItem(
        AuthenticationService.ACCESS_TOKEN_KEY_IN_LOCAL_STORAGE, currentAuthenticatedUser.accessToken
      );
    }

    // ...
  }
}

Above code works fine on server side, but then, on the client side, if to try to get AuthenticationService.getInstance().currentAuthenticatedUser, it will be null! I expected that Nuxt.js synchronizes the Vuex store including AuthenticationService with server side, however, it does not.

Target

AuthenticationService must be synchronized with server side, so if user has been authenticated, in the client side AuthenticationService.getInstance().currentAuthenticatedUser it must be non-null even after page reloading.

There no need to synchronize whole Vuex store in server side (for example, the module responsible floating notification bar is required in the client side only) but if the selective methodology has not been developed, at least synchronizing of whole Vuex store will be enough for now.

Please don't recommend me the libraries or Nuxt modules for authentication like Nuxt Auth module because here we are talking about synchronizing of the Vuex store with server, not about best Nuxt modules for authentication. Also, the syncronizing of the vuex store between client and server could be used not just for authentication.

Update

preserveState solution attempt

Unfortunately,

import { store } from "~/Store";
import { VuexModule, Module as VuexModuleConfiguration } from "nuxt-property-decorator";


@VuexModuleConfiguration({
  name: "AuthenticationService",
  store,
  namespaced: true,
  stateFactory: true,
  dynamic: true,
  preserveState: true /* New */
})
export default class AuthenticationService extends VuexModule {}

causes

Cannot read property '_currentAuthenticatedUser' of undefined

error on the server side.

enter image description here

The error refers to

@VuexAction({ rawError: true })
public async inspectAuthentication(): Promise<boolean> {
  if (this.isAuthenticationInspectionSuccessfullyComplete) {
    // HERE ⇩
    return isNotNull(this._currentAuthenticatedUser);
  }
}

I checked this value. It's a big object; I'll leave the the noticable part only:

{                                                                                                                      
  store: Store {
    _committing: false,
    // === ✏ All actual action here
    _actions: [Object: null prototype] {
      'AuthenticationService/inspectAuthentication': [Array],
      'AuthenticationService/signIn': [Array],
      'AuthenticationService/applySignUp': [Array],
      // ...      

  // === ✏ Some mutations ...
  onAuthenticationInspectionStarted: [Function (anonymous)],
  completeAuthenticationInspection: [Function (anonymous)],
  // ...
  context: {
    dispatch: [Function (anonymous)],
    commit: [Function (anonymous)],
    getters: {
      currentAuthenticatedUser: [Getter],
      isAuthenticationInspectionSuccessfullyComplete: [Getter]
    },
    // === ✏ The state in undefined!
    state: undefined
  }
}

I suppose I need to tell how I initializing the vuex store. The working Nuxt methodology for dynamic modules is:

// store/index.ts
import Vue from "vue";
import Vuex, { Store } from "vuex";


Vue.use(Vuex);

export const store: Store<unknown> = new Vuex.Store<unknown>({});

nuxtServerInit solution attempt

Here is the another problem - how to integrate nuxtServerInit in above store initialization method? I suppose, to answer this question it's required the Vuex and vuex-module-decorators. In below store/index.ts, the nuxtServerInit even will not be called:

import Vue from "vue";
import Vuex, { Store } from "vuex";


Vue.use(Vuex);

export const store: Store<unknown> = new Vuex.Store<unknown>({
  actions: {
    nuxtServerInit(blackbox: unknown): void {
      console.log("----------------");
      console.log(blackbox);
    }
  }
});

I extracted this problem to other question.

1

There are 1 best solutions below

4
On BEST ANSWER

This is one of the main challenges when working with SSR. So there's a process called Hydration that happens on the client after receiving the response with static HTML from the server. (you could read more on this Vue SSR guide)

Because of the way Nuxt is built and how the SSR/Client relationship works for hydration, what might happen is your server rendering an snapshot of your app, but the async data not being available before the client mounts the app, causing it to render a different store state, breaking the hydration.

The fact frameworks like Nuxt and Next (for React) implement their own components for Auth, and many others, is to deal with the manual conciliation process for correct hydration.

So going deeper on how to fix that without using Nuxt built-in auth module, there are a few things you could be aware of:

  1. There the serverPrefetch method, that will be called on the Server-side which will wait until the promise is resolved before sending to the client to render it
  2. Besides the component rendering, there's the context sent by the server to the client, which can be injected using the rendered hook, that's called when the app finishes the rendering, so the right moment to send your store state back to the client to reuse it during hydration process
  3. On the store itself, if you're using registerModule, it supports an attribute preserveState, that's responsible for keeping the state injected by the server.

For the examples on how to work those parts, you could check the code on this page

Last, more related to your user auth challenge, another option would be to use nuxtServerInit on store actions to run this auth processing, since it'll be passed directly to the client afterwards, as described on Nuxt docs.

Updates

On the same page, the docs present that the first argument on the nextServerInit is the context, meaning you could get the store, for instance, from there.

Also one important point to mention, is that on your original question, you've mentioned that you don't want 3rd party libs, but you're already using one that brings a lot of complexity to the table, namely the nuxt-property-decorator. So not only you're dealing with SSR that's as complicated as it gets when using frameworks, but you're not using pure Vue, but Next, and not using pure TS Nuxt, but adding another complexity with decorators for the store.

Why am I mentioning it? Because taking a quick look on the lib issues, there are other people with the same issue of not accessing the this correctly.

Coming from a background of someone that used both Nuxt (Vue) and Next (React), my suggestion with you is to try to reduce the complexity, before trying out a lot of different stuff. So I'd test running your app without this nuxt-property-decorator to check if this works with the out-of-the-box store implementation, ensuring it's not a bug caused on the lib not fully prepared to support SSR complexity.