[Special Summer Sale] 40% OFF All Magento 2 Themes

Cart

How to store objects in HTML5 localStorage/sessionStorage

  • This topic is empty.
Viewing 15 posts - 1 through 15 (of 25 total)
  • Author
    Posts
  • #9136
    David Hoang
    Keymaster

    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.

    #9160
    David Hoang
    Keymaster

    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));
    
    #9158
    David Hoang
    Keymaster

    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.

    #9159
    David Hoang
    Keymaster

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

    #9157
    David Hoang
    Keymaster

    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;
      }
    }
    
    #9153
    David Hoang
    Keymaster

    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.

    #9155
    David Hoang
    Keymaster

    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")
    
    #9154
    David Hoang
    Keymaster

    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
    
    #9146
    David Hoang
    Keymaster

    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.

    #9156
    David Hoang
    Keymaster

    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

    #9139
    David Hoang
    Keymaster

    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.

    #9151
    David Hoang
    Keymaster

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

    #9140
    David Hoang
    Keymaster

    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.

    #9147
    David Hoang
    Keymaster

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

    #9149
    David Hoang
    Keymaster

    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

Viewing 15 posts - 1 through 15 (of 25 total)
  • You must be logged in to reply to this topic.