From 76ae21c946b22563ecf390bd0b8c816dafc2f755 Mon Sep 17 00:00:00 2001 From: Shaban-Eissa Date: Wed, 29 May 2024 22:09:46 +0300 Subject: [PATCH] feat: building Immutability --- 12.implement-Immutability-helper.md | 430 ++++++++++++++++++++++++++++ 1 file changed, 430 insertions(+) create mode 100644 12.implement-Immutability-helper.md diff --git a/12.implement-Immutability-helper.md b/12.implement-Immutability-helper.md new file mode 100644 index 0000000..91899aa --- /dev/null +++ b/12.implement-Immutability-helper.md @@ -0,0 +1,430 @@ +# 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 + +```js +const state = { + a: { + b: { + c: 1, + }, + }, + d: 2, +}; +``` + +if we are to modify `d` to a new state, we could use [\_.cloneDeep](https://lodash.com/docs/4.17.15#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 + +```js +const newState = { + ...state, + d: 3, +}; +``` + +now is the problem, if we want to modify `c`, we would have to do something like + +```js +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](https://reactjs.org/docs/update.html), 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.** + +```js +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** + +```js +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` + +```js +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** + +```js +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** + +```js +const arr = [1, 2, 3, 4]; +const newArr = update(arr, { 0: { $apply: (item) => item * 2 } }); +// [2, 2, 3, 4] +``` + +# + +### Solution + +```js +/** + * @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 +```js +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: + +```javascript +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`: + +```javascript +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. + +```javascript +class TodoList extends React.Component { + shouldComponentUpdate(nextProps) { + return nextProps.todos !== this.props.todos; + } + + render() { + return ( + + ); + } +} +``` + +### 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: + +```javascript +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: + +```javascript +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. + +```javascript +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. + +```javascript +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](https://legacy.reactjs.org/docs/update.html) + +[Understanding Immutability in JavaScript](https://css-tricks.com/understanding-immutability-in-javascript) \ No newline at end of file