12/16 lessons75%

Examples of Immutability

In this lesson, we’ll take a hands-on approach to applying immutability in real-world scenarios. You’ll learn how immutable data structures can simplify your code and make it more reliable. We’ll focus on practical use cases and how immutability integrates with functional programming principles.

Todo App

Imagine a simple todo application where users can add, edit, and delete tasks. Using immutability ensures that each state change is predictable and traceable.

Initial State:

javascript
1
2
3
4
          const todos = [
  { id: 1, task: "Learn JavaScript", completed: false },
  { id: 2, task: "Practice Functional Programming", completed: false },
];
        

Adding a New Task:

Instead of mutating the array, use concat to create a new version:

javascript
1
2
3
4
5
6
          const addTodo = (todos, newTask) => {
  return todos.concat({ id: Date.now(), task: newTask, completed: false });
};

const updatedTodos = addTodo(todos, "Understand Immutability");
console.log(updatedTodos);
        

Toggling Task Completion:

Use map to return a new array with the updated task:

javascript
1
2
3
4
5
6
7
8
          const toggleTodo = (todos, id) => {
  return todos.map(todo =>
    todo.id === id ? { ...todo, completed: !todo.completed } : todo
  );
};

const toggledTodos = toggleTodo(todos, 1);
console.log(toggledTodos);
        

Deleting a Task:

Use filter to exclude the task to be deleted:

javascript
1
2
3
4
5
6
          const deleteTodo = (todos, id) => {
  return todos.filter(todo => todo.id !== id);
};

const remainingTodos = deleteTodo(todos, 2);
console.log(remainingTodos);
        

Update a User Profile

When managing user profiles, immutability ensures the original data is preserved.

Initial Profile:

javascript
1
2
3
4
5
6
7
8
          const userProfile = {
  name: "Alice",
  age: 25,
  preferences: {
    theme: "dark",
    notifications: true,
  },
};
        

Updating Nested Properties with Spread Operator:

Use the spread operator to update nested properties:

javascript
1
2
3
4
5
6
          const updateProfile = (profile, updates) => {
  return { ...profile, ...updates };
};

const updatedProfile = updateProfile(userProfile, { age: 26 });
console.log(updatedProfile);
        

Updating Deeply Nested Properties:

For deeply nested updates, combine spread operators:

javascript
1
2
3
4
5
6
7
8
9
          const updatePreferences = (profile, newPreferences) => {
  return {
    ...profile,
    preferences: { ...profile.preferences, ...newPreferences },
  };
};

const updatedPreferences = updatePreferences(userProfile, { theme: "light" });
console.log(updatedPreferences);
        

Functional State Management

Let’s build a small example of managing application state with immutable updates.

Initial State:

javascript
1
2
3
4
          const initialState = {
  count: 0,
  items: [],
};
        

Updating State with a Reducer Function:

A reducer function is a common pattern in functional programming.

javascript
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
          const reducer = (state, action) => {
  switch (action.type) {
    case "INCREMENT":
      return { ...state, count: state.count + 1 };
    case "ADD_ITEM":
      return { ...state, items: state.items.concat(action.payload) };
    default:
      return state;
  }
};

let currentState = initialState;

currentState = reducer(currentState, { type: "INCREMENT" });
console.log(currentState); // { count: 1, items: [] }

currentState = reducer(currentState, { type: "ADD_ITEM", payload: "New Item" });
console.log(currentState); // { count: 1, items: ["New Item"] }
        

Using Immer for Simpler Immutable Updates

Immer simplifies immutable updates by allowing mutable-like syntax:

javascript
1
2
3
4
5
6
7
8
9
10
          const produce = require("immer").produce;

const initialState = { name: "Bob", hobbies: ["reading", "coding"] };

const updatedState = produce(initialState, draft => {
  draft.hobbies.push("hiking");
});

console.log(updatedState); // { name: "Bob", hobbies: ["reading", "coding", "hiking"] }
console.log(initialState); // { name: "Bob", hobbies: ["reading", "coding"] }
        

Using Immutability in Applications Benefits

  1. 1
    Time Travel Debugging: Easily trace changes to application state over time.
  2. 2
    Predictable State Updates: No side effects, leading to more predictable behavior.
  3. 3
    Functional Patterns: Immutable data integrates seamlessly with functional techniques like reducers.

Conclusion

Now that you’ve seen practical examples of immutability, we’ll explore how to combine these ideas with first-class functions in the next chapter.