How to store objects in HTML5 localStorage/sessionStorage
Asked Answered
M

24

3074

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 specification, not a browser-specific bug or limitation.

I've tried to make sense of the structured clone algorithm described in 2 Common infrastructure. 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 12111 – spec for Storage object getItem(key) method does not match implementation behavior. So this question is no longer 100% valid, but the answers still may be of interest.

Mincemeat answered 6/1, 2010 at 4:5 Comment(5)
BTW, your reading of "structured clone algorithm" is correct, it's just that the spec was changed from string-only values to this after the implementations were out. I filed bug bugzilla.mozilla.org/show_bug.cgi?id=538142 with mozilla to track this issue.Elbowroom
This seems like a job for indexedDB...Embonpoint
How about storing an array of Objects in localStorage? I am facing same problem that it is getting converted to string.Belgium
could you instead just serialize the array? like store with JSON stringify then parse again upon loading?Practice
You can use localDataStorage to transparently store javascript data types (Array, Boolean, Date, Float, Integer, String and Object)Teucer
G
3789

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));
Godown answered 6/1, 2010 at 4:25 Comment(7)
do observe that any metadata will be removed. you just get an object with the key-value pairs, so any object with behaviour need to be rebuilt.Kaph
@CMS can setItem throw some exception if the data is over the capacity ?Leper
... applies to objects with circular references only, JSON.stringify() expands the referenced object to its full "content" (implicitly stringified) in the object we stringify. See: https://mcmap.net/q/40798/-stringify-convert-to-json-a-javascript-object-with-circular-referenceHeritor
The problem with this approach are performance issues, if you have to handle large arrays or objects.Peyter
@Kaph true, but as maja correctly suggested eval() => , this is one of the good use of , you can easily retrieve function code => store it as string and then eval() it back :)Leathers
I have just used it for the first time. So, should we need to remove item just after retrieving it from local storage?Homemade
@Kaph It's not just metadata or behaviour that will be removed. All data which doesn't fit into JSON will be removed or modified: undefined can't be represented in JSON, nor Infinity, nor NaN, nor Date objects, or many other things, at least not in a way that will be parsed back as expected using JSON.parse. You've basically just got the basic types that JSON supports.Sharyl
A
681

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).

Abelabelard answered 30/6, 2010 at 6:45 Comment(10)
I just want to quickly add the usage as it wasn't immediately clear for me: var userObject = { userId: 24, name: 'Jack Bauer' }; And to set it localStorage.setObject('user', userObject); Then get it back from storage userObject = localStorage.getObject('user'); You can even store an array of objects if you want.Blare
when you do return this.getItem(key) && JSON.parse(this.getItem(key)), is it combining those two things? I've never seen this way of returning something.Megaton
It is just boolean expression. Second part are evaluated only if left one is true. In that case result of whole expression will be from right part. It is popular technic based on the way how boolean expressions are evaluated.Abelabelard
I do not see the point of the local variable and the shortcut evaluation here (minor performance improvements aside). If key is not in the Local Storage, window.localStorage.getItem(key) returns null – it does not throw an "Illegal access" exception – and JSON.parse(null) returns null as well – it does not throw an exception either, neither in Chromium 21 nor per ES 5.1 section 15.12.2, because String(null) === "null" which can be interpreted as a JSON literal.Sari
The values in Local Storage are always primitive string values. So what this shortcut evaluation does handle is when someone stored "" (the empty string) before. Because it type-converts to false and JSON.parse(""), which would throw a SyntaxError exception, is not called.Sari
This wont't work in IE8, so you're better of using the functions in the confirmed answer if you need to support it.Forwarding
FYI: The getObject() function barfs if the value undefined finds it's way into localStorage.Chivaree
To complete the answer, because who sets might also want to clear the storage: localStorage.clear(); Plus after the getObject() you check with: if(userObject) or if (!userObject). Disclaimer: I am noting this down because I tried undefined first.Minefield
This throw exception when the value was undefined, maybe it's safe to check first if the value i.e. value || null this was also safe in JSON.parse(value);Banian
Generally, it's not a good idea to monkey-patch a global like this, as it can break code that depends on Storage not having those methods, and it's not future-compatible in case browsers implement a setObject or getObject method on it in the future.Sharyl
O
246

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.

Oldham answered 6/1, 2010 at 4:42 Comment(4)
Wrapping CMS' approach up into a function is a good idea, it just needs a feature tests: One for JSON.stringify, one for JSON.parse, and one to test if localStorage can in fact set and retrieve an object. Modifying host objects is not a good idea; I would rather see this as a separate method and not as localStorage.setObject.Kampala
This getObject() will throw a SyntaxError exception if the stored value is "", because JSON.parse() cannot handle that. See my edit to Guria's answer for details.Sari
Just my two cents, but I'm pretty sure it's not a good idea to extend objects provided by the vendor like this.Cockahoop
I completely agree with @Cockahoop . Please don't monkey-patch globals implemented by the browser like this. It can break code and it's not future-compatible with browsers that may ship a setObject method in this global in the future.Sharyl
O
84

Creating a facade for the Storage object is an awesome solution. That way, you can implement your own get and set methods. 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;
  }
}
Ogdan answered 21/1, 2011 at 18:29 Comment(10)
This was almost exactly what i needed. Just had to add if (value == null) { return false } before the comment, otherwise it resulted in error when checking the existence of a key on localStorage.Belga
This is pretty cool actually. Agree with @FrancescoFrapporti you need an if in there for null values. I also added an ' || value[0] == "[" ' test in case there in an array in there.Empathize
Good point, I'll edit this. Although you don't need the null part, but if you do I recommend three ===. If you use JSHint or JSLint you will be warned against using ==.Ogdan
And for non-ninjas (like me), could someone please provide a usage example for this answer? Is it: data.set('username': 'ifedi', 'fullname': { firstname: 'Ifedi', lastname: 'Okonkwo'});?Cumshaw
Yes indeed! When I overcame my desire to be spoon-fed, I took the code to test out, and got it. I think this answer is great because 1)Unlike the accepted answer, it takes time to do certain checks on the string data, and 2)Unlike the next one, it doesn't go extending a native object.Cumshaw
And then someone tries to store a string starting with {Fullfledged
@JimmyT. that has kept me up at night in a cold sweat wondering if someone would do that. If you suggest a better regex or test for it being proper json in an efficient way, I'll edit the answer.Ogdan
You could just stringify everythingFullfledged
Your set function won't work if you want to set a key to 0, "" or any other value that converts to false. Instead you should write: if (!key || value === undefined) return; This will also let you store a value of 'null' for a key.Intramuscular
This answer could be improved by mentioning and handling circular references and objects that may include custom classes.Birdt
I
83

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 and 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 Stack Overflow dealing with this problem: Stringify (convert to JSON) a JavaScript object with circular reference

Immitigable answered 19/11, 2014 at 9:51 Comment(5)
Therefore, and needless to say - storing data into Storage should be based on the sole premise of copies of simple data. Not live Objects.Tubman
Probably would use a custom toJSON instead of toString() these days. No symmetric equivalent for parsing, unfortunately.Kaph
toJSON won't support types that have no direct json representation like date, regex, function and many other, newer types that were added to JavaScript after I wrote this answer.Immitigable
Why "+" in front of localStorage.num (num = +localStorage.num)?Ascensive
@PeterMortensen to convert the stored string back into a numberImmitigable
O
57

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")
Outlive answered 23/8, 2011 at 3:52 Comment(1)
@SuperUberDuper jStorage requires Prototype, MooTools or jQueryOutlive
M
39

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
Macro answered 7/5, 2014 at 11:35 Comment(3)
This is a very handy set of methods for manipulating arrays stored in localStorage or sessionStorage, and deserves much more credit than it's attracted. @Andy Lorenz Thanks for taking the time to share!Desperate
It's generally not a good idea to monkey patch a global shipped by the browser like this. It can cause other code to break, and it's not forwards compatible with future browsers that may want to ship their own identically named methods in the global.Sharyl
@Sharyl I agree its GENERALLY not a good idea to do this, but that opinion is based much more on theory than practice. e.g. Since my posting in 2014 nothing in the localStorage or sessionStorage implementations have changed that have been compromised. And I doubt they ever will to be honest. But if that possibility was a concern for someone - and it is a personal decision to consider the risk, not a 'thou shalt/not' - my answer could easily be used as a blueprint to implement a custom array class that wraps around actual localStorage/sessionStorage.Macro
E
32

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.

Emissivity answered 5/4, 2011 at 21:20 Comment(5)
Function serialization/deserialization is unreliable because it is implementation-dependent. Also, you want to replace c.f[k] = escape(a[k]); with the Unicode-safe c.f[k] = encodeURIComponent(a[k]); and eval('b.' + k + ' = ' + unescape(data.f[k])); with b[k] = eval("(" + decodeURIComponent(data.f[k]) + ")");. The parentheses are required because your function, if serialized properly, is likely to be anonymous, which is not as-is a valid /Statement/ (so eval()) would throw a SyntaxError exception otherwise).Sari
And typeof is an operator, do not write it as if it was a function. Replace typeof(a[k]) with typeof a[k].Sari
In addition to applying my suggestions and emphasizing the unreliability of the approach, I have fixed the following bugs: 1. Not all variables were declared. 2. for-in was not filtered for own properties. 3. Code style, including referencing, was inconsistent.Sari
@Sari what practical difference does this make? the spec says the use and placement of white space, line terminators, and semicolons within the representation String is implementation-dependent. I don't see any functional differences.Consternate
@Consternate The part that you quoted starts with Note *in particular* that …. But the return value specification starts with An implementation-dependent representation of the function is returned. This representation has the syntax of a FunctionDeclaration. The return value can be function foo () {} – assuming a conforming implementation.Sari
E
23

You cannot store a key value without a string format.

LocalStorage only supports string formats for keys/values.

That is why you should convert your data to string whatever it is an array or object.

To store data in localStorage, first of all stringify it using the JSON.stringify() method.

var myObj = [{name:"test", time:"Date 2017-02-03T08:38:04.449Z"}];
localStorage.setItem('item', JSON.stringify(myObj));

Then when you want to retrieve data, you need to parse the string to object again.

var getObj = JSON.parse(localStorage.getItem('item'));
Economically answered 21/1, 2020 at 6:11 Comment(1)
Thanks,i got concept of localstorage clearedAlign
V
17

It is recommended using an abstraction library for many of the features discussed here, as well as better compatibility. There are lots of options:

Varney answered 12/3, 2015 at 7:59 Comment(1)
Please revise to include "why" it is recommended to use an abstraction lib, especially when there is an extremely easy and supported method to encode/decode to strings (i.e. JSON.stringify/parse) which adequately supports 99% of use cases.Birdt
T
14

localDataStorage is a synchronous JavaScript interface for the HTML5 localStorage API that--

  1. transparently sets/gets key values using data "types" such as Array, BigInt, Boolean, Date, Float, Integer, Object and String;
  2. provides lightweight data obfuscation;
  3. intelligently compresses strings (to save storage space);
  4. facilitates robust lookup including query by key (name), query by (key) value and query by existence (boolean check);
  5. enforces segmented shared storage within the same domain by prefixing keys;
  6. allows you to respond to localStorage change events on the same page/tab that fired them;
  7. broadcasts change events across the origin for the benefit of other windows/tabs;
  8. lets you easily work with arrays using dedicated Array Keys; and
  9. offers Memory Keys (that can be backed up to disk) for the fastest read times possible.

[DISCLAIMER] I am the author of the utility [/DISCLAIMER]

Basic 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.

Teucer answered 17/5, 2017 at 0:58 Comment(4)
This is a brilliant resource and just what I need. I'm doing Ionic apps with AngularJS where I need to save certain javascript objects in localStorage and up to this point I've just been doing JSON.parse and JSON.stringify, and they work, but it's a bit more cumbersome than being able to just use a utility like this one. I'm going to try it.Jake
No offense, but your lib seems like overkill for 99% use cases. And to not even mention the lighter approach (e.e. JSON.stringify/parse) or alternative libraries seems misleading to me.Birdt
Sorry, let me be a bit more constructive/positive: This answer could be improved by comparing against alternatives approaches. Why and when does your library best alternatives?Birdt
Misleading? No, not really. I'm not on the hook to mention other libraries. The answers directly above and below mention several libraries: I'm sure they're all worthy. JSON.stringify is cited plentifully on this page; no need for me to be redundant about it. My library does some things most others don't, but I don't expect anyone to consider it unless they need it (I know I wouldn't). Beyond that, it's unclear why you're critical of something because it does more than bare minimum. I'd be wasting time offering yet another "me too" solution.Teucer
D
7

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:

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

Dowsabel answered 28/7, 2016 at 15:41 Comment(0)
G
6

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).

persisto features

(Disclaimer: I am the author.)

Glomerulonephritis answered 16/6, 2016 at 17:10 Comment(1)
Still working on my readme, but my version doesn't require jQuery, as it appears persisto does, but it does provide an alternative for dealing with jQuery element Objects. I will add more in the near future, as I work with it more, to help it further handle different jQuery Objects and maintain things like persistent data. Also, +1 for trying to provide a simpler solution! Also, it uses all the traditional methods of localStroage; exp: var lsh = new localStorageHelper(); lsh.setItem('bob', 'bill'); Also includes events.Cleaves
T
6

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);
    }
}

Example usage:

// 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");
Triangular answered 30/5, 2018 at 7:34 Comment(0)
V
4

https://github.com/adrianmay/rhaboo 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.

Vania answered 1/10, 2014 at 11:14 Comment(0)
E
3
localStorage.setItem('obj',JSON.stringify({name:'Akash'})); // Set Object in localStorage
localStorage.getItem('obj'); // Get Object from localStorage

sessionStorage.setItem('obj',JSON.stringify({name:'Akash'})); // Set Object in sessionStorage
sessionStorage.getItem('obj'); // Get Object from sessionStorage
Expressman answered 16/12, 2021 at 18:33 Comment(0)
A
2

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');

https://github.com/zevero/simpleWebstorage

Arly answered 28/9, 2016 at 10:30 Comment(0)
U
1

Here is some extended version of the code posted by danott:

It'll also implement a 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') //..undefined
    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')
//...

// Configuration 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 took me hours to find out why a simple get = localStorage.getItem; don't work.

Ungotten answered 10/2, 2015 at 20:25 Comment(2)
It's generally not a good idea to monkey patch globals like this. It can break code and it's not future compatible.Sharyl
danott's answer is now deleted. It was deleted without any explanation near the end of 2019 in a mass deletion of answers by a moderator.Ascensive
J
1

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.

The thing I made.

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.

Jaquith answered 28/11, 2015 at 20:39 Comment(1)
Object.observe is deprecated in all major browsers, now.Sharyl
S
1

I found a way to make it work with objects that have cyclic references.

Let's make an object with cyclic references.

obj = {
    L: {
        L: { v: 'lorem' },
        R: { v: 'ipsum' }
    },
    R: {
        L: { v: 'dolor' },
        R: {
            L: { v: 'sit' },
            R: { v: 'amet' }
        }
    }
}
obj.R.L.uncle = obj.L;
obj.R.R.uncle = obj.L;
obj.R.R.L.uncle = obj.R.L;
obj.R.R.R.uncle = obj.R.L;
obj.L.L.uncle = obj.R;
obj.L.R.uncle = obj.R;

We can't do JSON.stringify here, because of the circular references.

circularUncle

LOCALSTORAGE.CYCLICJSON has .stringify and .parse just like normal JSON, but works with objects with circular references. ("Works" meaning parse(stringify(obj)) and obj are deep equal AND have identical sets of 'inner equalities')

But we can just use the shortcuts:

LOCALSTORAGE.setObject('latinUncles', obj)
recovered = LOCALSTORAGE.getObject('latinUncles')

Then, recovered will be "the same" to obj, in the following sense:

[
obj.L.L.v === recovered.L.L.v,
obj.L.R.v === recovered.L.R.v,
obj.R.L.v === recovered.R.L.v,
obj.R.R.L.v === recovered.R.R.L.v,
obj.R.R.R.v === recovered.R.R.R.v,
obj.R.L.uncle === obj.L,
obj.R.R.uncle === obj.L,
obj.R.R.L.uncle === obj.R.L,
obj.R.R.R.uncle === obj.R.L,
obj.L.L.uncle === obj.R,
obj.L.R.uncle === obj.R,
recovered.R.L.uncle === recovered.L,
recovered.R.R.uncle === recovered.L,
recovered.R.R.L.uncle === recovered.R.L,
recovered.R.R.R.uncle === recovered.R.L,
recovered.L.L.uncle === recovered.R,
recovered.L.R.uncle === recovered.R
]

Here is the implementation of LOCALSTORAGE

LOCALSTORAGE = (function(){
  "use strict";
  var ignore = [Boolean, Date, Number, RegExp, String];
  function primitive(item){
    if (typeof item === 'object'){
      if (item === null) { return true; }
      for (var i=0; i<ignore.length; i++){
        if (item instanceof ignore[i]) { return true; }
      }
      return false;
    } else {
      return true;
    }
  }
  function infant(value){
    return Array.isArray(value) ? [] : {};
  }
  function decycleIntoForest(object, replacer) {
    if (typeof replacer !== 'function'){
      replacer = function(x){ return x; }
    }
    object = replacer(object);
    if (primitive(object)) return object;
    var objects = [object];
    var forest  = [infant(object)];
    var bucket  = new WeakMap(); // bucket = inverse of objects 
    bucket.set(object, 0);    
    function addToBucket(obj){
      var result = objects.length;
      objects.push(obj);
      bucket.set(obj, result);
      return result;
    }
    function isInBucket(obj){ return bucket.has(obj); }
    function processNode(source, target){
      Object.keys(source).forEach(function(key){
        var value = replacer(source[key]);
        if (primitive(value)){
          target[key] = {value: value};
        } else {
          var ptr;
          if (isInBucket(value)){
            ptr = bucket.get(value);
          } else {
            ptr = addToBucket(value);
            var newTree = infant(value);
            forest.push(newTree);
            processNode(value, newTree);
          }
          target[key] = {pointer: ptr};
        }
      });
    }
    processNode(object, forest[0]);
    return forest;
  };
  function deForestIntoCycle(forest) {
    var objects = [];
    var objectRequested = [];
    var todo = [];
    function processTree(idx) {
      if (idx in objects) return objects[idx];
      if (objectRequested[idx]) return null;
      objectRequested[idx] = true;
      var tree = forest[idx];
      var node = Array.isArray(tree) ? [] : {};
      for (var key in tree) {
        var o = tree[key];
        if ('pointer' in o) {
          var ptr = o.pointer;
          var value = processTree(ptr);
          if (value === null) {
            todo.push({
              node: node,
              key: key,
              idx: ptr
            });
          } else {
            node[key] = value;
          }
        } else {
          if ('value' in o) {
            node[key] = o.value;
          } else {
            throw new Error('unexpected')
          }
        }
      }
      objects[idx] = node;
      return node;
    }
    var result = processTree(0);
    for (var i = 0; i < todo.length; i++) {
      var item = todo[i];
      item.node[item.key] = objects[item.idx];
    }
    return result;
  };
  var console = {
    log: function(x){
      var the = document.getElementById('the');
      the.textContent = the.textContent + '\n' + x;
	},
	delimiter: function(){
      var the = document.getElementById('the');
      the.textContent = the.textContent +
		'\n*******************************************';
	}
  }
  function logCyclicObjectToConsole(root) {
    var cycleFree = decycleIntoForest(root);
    var shown = cycleFree.map(function(tree, idx) {
      return false;
    });
    var indentIncrement = 4;
    function showItem(nodeSlot, indent, label) {
      var leadingSpaces = ' '.repeat(indent);
      var leadingSpacesPlus = ' '.repeat(indent + indentIncrement);
      if (shown[nodeSlot]) {
        console.log(leadingSpaces + label + ' ... see above (object #' + nodeSlot + ')');
      } else {
        console.log(leadingSpaces + label + ' object#' + nodeSlot);
        var tree = cycleFree[nodeSlot];
        shown[nodeSlot] = true;
        Object.keys(tree).forEach(function(key) {
          var entry = tree[key];
          if ('value' in entry) {
            console.log(leadingSpacesPlus + key + ": " + entry.value);
          } else {
            if ('pointer' in entry) {
              showItem(entry.pointer, indent + indentIncrement, key);
            }
          }
        });
      }
    }
	console.delimiter();
    showItem(0, 0, 'root');
  };
  function stringify(obj){
    return JSON.stringify(decycleIntoForest(obj));
  }
  function parse(str){
    return deForestIntoCycle(JSON.parse(str));
  }
  var CYCLICJSON = {
    decycleIntoForest: decycleIntoForest,
    deForestIntoCycle : deForestIntoCycle,
    logCyclicObjectToConsole: logCyclicObjectToConsole,
    stringify : stringify,
    parse : parse
  }
  function setObject(name, object){
    var str = stringify(object);
    localStorage.setItem(name, str);
  }
  function getObject(name){
    var str = localStorage.getItem(name);
    if (str===null) return null;
    return parse(str);
  }
  return {
    CYCLICJSON : CYCLICJSON,
    setObject  : setObject,
    getObject  : getObject
  }
})();
obj = {
	L: {
		L: { v: 'lorem' },
		R: { v: 'ipsum' }
	},
	R: {
		L: { v: 'dolor' },
		R: {
			L: { v: 'sit' },
			R: { v: 'amet' }
		}
	}
}
obj.R.L.uncle = obj.L;
obj.R.R.uncle = obj.L;
obj.R.R.L.uncle = obj.R.L;
obj.R.R.R.uncle = obj.R.L;
obj.L.L.uncle = obj.R;
obj.L.R.uncle = obj.R;

// LOCALSTORAGE.setObject('latinUncles', obj)
// recovered = LOCALSTORAGE.getObject('latinUncles')
// localStorage not available inside fiddle ):
LOCALSTORAGE.CYCLICJSON.logCyclicObjectToConsole(obj)
putIntoLS = LOCALSTORAGE.CYCLICJSON.stringify(obj);
recovered = LOCALSTORAGE.CYCLICJSON.parse(putIntoLS);
LOCALSTORAGE.CYCLICJSON.logCyclicObjectToConsole(recovered);

var the = document.getElementById('the');
the.textContent = the.textContent + '\n\n' +
JSON.stringify(
[
obj.L.L.v === recovered.L.L.v,
obj.L.R.v === recovered.L.R.v,
obj.R.L.v === recovered.R.L.v,
obj.R.R.L.v === recovered.R.R.L.v,
obj.R.R.R.v === recovered.R.R.R.v,
obj.R.L.uncle === obj.L,
obj.R.R.uncle === obj.L,
obj.R.R.L.uncle === obj.R.L,
obj.R.R.R.uncle === obj.R.L,
obj.L.L.uncle === obj.R,
obj.L.R.uncle === obj.R,
recovered.R.L.uncle === recovered.L,
recovered.R.R.uncle === recovered.L,
recovered.R.R.L.uncle === recovered.R.L,
recovered.R.R.R.uncle === recovered.R.L,
recovered.L.L.uncle === recovered.R,
recovered.L.R.uncle === recovered.R
]
)
<pre id='the'></pre>
Seng answered 12/11, 2019 at 19:38 Comment(0)
J
1

Circular References

In this answer I focus on data-only objects (without functions, etc.) with circular references and develop ideas mentioned by maja and mathheadinclouds (I use his test case and my code is several times shorter).

Actually, we can use JSON.stringify with a proper replacer - if the source object contains multi-references to some object, or contains circular references then we reference it by special path-string (similar to JSONPath).

// JSON.strigify replacer for objects with circ ref
function refReplacer() {
  let m = new Map(), v = new Map(), init = null;

  return function(field, value) {
    let p = m.get(this) + (Array.isArray(this) ? `[${field}]` : '.' + field);
    let isComplex = value === Object(value)

    if (isComplex) m.set(value, p);

    let pp = v.get(value)||'';
    let path = p.replace(/undefined\.\.?/, '');
    let val = pp ? `#REF:${pp[0] == '[' ? '$':'$.'}${pp}` : value;

    !init ? (init=value) : (val===init ? val="#REF:$" : 0);
    if(!pp && isComplex) v.set(value, path);

    return val;
  }
}


// ---------------
// TEST
// ---------------

// Generate obj with duplicate/circular references
let obj = {
    L: {
        L: { v: 'lorem' },
        R: { v: 'ipsum' }
    },
    R: {
        L: { v: 'dolor' },
        R: {
            L: { v: 'sit' },
            R: { v: 'amet' }
        }
    }
}
obj.R.L.uncle = obj.L;
obj.R.R.uncle = obj.L;
obj.R.R.L.uncle = obj.R.L;
obj.R.R.R.uncle = obj.R.L;
obj.L.L.uncle = obj.R;
obj.L.R.uncle = obj.R;
testObject = obj;

let json = JSON.stringify(testObject, refReplacer(), 4);

console.log("Test Object\n", testObject);
console.log("JSON with JSONpath references\n", json);

Parse such JSON content with JSONpath-like references:

// Parse JSON content with JSONpath references to object
function parseRefJSON(json) {
  let objToPath = new Map();
  let pathToObj = new Map();
  let o = JSON.parse(json);

  let traverse = (parent, field) => {
    let obj = parent;
    let path = '#REF:$';

    if (field !== undefined) {
      obj = parent[field];
      path = objToPath.get(parent) + (Array.isArray(parent) ? `[${field}]` : `${field ? '.' + field : ''}`);
    }

    objToPath.set(obj, path);
    pathToObj.set(path, obj);

    let ref = pathToObj.get(obj);
    if (ref) parent[field] = ref;

    for (let f in obj) if (obj === Object(obj)) traverse(obj, f);
  }

  traverse(o);
  return o;
}


// ---------------
// TEST 1
// ---------------

let json = `
{
    "L": {
        "L": {
            "v": "lorem",
            "uncle": {
                "L": {
                    "v": "dolor",
                    "uncle": "#REF:$.L"
                },
                "R": {
                    "L": {
                        "v": "sit",
                        "uncle": "#REF:$.L.L.uncle.L"
                    },
                    "R": {
                        "v": "amet",
                        "uncle": "#REF:$.L.L.uncle.L"
                    },
                    "uncle": "#REF:$.L"
                }
            }
        },
        "R": {
            "v": "ipsum",
            "uncle": "#REF:$.L.L.uncle"
        }
    },
    "R": "#REF:$.L.L.uncle"
}`;

let testObject = parseRefJSON(json);

console.log("Test Object\n", testObject);


// ---------------
// TEST 2
// ---------------

console.log('Tests from mathheadinclouds answer: ');

let recovered = testObject;

let obj = { // Original object
    L: {
        L: { v: 'lorem' },
        R: { v: 'ipsum' }
    },
    R: {
        L: { v: 'dolor' },
        R: {
            L: { v: 'sit' },
            R: { v: 'amet' }
        }
    }
}
obj.R.L.uncle = obj.L;
obj.R.R.uncle = obj.L;
obj.R.R.L.uncle = obj.R.L;
obj.R.R.R.uncle = obj.R.L;
obj.L.L.uncle = obj.R;
obj.L.R.uncle = obj.R;

[
  obj.L.L.v === recovered.L.L.v,
  obj.L.R.v === recovered.L.R.v,
  obj.R.L.v === recovered.R.L.v,
  obj.R.R.L.v === recovered.R.R.L.v,
  obj.R.R.R.v === recovered.R.R.R.v,
  obj.R.L.uncle === obj.L,
  obj.R.R.uncle === obj.L,
  obj.R.R.L.uncle === obj.R.L,
  obj.R.R.R.uncle === obj.R.L,
  obj.L.L.uncle === obj.R,
  obj.L.R.uncle === obj.R,
  recovered.R.L.uncle === recovered.L,
  recovered.R.R.uncle === recovered.L,
  recovered.R.R.L.uncle === recovered.R.L,
  recovered.R.R.R.uncle === recovered.R.L,
  recovered.L.L.uncle === recovered.R,
  recovered.L.R.uncle === recovered.R
].forEach(x => console.log('test pass: ' + x));

To load/save the resulting JSON content into storage, use the following code:

localStorage.myObject = JSON.stringify(testObject, refReplacer());  // Save
testObject = parseRefJSON(localStorage.myObject);                   // Load
Jackboot answered 4/8, 2020 at 9:26 Comment(1)
Thank you for answering with the obvious approach for 99% of use cases, and including the tips about circular references which is a huge gotcha for many people.Birdt
B
1

This question has been answered sufficiently from the JavaScript-only perspective, and others have already noted that both localStorage.getItem and localStorage.setItem have no concept of objects—they handle strings and strings only. This answer provides a TypeScript-friendly solution that incorporates what others have suggested in JavaScript-only solutions.

TypeScript 4.2.3

Storage.prototype.setObject = function (key: string, value: unknown) {
  this.setItem(key, JSON.stringify(value));
};

Storage.prototype.getObject = function (key: string) {
  const value = this.getItem(key);
  if (!value) {
    return null;
  }

  return JSON.parse(value);
};

declare global {
  interface Storage {
    setObject: (key: string, value: unknown) => void;
    getObject: (key: string) => unknown;
  }
}

Usage

localStorage.setObject('ages', [23, 18, 33, 22, 58]);
localStorage.getObject('ages');

Explanation

We declare both setObject and getObject functions on the Storage prototype—localStorage is an instance of this type. There's nothing special we really need to note besides the null handling in getObject. Since getItem can return null, we must exit early since calling JSON.parse on a null value will throw a runtime exception.

After declaring the functions on the Storage prototype, we include their type definitions on the Storage type in the global namespace.

Note: If we defined these functions with arrow functions, we'd need to assume that the storage object we're calling is always localStorage, which might not be true. For instance, the above code will add setObject and getObject support to sessionStorage as well.

Basilius answered 4/4, 2021 at 19:53 Comment(1)
It's generally not a good idea to monkey-patch a global shipped by the browser. It can break other code, and it's not future-compatible.Sharyl
C
0

I suggest using Jackson-js. It is a library that handles serializing and deserializing of Objects while retaining their structure, based on decorators.

The library handles all the pitfalls such as cyclic reference, attributes aliasing, etc.

Simply describe your class using the @JsonProperty() and @JsonClassType() decorators.

Serialize your object using:

const objectMapper = new ObjectMapper();
localstore.setItem(key, objectMapper.stringify<yourObjectType>(yourObject));

For slightly more detailed explanation, check my answer here:

Typescript objects serialization?

And the Jackson-js tutorial here:

Jackson-js: Powerful JavaScript decorators to serialize/deserialize objects into JSON and vice versa (Part 1)

Carrelli answered 19/3, 2021 at 10:43 Comment(1)
This answer could be improved by comparing against alternatives approaches.Birdt
G
-6
localStorage.setItem('user', JSON.stringify(user));

Then to retrieve it from the store and convert to an object again:

var user = JSON.parse(localStorage.getItem('user'));

If we need to delete all entries of the store we can simply do:

localStorage.clear();
Giuliana answered 6/2, 2020 at 5:2 Comment(1)
This is a 10-year-old question. Do you think your answer adds anything not already covered by the other answers?Mincemeat

© 2022 - 2024 — McMap. All rights reserved.