How to subscribe to object changes?

I wonder how to subscribe to the changes of a JavaScript object e.g. like Redux does. I read through a lot of JS documentations but I couldn't find a non-deprecated way to handle this problem (Object.protype.watch() as well as Object.observe() are deprecated). Moreover I read a few Stackoverflow questions regarding this topic but they are all at least 5 years old. To visualize my problem I'll show an example.

This could be an object I want to watch for:

const store = {
    anArray = [
    'Hi',
    'my',
    'name',
    'is'
  ]
}

.. and this a function which changes the store object:

function addAName() {
    store.anArray.push('Bob')
}

My goal in this example is to trigger the following function every time the store object changes

function storeChanged() {
    console.log('The store object has changed!')
}

Thank you in advance!

Answers:

Answer

Have you tried using Proxy from ECMA6? I think this is what you are looking for You only have to define a function as the set of the validator of the Proxy like this:

let validator = {
    set: function(target, key, value) {
        console.log(`The property ${key} has been updated with ${value}`);
        return true;
    }
};
let store = new Proxy({}, validator);
store.a = 'hello';
// console => The property a has been updated with hello
Answer

It's non-trivial.

There are several approaches that different tools (Redux, Angular, KnockoutJS, etc.) use.

  1. Channeling changes through functions - This is the approach Redux uses (more). You don't directly modify things, you pass them through reducers, which means Redux is aware that you've changed something.

  2. Diffing - Literally comparing the object tree to a previous copy of the object tree and acting on changes made. At least some versions of Angular/AngularJS use(d) this approach.

  3. Wrapping - (Kind of a variant on #1) Wrapping all modification operations on all objects in the tree (such as the push method on your array) with wrappers that notify a controller that they object they're on has been called — by wrapping those methods (and replacing simple data properties with accessor properties) and/or using Proxy objects. KnockoutJS uses a version of this approach.

Answer

I am not sure if there is a way to do it with native functionality and even if there is I think you won't be able to do this without some sort of abstraction.

It doesn't even work natively in react/redux either as you need to specifically call setState explicitly to trigger changes. I recommend a very observer pattern whose implementation roughly looks like this.

var store = {
  names: ['Bob', 'Jon', 'Doe']
}

var storeObservers = [];

Now, simple push your observer functions doesn't matter even if they are part of some component or module

storeObservers.push(MyComponent.handleStoreChange)

Now simple expose a function to change the store similar to setState

function changeStore(store) {
  store = store
  storeObservers.forEach(function(observer){
    observer()
  })
}

You can obviously modularized this to handle more complex situations or if you only want to change a part of state and allow observers to bind callbacks to partial state changes.

Answer

To solve this problem without any indirections (in using object) you can use proxy.

By wrapping all objects with observalbe you can edit your store frealy and _base keeps track of which property has changed.

const observalbe = (target, callback, _base = []) => {
  for (const key in target) {
    if (typeof target[key] === 'object')
      target[key] = observalbe(target[key], callback, [..._base, key])
  }
  return new Proxy(target, {
    set(target, key, value) {
      if (typeof value === 'object') value = observalbe(value, callback, [..._base, key])
      callback([..._base, key], target[key] = value)
      return value
    }
  })
}

const a = observalbe({
  a: [1, 2, 3],
  b: { c: { d: 1 } }
}, (key, val) => {
  console.log(key, val);
})

a.a.push(1)

a.b.c.d = 1
a.b = {}
a.b.c = 1

Answer

You can use Object.defineproperty() to create reactive getters/setters. It has good browser support and looks handy.

function Store() {

  let array = [];

  Object.defineProperty(this, 'array', {
    get: function() {
      console.log('Get:', array);
      return array;
    },
    set: function(value) {
      array = value;
      console.log('Set:', array)
    }
  });


}

var store = new Store();
store.array; //Get: []
store.array = [11]; //Set: [11]
store.array.push(5) //Set: [11, 5]
store.array = store.array.concat(1, 2, 3) //Set: [11, 5, 1, 2, 3]

Answer

Alejandro Riera's answer using Proxy is probably the correct approach in most cases. However, if you're willing to include a (small-ish, ~90kb) library, Vue.js can have watched properties on its instances, which can sort of do what you want.

It is probably overkill to use it for just change observation, but if your website has other uses for a reactive framework, it may be a good approach.

I sometimes use it as an object store without an associated element, like this:

const store = new Vue({
    data: {
        anArray: [
            'Hi',
            'my',
            'name',
            'is'
        ]
    },
    watch: {
        // whenever anArray changes, this function will run
        anArray: function () {
            console.log('The store object has changed:', this.anArray);
        }
    }
});

function addAName() {
    // push random strings as names
    const newName = '_' + Math.random().toString(36).substr(2, 6);
    store.anArray.push(newName);
}

// demo
setInterval(addAName, 5000);

Tags

Recent Questions

Top Questions

Home Tags Terms of Service Privacy Policy DMCA Contact Us Javascript

©2020 All rights reserved.