Sunday, December 06, 2015

Aurelia - managing global state

One of the things I love about writing client-side web sites is that no longer do we have to write stateless applications, or leverage hidden tags to pass state back and forth to the server. State can live on the client, persisting as long as the user keeps the browser window open. As the user navigates around, nothing gets lost since in reality, the whole web site is one page, and the data associated with that page persists until the user closes it.

There is one small problem with this scenario: if the user refreshes the page (or BrowserSync does so), then this causes a new load of the page. In Aurelia, this  will cause new singletons to be created for items in the DI system. If I am using the DI container to hold my singletons, then I will lose the user's data. The site will likely redirect the user to the login page, which is not likely the experience the user expects. How do we resolve this? In the past, we would place the data into a cookie, but modern browsers offer a better alternative: sessionStorage.

My pattern is to place objects that I want to have persist throughout a session into sessionStorage, and use that as a fallback if suddenly the object is not available (as would happen after a browser refresh). That comes out something like this:

  class GlobalState {
    constructor() {
      this._entity = null;
    }

    get entity {
      if (!this._entity) {
        let sessionEntity = sessionStorage._entity;
        if (sessionEntity) {
          this._entity = JSON.parse(sessionEntity);
        } else {
          this.entity = {};
        }
      }

      return this._entity;
    }

    set entity(value) {
      this._entity = value;
      sessionStorage.entity = JSON.stringify(value);
    }
  }

This way, every time I go to fetch the state, I verify its persistence, and return it. A couple of things to note: I use backing storage _entity to hold the value. The key name in sessionStorage is the same as the actual entity. Since sessionStorage has to be a string, we use JSON to stringify/parse it. Pay attention to the places where I use the backing sotrage vs. the actual entity. In the getter, I set the entity to a default value using the setter, rather than setting the backing value. This puts the default value into sessionStorage. In the case where the entity is simple to build, this may not be needed, but for consistency, I choose to go this way.

There might be a solution for this in Aurelia's DI container, if I find one, I'll post it later.

No comments: