Search code examples
javascriptdesign-patterns

Design pattern to check if a JavaScript object has changed


I get from the server a list of objects

[{name:'test01', age:10},{name:'test02', age:20},{name:'test03', age:30}]

I load them into html controls for the user to edit. Then there is a button to bulk save the entire list back to the database.

Instead of sending the whole list I only want to send the subset of objects that were changed.

It can be any number of items in the array. I want to do something similar to frameworks like Angular that mark an object property like "pristine" when no change has been done to it. Then use that flag to only post to the server the items that are not "pristine", the ones that were modified.


Solution

  • 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!