Key Takeaways
- The process of persisting state in AngularJS involves maintaining user data or application settings even after the page is refreshed or the application is restarted. This is crucial for single-page applications where the state needs to be maintained across different views.
- AngularJS does not inherently provide a mechanism for state persistence, but it can be achieved using methods such as cookies, local storage, or session storage. These methods allow data to be stored on the client side and retrieved even after the page is refreshed or the application is restarted.
- The demo app in the text 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. You can edit the entries and as soon as you blur out of any input field, it starts the saving process.
- The article concludes that the back-end should be thought of as a sync server, with business logic placed in the front-end for an offline-first app experience. This makes the back-end technology more agnostic, focusing on being available, backed up, and fast.
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 useslocalStorage
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 thatlocalStorage
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 useslocalStorage
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.
Frequently Asked Questions on Persisting State in AngularJS
What is the significance of persisting state in AngularJS?
Persisting state in AngularJS is crucial for maintaining user data or application settings even after the page is refreshed or the application is restarted. This is particularly important in single-page applications where the state needs to be maintained across different views. Without state persistence, any data entered or changes made by the user would be lost upon refreshing or navigating away from the page, leading to a poor user experience.
How does AngularJS handle state persistence?
AngularJS does not inherently provide a mechanism for state persistence. However, it can be achieved using various methods such as cookies, local storage, or session storage. These methods allow data to be stored on the client side and retrieved even after the page is refreshed or the application is restarted.
What are the differences between cookies, local storage, and session storage?
Cookies, local storage, and session storage are all methods of storing data on the client side, but they differ in their capacities and lifetimes. Cookies can store up to 4KB of data and are sent with every HTTP request, making them suitable for small amounts of data. Local storage can store up to 5MB of data and persists even when the browser is closed and reopened. Session storage also stores up to 5MB of data but is cleared when the browser is closed.
How can I implement state persistence in AngularJS using local storage?
Implementing state persistence using local storage in AngularJS involves storing the application state in the local storage object provided by the browser. This can be done using the localStorage.setItem()
method to store data and localStorage.getItem()
method to retrieve data. However, since local storage only supports string values, the application state needs to be serialized before storing and parsed after retrieving.
Are there any libraries available to simplify state persistence in AngularJS?
Yes, there are several libraries available that simplify state persistence in AngularJS. One such library is angular-local-storage
, which provides a convenient API for handling local storage and also supports automatic serialization and parsing of data.
What are the security implications of persisting state in AngularJS?
While persisting state in AngularJS can enhance user experience, it also has potential security implications. Since the data is stored on the client side, it can be accessed by anyone who has access to the user’s machine. Therefore, sensitive data should be encrypted before storing and decrypted after retrieving.
Can I persist state in AngularJS using a database?
Yes, state persistence in AngularJS can also be achieved using a database. This involves storing the application state in a database on the server side and retrieving it using AJAX requests. However, this method is more complex and requires a server-side language and database.
How can I handle state persistence in AngularJS for complex objects?
For complex objects, state persistence in AngularJS can be handled using JSON serialization. This involves converting the object into a JSON string before storing and parsing the JSON string back into an object after retrieving.
Can I use cookies for state persistence in AngularJS?
Yes, cookies can be used for state persistence in AngularJS. However, due to their limited capacity and the overhead of sending them with every HTTP request, they are typically used for small amounts of data or for session management.
What are the performance implications of persisting state in AngularJS?
Persisting state in AngularJS can have performance implications, especially when large amounts of data are involved. Storing and retrieving data from local storage or session storage can be slow, and sending cookies with every HTTP request can increase network latency. Therefore, state persistence should be used judiciously and optimized for performance.
Peter is a full-stack Web Developer at Mozilla where he works in the Web Engineering team. His passion is building fun web apps that are fast as greased lightning and has published open source software for over 15 years. He blogs and experiments on www.peterbe.com.