Smart Front-ends & Dumb Back-ends: Persisting State in AngularJS

Peter Bengtsson
Share

A state is something generated by you as you interact with a website — for example by clicking on a button, or typing some text into a text field. This state resides in your browser’s RAM and consists of JavaScript objects such as arrays, strings and objects.

In this article we’re going to dig into a pattern of taking these arrays and objects and persisting them. To do that, we’ll need some clever ways to separate the temporary “junk” from the really important stuff we can’t lose.

Getting Started

Let’s say that we have an array of objects and each object has “temporary” keys in it. We don’t want these keys to be persistent when sending the array of objects in for a persistent store (localStorage or Ajax).

In the example I’m going to show you, we’ll work with an AngularJS app. In it, we put things into each object that don’t need to be persistent. Everything that is not important to be persistent starts with an underscore.

Here’s the HTML code that we start with:

<div ng-repeat="thing in things track by thing.id"
     ng-click="thing._expanded=!thing._expanded">
    <div ng-if="thing._expanded">
        EXPANDED VIEW
    </div>
    <div ng-if="!thing._expanded">
        collapsed view
    </div>
</div>

and this is the small JavaScript code that powers it:

angular
  .module('app', [])
  .controller('Ctrl', ($scope) => {
    $scope.things = [
      {id: 1, key: 'Value'},
      {id: 2, key: 'Value2'},
      {id: 3, key: 'Value3'},
    ]
  });

If you want to see this example live, take a look at this demo.

See the Pen XXNdab by SitePoint (@SitePoint) on CodePen.

Pretty cute right? It’s really practical.

Thinking about Storing This Persistently

Let’s now suppose that we want to persist $scope.things. It might be a Todo list that we want to come back to after having closed the tab. And when we do come back, a lot of these shallow attributes (such as _expanded or _dateAsString for example) are things we don’t want to clutter up in the persistent store. We want to do that because they can easily be re-generated or reset without losing what’s valuable. Or perhaps we’re worried about memory and some of those temporary state items are heavy.

Let’s say we want to persist it in localStorage. What we need to do look like:

localStorage.setItem('mystuff', JSON.stringify($scope.things));

However, that’s going to save a bunch of other things in the objects that we don’t want to remember (again, because they can be regenerated or because they’re simply not worth saving).

So the first thing we need to do is to clone them. To clone an array, you have to write a JavaScript code like this (I’m using ECMAScript6 here):

let copy = Array.from(myArray)

But if the array is one full of objects (i.e. dictionaries), then we have to do a little bit more magic:

let copy = Array.from(
  myArray,
  (item) => Object.assign({}, item)
);

In this way, we get a deep copy.

And here is our chance to modify that copy so that it’s prepared for being saved persistently. Without further ado, here’s the code that does it:

let copy = Array.from(myArray, (item) => {
  let obj = Object.assign({}, item)
  for (let key of Object.keys(obj)) {
    if (key.startsWith('_') || key === '$$hashKey') {
      delete obj[key]
    }
  }
  return obj
});

If you want to see this example live, take a look at this demo.

What’s With That $$hashKey Thing?

AngularJS puts a key called $$hashKey into every object that is being rendered (if it believes it needs to). In this way it can internally know what has changed and what hasn’t. There’s a built-in utility in AngularJS that we can use to strip these:

angular.toJson(myObject);

You can find the documentation about it here.

If we do repeats like ng-repeat="thing in things", AngularJS will put those keys in. But to avoid it, we can use a tracking key like:

ng-repeat="thing in things track by thing.id"

And it won’t be put in.

Either way, since we’re probably building a utility tool that we can re-use for various AngularJS constructs, it might be a very good idea to strip those $$hashKey things.

Putting It Together

Our first demo app was rather silly because you never “enter” anything into the state, so there’s no point in saving it. First, let’s change our demo application so that it actually takes input that is worth saving. The application is going to be a “week log” where you type in what you did every day of the week.

Let’s just build it first so that we have the bones working. The following is the HTML code we need:

<div ng-app="app">
  <div ng-controller="Ctrl">
    
    <div ng-repeat="week in weeks track by week.date">
      <h3 ng-click="week._expanded=!week._expanded">
        Week of {{ week._date | date: 'EEEE MMM d' }} - {{ week._end | date: 'EEEE d, yyyy' }}
        <span ng-if="week._expanded">(click to close)</span>
        <span ng-if="!week._expanded">(click to edit)</span>
      </h3>
      <div ng-if="week._expanded" class="expanded">
        <table>
          <tr ng-repeat="day in week._days">
            <td>{{ day.name }}</td>
            <td><input type="text" ng-model="day.text" ng-blur="saveWeeks()"></td>
          </tr>
        </table>
      </div>
    <div ng-if="!week._expanded" class="collapsed">
      <table>
        <tr ng-repeat="day in week._days">
          <td><b>{{ day.name }}</b></td>
          <td>{{ day.text }}</td>
        </tr>
      </table>
    </div>
    </div>
    <hr>
    <p>
      Click to edit the week entries. After reloading the page you
      get back what you typed last.
    </p>
    <p>
      A useful extension of this would be to be able to add new weeks. And make it infinitely prettier. 
    </p>
  </div>
</div>

And this is the JavaScript code:

angular.module('app', [])
.controller('Ctrl', ($scope) => {
  const weekdays = [
    [0, 'Monday'],
    [1, 'Tuesday'],
    [2, 'Wednesday'],
    [3, 'Thursday'],
    [4, 'Friday'],
    [5, 'Saturday'],
    [6, 'Sunday'],
  ]
  let dressUp = (weeks) => {
    // add internally needed things
    weeks.forEach((week) => {
      week._date = Date.create(week.date)
      week._end = week._date.clone()
      week._end.addDays(6)
      week._days = [];
      weekdays.forEach((pair) => {
        week._days.push({
          index: pair[0],
          name: pair[1],
          text: week.days[pair[0]] || ''
        })
      })
    })
  }
  
  let dressDown = (weeks) => {
    // week.days is an object, turn it into an array
    weeks.forEach((week) => {
      week._days.forEach((day) => {
        week.days[day.index] = day.text || ''
      })
    })
  }
  
  // try to retrieve from persistent storage
  $scope.weeks = JSON.parse(
    localStorage.getItem('weeks') || '{"weeks":[]}'
  ).weeks
  if (!$scope.weeks.length) {
    // add a first default
    let monday = Date.create().beginningOfISOWeek().format('{yyyy}-{MM}-{dd}')
    $scope.weeks.push({date: monday, days: {}})
  }
  
  // when retrieved it doesn't have the internal 
  // stuff we need for rendering, so dress it up
  dressUp($scope.weeks)
  
  $scope.saveWeeks = () => {
    // copy from _days to days
    dressDown($scope.weeks)
    // make a deep copy clone
    let copy = Array.from($scope.weeks, (item) => {
      let obj = Object.assign({}, item)
      for (let key of Object.keys(obj)) {
        if (key.startsWith('_') || key === '$$hashKey') {
          delete obj[key]
        }
      }
      return obj
    })
    // actually save it persistently
    localStorage.setItem('weeks', JSON.stringify({weeks: copy}))
  }
});

A live demo is shown here.

See the Pen Weekly log by SitePoint (@SitePoint) on CodePen.

There’s a lot going on in there.

The demo app uses localStorage for persistency. Upon loading the app it retrieves past saved stuff and if there isn’t anything there, it creates a first sample. Then, it renders the weeks and you can edit the entries. As soon as you blur out of any input field, it starts the saving process. First it modifies the state a bit (copying from the list week._days to the object week.days) and secondly it creates a deep copy clone and when it does that, it strips out all the keys we don’t deem necessary to keep. Lastly, it stores it in localStorage.

Try opening the demo app, type something, blur out of the input fields and then refresh the whole page and you should see your entered data still being there.

Persistency and Beyond

Someone might laugh at the notion that localStorage in the browser is persistent. It’s only stored in your device and if you lose the device or completely wipe your profile that storage is gone. A more persistent solution is a proper database in the cloud. One that is backed up and replicated and whatnot. However, that’s outside the remit of this article and the extension to do that is really simple. Because we store JSON in localStorage, it means it’d be dead easy to Ajax send just the stuff we want to save and you don’t have to worry about columns or types.

Obviously, if you go down the Ajax route, saving the whole big thing on ever input field change would be potentially excessive. However, since each ng-blur knows which week and week day you edited, it would be possible to Ajax send just the change for that particular day. Exercise left to you, readers.

Conclusions

Is this realistic? Yes, it is! Does it scale? Yes, it does.

We’re entering an era where front-ends are getting smarter and back-ends are getting dumber. That means that you put much of the business logic in your front-end code and ask the back-end to just perform really basic tasks (like “Just store this blob!”). If you want to build one of those fancy offline-first apps, you need to start thinking of keeping all state in the webapp and you can’t depend on the server always being available. Eventually it has to be though.

Now you need to think of the back-end as a sync server. It doesn’t mean you have to send whole blobs of everything back and forth but the last thing you want to do is have complex business logic in both the front-end and the back-end and if you want that offline-first app experience, you have to lean towards the front-end for where the magic happens.

Also, putting the business logic into the front-end and thinking of the back-end as “dumb”, it means that you can become a lot more agnostic about your back-end technology. Those back-end technologies are busy being available, backed up and fast. The demo app mentioned above uses localStorage but that would be very easy to replace with Kinto, PouchDB or Firebase. Those are all very scalable platforms. So, again, this pattern does scale.