Design pattern to check if a JavaScript object has changed

Here is a function down below that will return an array/object of changed objects when supplied with an old array/object of objects and a new array of objects:

// intended to compare objects of identical shape; ideally static.
//
// any top-level key with a primitive value which exists in `previous` but not
// in `current` returns `undefined` while vice versa yields a diff.
//
// in general, the input type determines the output type. that is if `previous`
// and `current` are objects then an object is returned. if arrays then an array
// is returned, etc.
const getChanges = (previous, current) => {
  if (isPrimitive(previous) && isPrimitive(current)) {
    if (previous === current) {
      return "";
    }

    return current;
  }

  if (isObject(previous) && isObject(current)) {
    const diff = getChanges(Object.entries(previous), Object.entries(current));

    return diff.reduce((merged, [key, value]) => {
      return {
        ...merged,
        [key]: value
      }
    }, {});
  }

  const changes = [];

  if (JSON.stringify(previous) === JSON.stringify(current)) {
    return changes;
  }

  for (let i = 0; i < current.length; i++) {
    const item = current[i];

    if (JSON.stringify(item) !== JSON.stringify(previous[i])) {
      changes.push(item);
    }
  }

  return changes;
};

For Example:

const arr1 = [1, 2, 3, 4]
const arr2 = [4, 4, 2, 4]

console.log(getChanges(arr1, arr2)) // [4,4,2]

const obj1 = {
  foo: "bar",
  baz: [
    1, 2, 3
  ],
  qux: {
    hello: "world"
  },
  bingo: "name-o",
}

const obj2 = {
  foo: "barx",
  baz: [
    1, 2, 3, 4
  ],
  qux: {
    hello: null
  },
  bingo: "name-o",
}

console.log(getChanges(obj1.foo, obj2.foo))     // barx
console.log(getChanges(obj1.bingo, obj2.bingo)) // ""
console.log(getChanges(obj1.baz, obj2.baz))     // [4]
console.log(getChanges(obj1, obj2))             // {foo:'barx',baz:[1,2,3,4],qux:{hello:null}}

const obj3 = [{ name: 'test01', age: 10 }, { name: 'test02', age: 20 }, { name: 'test03', age: 30 }]
const obj4 = [{ name: 'test01', age: 10 }, { name: 'test02', age: 20 }, { name: 'test03', age: 20 }]

console.log(getChanges(obj3, obj4)) // [{name:'test03', age:20}]

Utility functions used:

// not required for this example but aid readability of the main function
const typeOf = o => Object.prototype.toString.call(o);
const isObject = o => o !== null && !Array.isArray(o) && typeOf(o).split(" ")[1].slice(0, -1) === "Object";

const isPrimitive = o => {
  switch (typeof o) {
    case "object": {
      return false;
    }
    case "function": {
      return false;
    }
    default: {
      return true;
    }
  }
};

You would simply have to export the full list of edited values client side, compare it with the old list, and then send the list of changes off to the server.

Hope this helps!


With ES6 we can use Proxy

to accomplish this task: intercept an Object write, and mark it as dirty.

Proxy allows to create a handler Object that can trap, manipulate, and than forward changes to the original target Object, basically allowing to reconfigure its behavior.
The trap we're going to adopt to intercept Object writes is the handler set().

At this point we can add a non-enumerable property flag like i.e: _isDirty using Object.defineProperty() to mark our Object as modified, dirty.

When using traps (in our case the handler's set()) no changes are applied nor reflected to the Objects, therefore we need to forward the argument values to the target Object using Reflect.set().

Finally, to retrieve the modified objects, filter() the Array with our proxy Objects in search of those having its own Property "_isDirty".

// From server:
const dataOrg = [
  {id:1, name:'a', age:10},
  {id:2, name:'b', age:20},
  {id:3, name:'c', age:30}
];

// Mirror data from server to observable Proxies:
const data = dataOrg.map(ob => new Proxy(ob, {
  set() {
    Object.defineProperty(ob, "_isDirty", {value: true}); // Flag
    return Reflect.set(...arguments); // Forward trapped args to ob
  }
}));

// From now on, use proxied data. Let's change some values:
data[0].name = "Lorem";
data[0].age = 42;
data[2].age = 31;

// Collect modified data
const dataMod = data.filter(ob => ob.hasOwnProperty("_isDirty"));

// Test what we're about to send back to server:
console.log(JSON.stringify(dataMod, null, 2));

Without using .defineProperty()

If for some reason you don't feel comfortable into tapping into the original object adding extra properties as flags, you could instead populate immediately the dataMod (array with modified Objects) with references:

const dataOrg = [
  {id:1, name:'a', age:10},
  {id:2, name:'b', age:20},
  {id:3, name:'c', age:30}
];

// Prepare array to hold references to the modified Objects
const dataMod = [];

const data = dataOrg.map(ob => new Proxy(ob, {
  set() {
    if (dataMod.indexOf(ob) < 0) dataMod.push(ob); // Push reference
    return Reflect.set(...arguments); 
  }
}));

data[0].name = "Lorem";
data[0].age = 42;
data[2].age = 31;

console.log(JSON.stringify(dataMod, null, 2));
  • Can I Use - Proxy (IE)
  • Proxy - handler.set()
  • Global Objects - Reflect
  • Reflect.set()
  • Object.defineProperty()
  • Object.hasOwnProperty()

Here are a few ideas.

  1. Use a framework. You spoke of Angular.

  2. Use Proxies, though Internet Explorer has no support for it.

  3. Instead of using classic properties, maybe use Object.defineProperty's set/get to achieve some kind of change tracking.

  4. Use getter/setting functions to store data instead of properties: getName() and setName() for example. Though this the older way of doing what defineProperty now does.

  5. Whenever you bind your data to your form elements, set a special property that indicates if the property has changed. Something like __hasChanged. Set to true if any property on the object changes.

  6. The old school bruteforce way: keep your original list of data that came from the server, deep copy it into another list, bind your form controls to the new list, then when the user clicks submit, compare the objects in the original list to the objects in the new list, plucking out the changed ones as you go. Probably the easiest, but not necessarily the cleanest.

  7. A different take on #6: Attach a special property to each object that always returns the original version of the object:

    var myData = [{name: "Larry", age: 47}];
    var dataWithCopyOfSelf = myData.map(function(data) {  
        Object.assign({}, data, { original: data });
    });
    // now bind your form to dataWithCopyOfSelf.

Of course, this solution assumes a few things: (1) that your objects are flat and simple since Object.assign() doesn't deep copy, (2) that your original data set will never be changed, and (3) that nothing ever touches the contents of original.

There are a multitude of solutions out there.