AJAX Tutorial: Saving Session Across Page Loads Without Cookies, On The Client Side

This is a mini-tutorial on saving state across page loads on the client side, without using cookies so as to save large amounts of data beyond cookies size limits.

In AJAX, most of the action occurs inside a single web page. When a page is loaded, a new instance of the JavaScript interpreter is started. When you leave a page, jumping to Google for example, all of your JavaScript objects are completely cleared away; you lose all of your state. If you then hit the back button to go back from Google to your original AJAX application, you will find that the page actually completely reloads, calling your onload listener, and that any JavaScript objects you stored anywhere are gone.

This can be a pain. First, it's something that not all programmers are aware of, which can lead to errors, so it's important to know about. Second, user's see their state completely wiped out; when they go back to their AJAX application with the back button they see the original state of their program, not the last place they left it. Third, this can impact performance, since the AJAX application has to re-retrieve everything from the server rather than use its local state.

Finally, there are some libraries that try to solve the back/forward button issues that store local JavaScript state in order to do so; this means they are clobbered if you leave the page, losing all history state. There are some back/forward button libraries that work without internal state, loading everything from the anchor hash on a URL on each change, but these libraries are restricted in the amount of state information you can represent in a URL. To find a truly robust solution to the back/forward button issue, one that allows for large, sophisticated blocks of state to be passed to programmers on each history change, we must find a way to solve the state problem.

Are there ways to save state on the client between page loads, akin to a user-level session? This is something I have been exploring recently. Note that session level state is different than the holy grail of saving offline information; they are similar, but session level state only lasts for a single instance of the browser. When it is closed state is lost, while permanent offline storage of state is saved forever. I have some ideas on how to solve the offline storage issue that I will explore in future blog posts.

I have been surprised to find two different ways to achieve session level state, each with their own stregnths and weaknesses.

Method number one is to use an iframe in some special ways. First, this iframe must exist on page load (See "AJAX Tutorial: A Tale of Two IFrames (or, How To Control Your Browsers History" for details on the different kinds of iframes):
<html>
<body>
<iframe id="sessionFrame"></iframe>
</body>
</html>
Then, whenever you wish to record state that you want to save, you must write that state into the iframe in the following way, programatically:
var sessionFrame = document.getElementById("sessionFrame");
var doc = sessionFrame.contentDocument;
if (doc == undefined) { // Internet Explorer
doc = sessionFrame.contentWindow.document;
}

doc.open();
doc.write(someNewState);
doc.close();
What this does is to actually write your state into the browser's history queue. The doc.write() business creates an entirely fresh new document each time, which the browser dutifully saves into its history list. As you move around with the back and forward buttons, you will see the iframe update with each state change you have saved into the iframe. Every state change must be done as above, with an open(), write(), and close() sequence to create a fresh document.

The full code is a bit more complicated:
<html>
<head>
<script language="JavaScript">
function initialize() {
if (sessionExists() == false) {
saveState("Hello World 1");
// some browsers need a bit of a timeout
window.setTimeout("saveState('Hello World 2')", 300);
window.setTimeout("saveState('Hello World 3')", 600);
}
}

function getIFrameDocument() {
var historyFrame =
document.getElementById("historyFrame");
var doc = historyFrame.contentDocument;
if (doc == undefined) // Internet Explorer
doc = historyFrame.contentWindow.document;

return doc;
}

function sessionExists() {
var doc = getIFrameDocument();
try {
if (doc.body.innerHTML == "")
return false;
else
return true;
}
catch (exp) {
// sometimes an exception is thrown if a
// value is already in the iframe
return true;
}
}

function saveState(message) {
// get our template that we will
// write into the history iframe
var templateDiv =
document.getElementById("iframeTemplate");
var currentElement = templateDiv.firstChild;

// loop until we get a COMMENT_NODE
while (currentElement &&
currentElement.nodeType != 8) {
currentElement = currentElement.nextSibling;
}
var template = currentElement.nodeValue;

// replace the templated value in the
// constants with our new value
var newContent =
template.replace(/\%message\%/, message);

// now write out the new contents
var doc = getIFrameDocument();
doc.open();
doc.write(newContent);
doc.close();
}
</script>
</head>

<body onload="initialize()">
<div id="iframeTemplate">
<!--
<html>
<body>
<p>%message%</p>
</body>
</html>
-->
</div>

<iframe id="historyFrame"></iframe>
</body>

<html>
In the code above, we use a template to store what we will stick into the iframe (see "DHTML Templates Tutorial" for details on using templates). We also have a new method, sessionExists, that checks to see if we have ever written anything into this iframe before. If a user goes to this page, goes to Google, and then comes back, our page's onload listener will fire. We don't want to rewrite our values into the session, so we must have a way to know if we have ever written anything before. If we have not, then we write our test values in.

In production code you would hide the iframe.

Give the code a try, and feel free to use it (it's under a BSD license). The code writes three state changes into the iframe. Go to another website, such as Google, and then hit the back button to return to the page; you will see that all of the changes we wrote into the iframe persist. If you close the browser you will see they disappear, or if you type the website's address seperate from the history.

A major problem (or strength) with the iframe approach is that it affects the history, which can be a problem since we don't always want to tie session storage to the history.

The other session approach I've found is a crazy hack that I saw used for something else, in Danny Goodman's book JavaScript and DHTML Cookbook. Danny was looking for a way to pass data between frames, and he found that you could persist data by saving them into hidden text fields.

This is a beautiful (and scary) hack that is a solution to our session state. All the browsers I have tried (IE, Firefox, and Safari) persist any text you put into a form field, even if they are hidden. To save state, then, we can simply write it into hidden form fields!

I have done tests with the Lorem Ipsum generator to see how much data I can store in a textarea in Firefox and IE, and I couldn't find the upper bound. I had the generator give me about 1 meg of data, placed it into a textarea, and then moved away from the page and back and the data was still there! The data disappears when you close the browser, but this is perfect for saving our session state.

There is a small footnote, though. Basicly, the form and the textarea field that stores state must exist on page load; they can not be dynamically created through JavaScript, which makes using them a bit more difficult.

Here is some simple sample code that shows how you can persist state into a hidden form:


<html>
<head>
<script language="JavaScript">
function initialize() {
var sessionField =
document.getElementById("sessionField");
var sessionValue = sessionField.value;
if (sessionValue == "empty") {
alert("Storing new session value");
sessionField.value = "Hello World";
}
else {
alert("Old session value: " + sessionValue);
}
}
</script>
</head>

<body onload="initialize()">
<form style="display: none;" id="sessionForm">
<textarea id="sessionField">empty</textarea>
</form>
</body>
</html>


Give the demo a whirl as well. The first time you load the page a popup will appear that we are storing state for the first time. Next, if you reload the page or go to another website and then come back, the popup will reappear but will say that we retrieved the old session value, which is "Hello World".

This code is also free to use, under a BSD license.

Comments

b0b0b0b said…
I'd argue that the scary parts are
what guarantees browsers make about this data, w.r.t lifetime, size, and consistency?

good post, good things to think about
elbloguero said…
In Firefox 1.5.04 it doesnt work..

A bug??
Illuminatus said…
Nice post man! Will post some comments later, let me experiment a bit.
I would never pass my session values in javascript....
Mostly you need Sessions to keep some IDs…
May be I missed something, but it sounds like sending sensitive information in a query string.
Again, sorry, if I missed the point.
Ilya Tretyakov said…
No more hacks. Simply use CSS:
================

<html>
<head>
<script language="JavaScript">
function initialize() {
var sessionField = document.getElementById("sessionField");
var sessionValue = sessionField.value;
if (sessionValue == "empty") {
alert("Storing new session value");
sessionField.value = "Hello World";
}
else {
alert("Old session value: " + sessionValue);
}
}
</script>
</head>
<body onload="initialize()">
<!--form style="display: none;" id="sessionForm"-->
<form id="sessionForm">
<!--textarea id="sessionField">empty</textarea-->
<div style="position:absolute; z-index:-10; overflow:hidden; width:0; height:0;">
<textarea id="sessionField">empty</textarea>
</div>
</form>
</body>
</html>
Ilya Tretyakov said…
My previous post "No more hacks. Simply use CSS" especialy for ASP.NET guys and other frameworks.
But don't forget turn on caching in brouser by HTTP headers (in PHP also).
But for me more usefull method with Url anchor serialization, like
http://someurl.dom/page.jsp#someData_adfhaafhgds7h6hh5sd5h87hdh69dsh
In ASP.NET exist AJAX History control for it
Unknown said…
this works fine in firefox 3.0.1, but when firebug is enabled - no,no : new session is stored at each page load.
Paul said…
In the latest webkit/safari 4 the form technique doesn't work because form fields aren't repopulated. The iframe technique as you wrote it also doesn't work, but it's VERY easy to fix.

For the iframe with id "historyFrame" just add an attribute for src="blahblahdasd.html" this forces the iframe to be added to the history. If you leave it blank the states will not be saved to history and WILL NOT persist. You don't even need a real html file (hence the blahblahblahasd.html filename). It will add an extra request hit, so maybe point it to a local non-existant file, I don't know. At most it will add just on more HTTP request and there won't be anything to download so it's not too bad. Anyway that's how I got it to work in Webkit/Safari 4. I didn't try it in Google Chrome, but it's based on webkit so it may fix problems there too.
Shaid said…
That simply brilliant.
But I have one problem with IE where ajax response are not updated may be it is showing from cache, in mozilla it is ok.

Can some body give me some idea?

Thank you.
Scholarships solutions
Unknown said…
@Paul i'm using the iframe technique and I have the "src" property of the iframe set but I still can't make it add a history entry to chrome or safari 4
Unknown said…
@Paul, I've tried setting the iframe src. I've tried a file that exists and one that doesn't exist. I can't get safari 3, 4 and chrome 3 to register the history