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:
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:
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:
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:
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:
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:
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:
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:
const initialState = {
count: 0,
items: [],
};
Updating State with a Reducer Function:
A reducer function is a common pattern in functional programming.
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:
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
- 1Time Travel Debugging: Easily trace changes to application state over time.
- 2Predictable State Updates: No side effects, leading to more predictable behavior.
- 3Functional 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.