Skip to content

Latest commit

 

History

History
430 lines (329 loc) · 10.7 KB

12.implement-Immutability-helper.md

File metadata and controls

430 lines (329 loc) · 10.7 KB

12. implement Immutability helpers

Immutability is a fundamental concept in JavaScript, especially when working with state management in frameworks like React or Redux. The idea is to ensure that data is not directly modified, but instead, new copies of the data structures are created with the required changes. This helps in maintaining predictable state and avoiding unintended side effects.

Using immutability helpers in JavaScript provides numerous benefits such as improving performance, ensuring predictable state updates, enabling time-travel debugging, and maintaining data integrity in concurrent applications. Libraries like immer and immutability-helper are popular tools that assist developers in managing immutable data structures efficiently.

Problem

https://bigfrontend.dev/problem/implement-Immutability-helper

Problem Description

If you use React, you would meet the scenario to copy the state for a slight change.

For example, for following state

const state = {
  a: {
    b: {
      c: 1,
    },
  },
  d: 2,
};

if we are to modify d to a new state, we could use _.cloneDeep, but it is not efficient because state.a is cloned while we don't need to change that.

A better way is to do shallow copy like this

const newState = {
  ...state,
  d: 3,
};

now is the problem, if we want to modify c, we would have to do something like

const newState = {
  ...state,
  a: {
    ...state.a,
    b: {
      ...state.b,
      c: 2,
    },
  },
};

We can see that for simple data structure it would be enough to use spread operator, but for complex data structures, it is verbose.

Here comes the Immutability Helper, you are asked to implement your own Immutability Helper update(), which supports following features.

1. {$push: array} push() all the items in array on the target.

const arr = [1, 2, 3, 4];
const newArr = update(arr, { $push: [5, 6] });
// [1, 2, 3, 4, 5, 6]

2. {$set: any} replace the target

const state = {
  a: {
    b: {
      c: 1,
    },
  },
  d: 2,
};

const newState = update(state, { a: { b: { c: { $set: 3 } } } });
/*
{
  a: {
    b: {
      c: 3
    }
  },
  d: 2
}
*/

Notice that we could also update array elements with $set

const arr = [1, 2, 3, 4];
const newArr = update(arr, { 0: { $set: 0 } });
//  [0, 2, 3, 4]

3. {$merge: object} merge object to the location

const state = {
  a: {
    b: {
      c: 1,
    },
  },
  d: 2,
};

const newState = update(state, { a: { b: { $merge: { e: 5 } } } });
/*
{
  a: {
    b: {
      c: 3,
      e: 5
    }
  },
  d: 2
}
*/

4. {$apply: function} custom replacer

const arr = [1, 2, 3, 4];
const newArr = update(arr, { 0: { $apply: (item) => item * 2 } });
// [2, 2, 3, 4]

Solution

/**
 * @param {any} data
 * @param {Object} command
 */
function update(data, command) {
  if (typeof data !== 'object' && !Array.isArray(data)) {
    throw new Error();
  }

  let copiedData = copy(data);
  _update(copiedData, command);
  return copiedData;
}

function _update(data, command) {
  for (const key in command) {
    if (key === '$push' && Array.isArray(command[key]) && Array.isArray(data)) {
      data.push(...command[key]);
      return;
    }

    if (
      typeof command[key] === 'object' &&
      command[key].hasOwnProperty('$set')
    ) {
      data[key] = command[key].$set;
      return;
    }

    if (
      typeof command[key] === 'object' &&
      command[key].hasOwnProperty('$apply') &&
      Array.isArray(data)
    ) {
      if (data[key]) {
        data[key] = command[key].$apply(data[key]);
        return;
      }
    }

    if (
      typeof command[key] === 'object' &&
      command[key].hasOwnProperty('$merge')
    ) {
      if (typeof data[key] === 'object') {
        data[key] = {
          ...data[key],
          ...command[key].$merge,
        };
        return;
      } else {
        throw new Error();
      }
    }

    if (typeof command[key] === 'object') {
      _update(data[key], command[key]);
    }
  }
}

function copy(data) {
  let newData;

  if (Array.isArray(data)) {
    newData = [];

    for (const el of data) {
      if (Array.isArray(el) || typeof el === 'object') {
        newData.push(copy(el));
      } else {
        newData.push(el);
      }
    }
  } else if (typeof data === 'object') {
    newData = {};

    for (const key in data) {
      if (typeof data[key] === 'object' || Array.isArray(data[key])) {
        newData[key] = copy(data[key]);
      } else {
        newData[key] = data[key];
      }
    }
  }

  return newData;
}

Usage

const data = {
  a: {
    b: {
      c: 1,
    },
  },
};

const command = {
  a: {
    b: {
      $set: {
        c: 2,
      },
    },
  },
};

const updatedData = update(data, command);
console.log(updatedData); // { a: { b: { c: 2 } } }

Explanation

Sure, let's break down this code:

  1. copy function: This function takes an input data and creates a deep copy of it. It checks if the data is an array or an object and recursively copies all elements or properties. This is done to avoid mutating the original data.

  2. _update function: This is a helper function that takes the copied data and a command object. It iterates over the command object and checks for specific keys that determine how to update the data:

    • If the key is $push and both the command[key] and data are arrays, it pushes the elements of command[key] into the data array.
    • If the command[key] is an object and has a $set property, it sets the data[key] to the value of command[key].$set.
    • If the command[key] is an object and has a $apply property and data is an array, it applies the function command[key].$apply to data[key] if it exists.
    • If the command[key] is an object and has a $merge property, it merges the properties of command[key].$merge into data[key] if data[key] is an object.
    • If the command[key] is an object and doesn't match any of the above conditions, it recursively calls _update on data[key] and command[key].
  3. update function: This function is the main function that users interact with. It takes data and a command as input. It first checks if the data is an object or an array, throwing an error if it's not. It then creates a copy of the data and calls _update on the copied data and the command. Finally, it returns the updated copy.

  4. Usage: The usage example shows how to use the update function. It creates a data object and a command object that instructs the function to set data.a.b.c to 2. After calling update(data, command), it logs the updated data, which is { a: { b: { c: 2 } } }.

This code provides a way to update JavaScript objects or arrays in a declarative way, specifying what updates to make in the form of a command object. It's similar to how state updates are handled in MongoDB or in the React setState function.

Real World Examples

Here are some real-world examples and use cases where immutability helpers are beneficial:

1. State Management in React

React uses a virtual DOM and requires components to re-render efficiently. Immutable state updates help React to optimize these updates. Here’s an example using the immer library:

import produce from 'immer';

const initialState = { 
  todos: [{ text: 'Learn JavaScript', completed: false }]
};

const newState = produce(initialState, draft => {
  draft.todos[0].completed = true;
});

console.log(newState); // { todos: [{ text: 'Learn JavaScript', completed: true }] }
console.log(initialState); // { todos: [{ text: 'Learn JavaScript', completed: false }] }

2. Redux State Management

In Redux, immutability is crucial as reducers must be pure functions. Here’s an example using immutability-helper:

import update from 'immutability-helper';

const state = { 
  todos: ['Learn Redux'] 
};

const newState = update(state, {
  todos: { $push: ['Implement immutability-helper'] }
});

console.log(newState); // { todos: ['Learn Redux', 'Implement immutability-helper'] }
console.log(state); // { todos: ['Learn Redux'] }

3. Optimizing Performance in React

By ensuring objects are immutable, React components can use shallow comparison to determine if re-renders are necessary, improving performance.

class TodoList extends React.Component {
  shouldComponentUpdate(nextProps) {
    return nextProps.todos !== this.props.todos;
  }

  render() {
    return (
      <ul>
        {this.props.todos.map(todo => <li key={todo}>{todo}</li>)}
      </ul>
    );
  }
}

4. Functional Programming Practices

Immutability is a core principle of functional programming, helping to avoid side effects. Here's an example using native JavaScript methods:

const array = [1, 2, 3];
const newArray = [...array, 4];

console.log(newArray); // [1, 2, 3, 4]
console.log(array); // [1, 2, 3]

5. Concurrency and Multi-threading

In environments where multiple threads or processes might access and modify data, immutability ensures that data remains consistent and prevents race conditions.

6. Undo/Redo Functionality

Immutability helps implement undo/redo functionality in applications, such as text editors or drawing tools:

const pastStates = [];
const presentState = { text: 'Hello' };
const futureStates = [];

function undo() {
  if (pastStates.length === 0) return;

  futureStates.push(presentState);
  presentState = pastStates.pop();
}

function redo() {
  if (futureStates.length === 0) return;

  pastStates.push(presentState);
  presentState = futureStates.pop();
}

7. Predictable State Updates

In applications where the state needs to be predictable and debuggable, immutability ensures that state transitions are clear and traceable.

const state = {
  user: {
    name: 'Alice',
    age: 25
  }
};

const newState = {
  ...state,
  user: {
    ...state.user,
    age: 26
  }
};

console.log(state); // { user: { name: 'Alice', age: 25 } }
console.log(newState); // { user: { name: 'Alice', age: 26 } }

8. Data Integrity in Collaborative Apps

In collaborative apps where multiple users can modify data simultaneously, immutability helps maintain data integrity.

const doc = {
  content: 'Hello World'
};

const newDoc = {
  ...doc,
  content: 'Hello Collaborative World'
};

console.log(doc); // { content: 'Hello World' }
console.log(newDoc); // { content: 'Hello Collaborative World' }

Reference

Immutability Helpers

Understanding Immutability in JavaScript