Create immutability helper in JavaScript – part 2

Create an immutability helper like Immer produce() that allows modifications of the restricted objects in JavaScript.

Example

const obj = {
  a: {
    b: {
      c: 2
    }
  }
};

// object is frozen
// its properties cannot be updated
deepFreeze(obj);

// obj can only be updated through the produce function
const newState = produce(obj, draft => {
  draft.a.b.c = 3;
  draft.a.b.d = 4;
});

console.log(newState);
/*
{
  "a": {
    "b": {
      "c": 3,
      "d": 4
    }
  }
}
*/

// newState will also be frozen
// it cannot be updated
delete newState.a.b.c;
console.log(newState);

/*
{
  "a": {
    "b": {
      "c": 3,
      "d": 4
    }
  }
}
*/

To implement this we will first create a clone of the input obj and then pass this input object to the callback function of produce, for processing.

After processing, perform a deep comparison to check if there is any change or not. If there is no change then return the original input, else return the new updated one.

deep freeze the object before returning it so that it cannot be updated directly.

// function to deep freeze object
function deepFreeze(object) {
  // Retrieve the property names defined on object
  var propNames = Object.getOwnPropertyNames(object);

  // Freeze properties before freezing self
  for (let name of propNames) {
    let value = object[name];

    object[name] = value && typeof value === "object" ? 
      deepFreeze(value) : value;
  }

  return Object.freeze(object);
};

// function to deep check two objects
const deepEqual = (object1, object2) => {
  // get object keys
  const keys1 = Object.keys(object1);
  const keys2 = Object.keys(object2);
  
  // if mismatched keys
  if (keys1.length !== keys2.length) {
    return false;
  }

  for (const key of keys1) {
    // get the values
    const val1 = object1[key];
    const val2 = object2[key];
    
    // if both values are objects
    const areObjects = val1 && typeof val1 === "object" && val2 && typeof val2 === "object";
    
    // if are objects
    if(areObjects){
      // deep check again
      if(!deepEqual(val1, val2)){
        return false;
      }
    }
    // if are not objects
    // compare the values
    else if(!areObjects && val1 !== val2){
       return false;
    }
  }

  return true;
};

// main function to update the value
function produce(base, recipe) {
  // clone the frozen object
  let clone = JSON.parse(JSON.stringify(base));
  
  // pass the clone to the recipe
  // get the updated value
  recipe(clone);
  
  // if both are different
  // update the value 
  if(deepEqual(base, clone)) {
    clone = base;
  }
  
  // deep freeze
  deepFreeze(clone);
  
  // return the clone
  return clone;
};
const obj = {
  a: {
    b: {
      c: 2
    }
  }
};

// object is frozen
// its properties cannot be updated
deepFreeze(obj);

// obj can only be updated through the produce function
const newState = produce(obj, draft => {
  draft.a.b.c = 3;
  draft.a.b.d = 4;
});

console.log(newState);
/*
{
  "a": {
    "b": {
      "c": 3,
      "d": 4
    }
  }
}
*/

// newState will also be frozen
// it cannot be updated
delete newState.a.b.c;
console.log(newState);

/*
{
  "a": {
    "b": {
      "c": 3,
      "d": 4
    }
  }
}
*/