Storing Objects in HTML5 localStorage
-
18-09-2019 - |
Question
I'd like to store a JavaScript object in HTML5 localStorage
, but my object is apparently being converted to a string.
I can store and retrieve primitive JavaScript types and arrays using localStorage
, but objects don't seem to work. Should they?
Here's my code:
var testObject = { 'one': 1, 'two': 2, 'three': 3 };
console.log('typeof testObject: ' + typeof testObject);
console.log('testObject properties:');
for (var prop in testObject) {
console.log(' ' + prop + ': ' + testObject[prop]);
}
// Put the object into storage
localStorage.setItem('testObject', testObject);
// Retrieve the object from storage
var retrievedObject = localStorage.getItem('testObject');
console.log('typeof retrievedObject: ' + typeof retrievedObject);
console.log('Value of retrievedObject: ' + retrievedObject);
The console output is
typeof testObject: object
testObject properties:
one: 1
two: 2
three: 3
typeof retrievedObject: string
Value of retrievedObject: [object Object]
It looks to me like the setItem
method is converting the input to a string before storing it.
I see this behavior in Safari, Chrome, and Firefox, so I assume it's my misunderstanding of the HTML5 Web Storage spec, not a browser-specific bug or limitation.
I've tried to make sense of the structured clone algorithm described in http://www.w3.org/TR/html5/infrastructure.html. I don't fully understand what it's saying, but maybe my problem has to do with my object's properties not being enumerable (???)
Is there an easy workaround?
Update: The W3C eventually changed their minds about the structured-clone specification, and decided to change the spec to match the implementations. See https://www.w3.org/Bugs/Public/show_bug.cgi?id=12111. So this question is no longer 100% valid, but the answers still may be of interest.
Solution
Looking at the Apple, Mozilla and Mozilla again documentation, the functionality seems to be limited to handle only string key/value pairs.
A workaround can be to stringify your object before storing it, and later parse it when you retrieve it:
var testObject = { 'one': 1, 'two': 2, 'three': 3 };
// Put the object into storage
localStorage.setItem('testObject', JSON.stringify(testObject));
// Retrieve the object from storage
var retrievedObject = localStorage.getItem('testObject');
console.log('retrievedObject: ', JSON.parse(retrievedObject));
OTHER TIPS
A minor improvement on a variant:
Storage.prototype.setObject = function(key, value) {
this.setItem(key, JSON.stringify(value));
}
Storage.prototype.getObject = function(key) {
var value = this.getItem(key);
return value && JSON.parse(value);
}
Because of short-circuit evaluation, getObject()
will immediately return null
if key
is not in Storage. It also will not throw a SyntaxError
exception if value
is ""
(the empty string; JSON.parse()
cannot handle that).
You might find it useful to extend the Storage object with these handy methods:
Storage.prototype.setObject = function(key, value) {
this.setItem(key, JSON.stringify(value));
}
Storage.prototype.getObject = function(key) {
return JSON.parse(this.getItem(key));
}
This way you get the functionality that you really wanted even though underneath the API only supports strings.
Extending the Storage object is an awesome solution. For my API, I have created a facade for localStorage and then check if it is an object or not while setting and getting.
var data = {
set: function(key, value) {
if (!key || !value) {return;}
if (typeof value === "object") {
value = JSON.stringify(value);
}
localStorage.setItem(key, value);
},
get: function(key) {
var value = localStorage.getItem(key);
if (!value) {return;}
// assume it is an object that has been stringified
if (value[0] === "{") {
value = JSON.parse(value);
}
return value;
}
}
Stringify doesn't solve all problems
It seems that the answers here don't cover all types that are possible in JavaScript, so here are some short examples on how to deal with them correctly:
//Objects and Arrays:
var obj = {key: "value"};
localStorage.object = JSON.stringify(obj); //Will ignore private members
obj = JSON.parse(localStorage.object);
//Boolean:
var bool = false;
localStorage.bool = bool;
bool = (localStorage.bool === "true");
//Numbers:
var num = 42;
localStorage.num = num;
num = +localStorage.num; //short for "num = parseFloat(localStorage.num);"
//Dates:
var date = Date.now();
localStorage.date = date;
date = new Date(parseInt(localStorage.date));
//Regular expressions:
var regex = /^No\.[\d]*$/i; //usage example: "No.42".match(regex);
localStorage.regex = regex;
var components = localStorage.regex.match("^/(.*)/([a-z]*)$");
regex = new RegExp(components[1], components[2]);
//Functions (not recommended):
function func(){}
localStorage.func = func;
eval( localStorage.func ); //recreates the function with the name "func"
I do not recommend to store functions because eval()
is evil can lead to issues regarding security, optimisation and debugging.
In general, eval()
should never be used in JavaScript code.
Private members
The problem with using JSON.stringify()
for storing objects is, that this function can not serialise private members.
This issue can be solved by overwriting the .toString()
method (which is called implicitly when storing data in web storage):
//Object with private and public members:
function MyClass(privateContent, publicContent){
var privateMember = privateContent || "defaultPrivateValue";
this.publicMember = publicContent || "defaultPublicValue";
this.toString = function(){
return '{"private": "' + privateMember + '", "public": "' + this.publicMember + '"}';
};
}
MyClass.fromString = function(serialisedString){
var properties = JSON.parse(serialisedString || "{}");
return new MyClass( properties.private, properties.public );
};
//Storing:
var obj = new MyClass("invisible", "visible");
localStorage.object = obj;
//Loading:
obj = MyClass.fromString(localStorage.object);
Circular references
Another problem stringify
can't deal with are circular references:
var obj = {};
obj["circular"] = obj;
localStorage.object = JSON.stringify(obj); //Fails
In this example, JSON.stringify()
will throw a TypeError
"Converting circular structure to JSON".
If storing circular references should be supported, the second parameter of JSON.stringify()
might be used:
var obj = {id: 1, sub: {}};
obj.sub["circular"] = obj;
localStorage.object = JSON.stringify( obj, function( key, value) {
if( key == 'circular') {
return "$ref"+value.id+"$";
} else {
return value;
}
});
However, finding an efficient solution for storing circular references highly depends on the tasks that need to be solved, and restoring such data is not trivial either.
There are already some question on SO dealing with this problem: Stringify (convert to JSON) a JavaScript object with circular reference
There is a great library that wraps many solutions so it even supports older browsers called jStorage
You can set an object
$.jStorage.set(key, value)
And retrieve it easily
value = $.jStorage.get(key)
value = $.jStorage.get(key, "default value")
Using JSON objects for local storage:
//SET
var m={name:'Hero',Title:'developer'};
localStorage.setItem('us', JSON.stringify(m));
//GET
var gm =JSON.parse(localStorage.getItem('us'));
console.log(gm.name);
// Iteration of all local storage keys and values
for (var i = 0, len = localStorage.length; i < len; ++i) {
console.log(localStorage.getItem(localStorage.key(i)));
}
// DELETE
localStorage.removeItem('us');
delete window.localStorage["us"];
In theory, it is possible to store objects with functions:
function store (a)
{
var c = {f: {}, d: {}};
for (var k in a)
{
if (a.hasOwnProperty(k) && typeof a[k] === 'function')
{
c.f[k] = encodeURIComponent(a[k]);
}
}
c.d = a;
var data = JSON.stringify(c);
window.localStorage.setItem('CODE', data);
}
function restore ()
{
var data = window.localStorage.getItem('CODE');
data = JSON.parse(data);
var b = data.d;
for (var k in data.f)
{
if (data.f.hasOwnProperty(k))
{
b[k] = eval("(" + decodeURIComponent(data.f[k]) + ")");
}
}
return b;
}
However, Function serialization/deserialization is unreliable because it is implementation-dependent.
You could also override the default Storage setItem(key,value)
and getItem(key)
methods to handle objects/arrays like any other data type. That way, you can simply call localStorage.setItem(key,value)
and localStorage.getItem(key)
as you normally would.
I haven't tested this extensively, but it has appeared to work without problems for a small project I've been tinkering with.
Storage.prototype._setItem = Storage.prototype.setItem;
Storage.prototype.setItem = function(key, value)
{
this._setItem(key, JSON.stringify(value));
}
Storage.prototype._getItem = Storage.prototype.getItem;
Storage.prototype.getItem = function(key)
{
try
{
return JSON.parse(this._getItem(key));
}
catch(e)
{
return this._getItem(key);
}
}
I arrived at this post after hitting on another post that has been closed as a duplicate of this - titled 'how to store an array in localstorage?'. Which is fine except neither thread actually provides a full answer as to how you can maintain an array in localStorage - however I have managed to craft a solution based on information contained in both threads.
So if anyone else is wanting to be able to push/pop/shift items within an array, and they want that array stored in localStorage or indeed sessionStorage, here you go:
Storage.prototype.getArray = function(arrayName) {
var thisArray = [];
var fetchArrayObject = this.getItem(arrayName);
if (typeof fetchArrayObject !== 'undefined') {
if (fetchArrayObject !== null) { thisArray = JSON.parse(fetchArrayObject); }
}
return thisArray;
}
Storage.prototype.pushArrayItem = function(arrayName,arrayItem) {
var existingArray = this.getArray(arrayName);
existingArray.push(arrayItem);
this.setItem(arrayName,JSON.stringify(existingArray));
}
Storage.prototype.popArrayItem = function(arrayName) {
var arrayItem = {};
var existingArray = this.getArray(arrayName);
if (existingArray.length > 0) {
arrayItem = existingArray.pop();
this.setItem(arrayName,JSON.stringify(existingArray));
}
return arrayItem;
}
Storage.prototype.shiftArrayItem = function(arrayName) {
var arrayItem = {};
var existingArray = this.getArray(arrayName);
if (existingArray.length > 0) {
arrayItem = existingArray.shift();
this.setItem(arrayName,JSON.stringify(existingArray));
}
return arrayItem;
}
Storage.prototype.unshiftArrayItem = function(arrayName,arrayItem) {
var existingArray = this.getArray(arrayName);
existingArray.unshift(arrayItem);
this.setItem(arrayName,JSON.stringify(existingArray));
}
Storage.prototype.deleteArray = function(arrayName) {
this.removeItem(arrayName);
}
example usage - storing simple strings in localStorage array:
localStorage.pushArrayItem('myArray','item one');
localStorage.pushArrayItem('myArray','item two');
example usage - storing objects in sessionStorage array:
var item1 = {}; item1.name = 'fred'; item1.age = 48;
sessionStorage.pushArrayItem('myArray',item1);
var item2 = {}; item2.name = 'dave'; item2.age = 22;
sessionStorage.pushArrayItem('myArray',item2);
common methods to manipulate arrays:
.pushArrayItem(arrayName,arrayItem); -> adds an element onto end of named array
.unshiftArrayItem(arrayName,arrayItem); -> adds an element onto front of named array
.popArrayItem(arrayName); -> removes & returns last array element
.shiftArrayItem(arrayName); -> removes & returns first array element
.getArray(arrayName); -> returns entire array
.deleteArray(arrayName); -> removes entire array from storage
Recommend using an abstraction library for many of the features discussed here as well as better compatibility. Lots of options:
- jStorage or simpleStorage << my preference
- localForage
- alekseykulikov/storage
- Lawnchair
- Store.js << another good option
- OMG
Better you make functions as setter and getter to localStorage, this way you will have better control and won't have to repeat the JSON parsing and all. it will even handle your (" ") empty string key/data case smoothly.
function setItemInStorage(dataKey, data){
localStorage.setItem(dataKey, JSON.stringify(data));
}
function getItemFromStorage(dataKey){
var data = localStorage.getItem(dataKey);
return data? JSON.parse(data): null ;
}
setItemInStorage('user', { name:'tony stark' });
getItemFromStorage('user'); /* return {name:'tony stark'} */
I've modified one of top-voted answer a little. I'm a fan of having single function instead of 2 if it's not needed.
Storage.prototype.object = function(key, val) {
if ( typeof val === "undefined" ) {
var value = this.getItem(key);
return value ? JSON.parse(value) : null;
} else {
this.setItem(key, JSON.stringify(val));
}
}
localStorage.object("test", {a : 1}); //set value
localStorage.object("test"); //get value
Also, if no value is set, it's returning null
instead of false
. false
has some meaning, null
does not.
Improvement on @Guria 's answer:
Storage.prototype.setObject = function (key, value) {
this.setItem(key, JSON.stringify(value));
};
Storage.prototype.getObject = function (key) {
var value = this.getItem(key);
try {
return JSON.parse(value);
}
catch(err) {
console.log("JSON parse failed for lookup of ", key, "\n error was: ", err);
return null;
}
};
You can use localDataStorage to transparently store javascript data types (Array, Boolean, Date, Float, Integer, String and Object). It also provides lightweight data obfuscation, automatically compresses strings, facilitates query by key (name) as well as query by (key) value, and helps to enforce segmented shared storage within the same domain by prefixing keys.
[DISCLAIMER] I am the author of the utility [/DISCLAIMER]
Examples:
localDataStorage.set( 'key1', 'Belgian' )
localDataStorage.set( 'key2', 1200.0047 )
localDataStorage.set( 'key3', true )
localDataStorage.set( 'key4', { 'RSK' : [1,'3',5,'7',9] } )
localDataStorage.set( 'key5', null )
localDataStorage.get( 'key1' ) --> 'Belgian'
localDataStorage.get( 'key2' ) --> 1200.0047
localDataStorage.get( 'key3' ) --> true
localDataStorage.get( 'key4' ) --> Object {RSK: Array(5)}
localDataStorage.get( 'key5' ) --> null
As you can see, the primitive values are respected.
Another option would be to use an existing plugin.
For example persisto is an open source project that provides an easy interface to localStorage/sessionStorage and automates persistence for form fields (input, radio buttons, and checkboxes).
(Disclaimer: I am the author.)
You can use ejson to store the objects as strings.
EJSON is an extension of JSON to support more types. It supports all JSON-safe types, as well as:
- Date (JavaScript
Date
)- Binary (JavaScript
Uint8Array
or the result of EJSON.newBinary)- User-defined types (see EJSON.addType. For example, Mongo.ObjectID is implemented this way.)
All EJSON serializations are also valid JSON. For example an object with a date and a binary buffer would be serialized in EJSON as:
{ "d": {"$date": 1358205756553}, "b": {"$binary": "c3VyZS4="} }
Here is my localStorage wrapper using ejson
https://github.com/UziTech/storage.js
I added some types to my wrapper including regular expressions and functions
http://rhaboo.org is a localStorage sugar layer that lets you write things like this:
var store = Rhaboo.persistent('Some name');
store.write('count', store.count ? store.count+1 : 1);
store.write('somethingfancy', {
one: ['man', 'went'],
2: 'mow',
went: [ 2, { mow: ['a', 'meadow' ] }, {} ]
});
store.somethingfancy.went[1].mow.write(1, 'lawn');
It doesn't use JSON.stringify/parse because that would be inaccurate and slow on big objects. Instead, each terminal value has its own localStorage entry.
You can probably guess that I might have something to do with rhaboo ;-)
Adrian.
I made another minimalistic wrapper with only 20 lines of code to allow using it like it should:
localStorage.set('myKey',{a:[1,2,5], b: 'ok'});
localStorage.has('myKey'); // --> true
localStorage.get('myKey'); // --> {a:[1,2,5], b: 'ok'}
localStorage.keys(); // --> ['myKey']
localStorage.remove('myKey');
For Typescript users willing to set and get typed properties:
/**
* Silly wrapper to be able to type the storage keys
*/
export class TypedStorage<T> {
public removeItem(key: keyof T): void {
localStorage.removeItem(key);
}
public getItem<K extends keyof T>(key: K): T[K] | null {
const data: string | null = localStorage.getItem(key);
return JSON.parse(data);
}
public setItem<K extends keyof T>(key: K, value: T[K]): void {
const data: string = JSON.stringify(value);
localStorage.setItem(key, data);
}
}
// write an interface for the storage
interface MyStore {
age: number,
name: string,
address: {city:string}
}
const storage: TypedStorage<MyStore> = new TypedStorage<MyStore>();
storage.setItem("wrong key", ""); // error unknown key
storage.setItem("age", "hello"); // error, age should be number
storage.setItem("address", {city:"Here"}); // ok
const address: {city:string} = storage.getItem("address");
Here some extented version of the code posted by @danott
It'll also implement delete value from localstorage and shows how to adds a Getter and Setter layer so instead of
localstorage.setItem(preview, true)
you can write
config.preview = true
Okay here were go:
var PT=Storage.prototype
if (typeof PT._setItem >='u') PT._setItem = PT.setItem;
PT.setItem = function(key, value)
{
if (typeof value >='u')//..ndefined
this.removeItem(key)
else
this._setItem(key, JSON.stringify(value));
}
if (typeof PT._getItem >='u') PT._getItem = PT.getItem;
PT.getItem = function(key)
{
var ItemData = this._getItem(key)
try
{
return JSON.parse(ItemData);
}
catch(e)
{
return ItemData;
}
}
// Aliases for localStorage.set/getItem
get = localStorage.getItem.bind(localStorage)
set = localStorage.setItem.bind(localStorage)
// Create ConfigWrapperObject
var config = {}
// Helper to create getter & setter
function configCreate(PropToAdd){
Object.defineProperty( config, PropToAdd, {
get: function () { return ( get(PropToAdd) ) },
set: function (val) { set(PropToAdd, val ) }
})
}
//------------------------------
// Usage Part
// Create properties
configCreate('preview')
configCreate('notification')
//...
// Config Data transfer
//set
config.preview = true
//get
config.preview
// delete
config.preview = undefined
Well you may strip the aliases part with .bind(...)
. However I just put it in since it's really good to know about this. I tooked me hours to find out why a simple get = localStorage.getItem;
don't work
I made a thing that doesn't break the existing Storage objects, but creates a wrapper so you can do what you want. The result is a normal object, no methods, with access like any object.
If you want 1 localStorage
property to be magic:
var prop = ObjectStorage(localStorage, 'prop');
If you need several:
var storage = ObjectStorage(localStorage, ['prop', 'more', 'props']);
Everything you do to prop
, or the objects inside storage
will be automatically saved into localStorage
. You're always playing with a real object, so you can do stuff like this:
storage.data.list.push('more data');
storage.another.list.splice(1, 2, {another: 'object'});
And every new object inside a tracked object will be automatically tracked.
The very big downside: it depends on Object.observe()
so it has very limited browser support. And it doesn't look like it'll be coming for Firefox or Edge anytime soon.
Let's say you have the following array called movies:
var movies = ["Reservoir Dogs", "Pulp Fiction", "Jackie Brown",
"Kill Bill", "Death Proof", "Inglourious Basterds"];
Using the stringify function, your movies array can be turned into a string by using the following syntax:
localStorage.setItem("quentinTarantino", JSON.stringify(movies));
Notice that my data is being stored under the key called quentinTarantino.
Retrieving Your Data
var retrievedData = localStorage.getItem("quentinTarantino");
To convert from a string back to an object, use the JSON parse function:
var movies2 = JSON.parse(retrievedData);
You can call all of the array methods on your movies2
To store an object, you could make a letters that you can use to get an object from a string to an object (may not make sense). For example
var obj = {a: "lol", b: "A", c: "hello world"};
function saveObj (key){
var j = "";
for(var i in obj){
j += (i+"|"+obj[i]+"~");
}
localStorage.setItem(key, j);
} // Saving Method
function getObj (key){
var j = {};
var k = localStorage.getItem(key).split("~");
for(var l in k){
var m = k[l].split("|");
j[m[0]] = m[1];
}
return j;
}
saveObj("obj"); // undefined
getObj("obj"); // {a: "lol", b: "A", c: "hello world"}
This technique will cause some glitches if you use the letter that you used to split the object, and it's also very experimental.
A small example of a library that use localStorage for keeping track of received messages from contacts:
// This class is supposed to be used to keep a track of received message per contacts.
// You have only four methods:
// 1 - Tells you if you can use this library or not...
function isLocalStorageSupported(){
if(typeof(Storage) !== "undefined" && window['localStorage'] != null ) {
return true;
} else {
return false;
}
}
// 2 - Give the list of contacts, a contact is created when you store the first message
function getContacts(){
var result = new Array();
for ( var i = 0, len = localStorage.length; i < len; ++i ) {
result.push(localStorage.key(i));
}
return result;
}
// 3 - store a message for a contact
function storeMessage(contact, message){
var allMessages;
var currentMessages = localStorage.getItem(contact);
if(currentMessages == null){
var newList = new Array();
newList.push(message);
currentMessages = JSON.stringify(newList);
}
else
{
var currentList =JSON.parse(currentMessages);
currentList.push(message);
currentMessages = JSON.stringify(currentList);
}
localStorage.setItem(contact, currentMessages);
}
// 4 - read the messages of a contact
function readMessages(contact){
var result = new Array();
var currentMessages = localStorage.getItem(contact);
if(currentMessages != null){
result =JSON.parse(currentMessages);
}
return result;
}
Localstorage can only store key-value pairs where both of the keys and the values have to be strings. However, you can store object by serializing them to JSON
strings and then deserialize them to JS objects when you retrieve them.
For example:
var testObject = { 'one': 1, 'two': 2, 'three': 3 };
// JSON.stringify turns a JS object into a JSON string, thus we can store it
localStorage.setItem('testObject', JSON.stringify(testObject));
// After we recieve a JSON string we can parse it into a JS object using JSON.parse
var jsObject = JSON.parse(localStorage.getItem('testObject'));
Be aware of the fact that this will remove the established prototype chain. This is best shown via an example:
function testObject () {
this.one = 1;
this.two = 2;
this.three = 3;
}
testObject.prototype.hi = 'hi';
var testObject1 = new testObject();
// logs the string hi, derived from prototype
console.log(testObject1.hi);
// the prototype of testObject1 is testObject.prototype
console.log(Object.getPrototypeOf(testObject1));
// stringify and parse the js object, will result in a normal JS object
var parsedObject = JSON.parse(JSON.stringify(testObject1));
// the newly created object now has Object.prototype as its prototype
console.log(Object.getPrototypeOf(parsedObject) === Object.prototype);
// no longer is testObject the prototype
console.log(Object.getPrototypeOf(parsedObject) === testObject.prototype);
// thus we cannot longer access the hi property since this was on the prototype
console.log(parsedObject.hi); // undefined
I have this JS Object * I want to store this in HTML5 Local storage
todosList = [
{ id: 0, text: "My todo", finished: false },
{ id: 1, text: "My first todo", finished: false },
{ id: 2, text: "My second todo", finished: false },
{ id: 3, text: "My third todo", finished: false },
{ id: 4, text: "My 4 todo", finished: false },
{ id: 5, text: "My 5 todo", finished: false },
{ id: 6, text: "My 6 todo", finished: false },
{ id: 7, text: "My 7 todo", finished: false },
{ id: 8, text: "My 8 todo", finished: false },
{ id: 9, text: "My 9 todo", finished: false }
];
I can store this in HTML5 Local storage in this way, by using JSON.stringify
localStorage.setItem("todosObject", JSON.stringify(todosList));
And Now i can get this Object from Local storage by JSON.parsing.
todosList1 = JSON.parse(localStorage.getItem("todosObject"));
console.log(todosList1);
Loop throught localstorage
var retrievedData = localStorage.getItem("MyCart");
retrievedData.forEach(function (item) {
console.log(item.itemid);
});