Deep JavaScript
Please support this book: buy it or donate
(Ad, please don’t block.)

7 Updating data destructively and non-destructively



In this chapter, we learn about two different ways of updating data:

The latter way is similar to first making a copy and then changing it destructively, but it does both at the same time.

7.1 Examples: updating an object destructively and non-destructively

The following code shows a function that updates object properties destructively and uses it on an object.

function setPropertyDestructively(obj, key, value) {
  obj[key] = value;
  return obj;
}

const obj = {city: 'Berlin', country: 'Germany'};
setPropertyDestructively(obj, 'city', 'Munich');
assert.deepEqual(obj, {city: 'Munich', country: 'Germany'});

The following code demonstrates non-destructive updating of an object:

function setPropertyNonDestructively(obj, key, value) {
  const updatedObj = {};
  for (const [k, v] of Object.entries(obj)) {
    updatedObj[k] = (k === key ? value : v);
  }
  return updatedObj;
}

const obj = {city: 'Berlin', country: 'Germany'};
const updatedObj = setPropertyNonDestructively(obj, 'city', 'Munich');

// We have created an updated object:
assert.deepEqual(updatedObj, {city: 'Munich', country: 'Germany'});

// But we didn’t change the original:
assert.deepEqual(obj, {city: 'Berlin', country: 'Germany'});

Spreading makes setPropertyNonDestructively() more concise:

function setPropertyNonDestructively(obj, key, value) {
  return {...obj, [key]: value};
}

Both versions of setPropertyNonDestructively() update shallowly: They only change the top level of an object.

7.2 Examples: updating an Array destructively and non-destructively

The following code shows a function that updates Array elements destructively and uses it on an Array.

function setElementDestructively(arr, index, value) {
  arr[index] = value;
}

const arr = ['a', 'b', 'c', 'd', 'e'];
setElementDestructively(arr, 2, 'x');
assert.deepEqual(arr, ['a', 'b', 'x', 'd', 'e']);

The following code demonstrates non-destructive updating of an Array:

function setElementNonDestructively(arr, index, value) {
  const updatedArr = [];
  for (const [i, v] of arr.entries()) {
    updatedArr.push(i === index ? value : v);
  }
  return updatedArr;
}

const arr = ['a', 'b', 'c', 'd', 'e'];
const updatedArr = setElementNonDestructively(arr, 2, 'x');
assert.deepEqual(updatedArr, ['a', 'b', 'x', 'd', 'e']);
assert.deepEqual(arr, ['a', 'b', 'c', 'd', 'e']);

.slice() and spreading make setElementNonDestructively() more concise:

function setElementNonDestructively(arr, index, value) {
  return [
    ...arr.slice(0, index), value, ...arr.slice(index+1)];
}

Both versions of setElementNonDestructively() update shallowly: They only change the top level of an Array.

7.3 Manual deep updating

So far, we have only updated data shallowly. Let’s tackle deep updating. The following code shows how to do it manually. We are changing name and employer.

const original = {name: 'Jane', work: {employer: 'Acme'}};
const updatedOriginal = {
  ...original,
  name: 'John',
  work: {
    ...original.work,
    employer: 'Spectre'
  },
};

assert.deepEqual(
  original, {name: 'Jane', work: {employer: 'Acme'}});
assert.deepEqual(
  updatedOriginal, {name: 'John', work: {employer: 'Spectre'}});

7.4 Implementing generic deep updating

The following function implements generic deep updating.

function deepUpdate(original, keys, value) {
  if (keys.length === 0) {
    return value;
  }
  const currentKey = keys[0];
  if (Array.isArray(original)) {
    return original.map(
      (v, index) => index === currentKey
        ? deepUpdate(v, keys.slice(1), value) // (A)
        : v); // (B)
  } else if (typeof original === 'object' && original !== null) {
    return Object.fromEntries(
      Object.entries(original).map(
        (keyValuePair) => {
          const [k,v] = keyValuePair;
          if (k === currentKey) {
            return [k, deepUpdate(v, keys.slice(1), value)]; // (C)
          } else {
            return keyValuePair; // (D)
          }
        }));
  } else {
    // Primitive value
    return original;
  }
}

If we see value as the root of a tree that we are updating, then deepUpdate() only deeply changes a single branch (line A and C). All other branches are copied shallowly (line B and D).

This is what using deepUpdate() looks like:

const original = {name: 'Jane', work: {employer: 'Acme'}};

const copy = deepUpdate(original, ['work', 'employer'], 'Spectre');
assert.deepEqual(copy, {name: 'Jane', work: {employer: 'Spectre'}});
assert.deepEqual(original, {name: 'Jane', work: {employer: 'Acme'}});