Implement a simple immutability helper in JavaScript that allows a certain set of actions to update the frozen input object. The input object can only be updated through this function and the returned value is also frozen.
Note โ For simplicity, only one operation is allowed at a time.
Actions
_push_ : Array โ Pushes of the destination array in the input array.
const inputArr = [1, 2, 3, 4] const outputArr = update( inputArr, {_push_: [5, 6, 7]} ); console.log(outputArr); // [1,2,3,4,5,6,7]
_replace_ : Object | Array โ Replaces the destination value in the input object.
--- Object --- const state = { a: { b: { c: 1 } }, d: 2 }; const newState = update( state, {a: {b: { c: {_replace_: 3}}}} ); console.log(newState); /* { "a": { "b": { "c": 3 } }, "d": 2 } */ --- Array --- const inputArr = [1, 2, 3, 4] const outputArr = update( inputArr, {1: {_replace_: 10}} ); console.log(outputArr); // [1,10,3,4]
_merge_ : Object โ Merges the destination value in the input object.
const state = { a: { b: { c: 1 } }, d: 2 }; const newState = update( state, {a: {b: { _merge_ : {e: 5 }}}} ); console.log(newState); /* { "a": { "b": { "c": 1, "e": 5 } }, "d": 2 } */
_transform_ : Object | Array โ Transforms the destination value by passing it through this function.
const inputArr = {a: { b: 2}}; const outputArr = update(inputArr, {a: { b: {_transform_: (item) => item * 2}}}); console.log(outputArr); /* { "a": { "b": 4 } } */
The implementation of this is straightforward. The helper(data, action)
accepts two arguments, the action is always an Object.
Thus we can recursively deep traverse the object and in each call check if the current key is any of the actions then perform the action accordingly.
Otherwise depending if the input an array or object recursively call the same function with the current value to update.
Wrap this helper function inside another parent function. As the input object will frozen, we cannot directly update it, thus we will create a clone of it. Pass the clone for update through the helper function and in the end freeze the output before returning it back.
// function to deepfreeze function deepFreeze(object) { // Retrieve the property names defined on object var propNames = Object.getOwnPropertyNames(object); // Freeze properties before freezing self for (let name of propNames) { let value = object[name]; object[name] = value && typeof value === "object" ? deepFreeze(value) : value; } return Object.freeze(object); }; // function to perform the action function update(inputObj, action){ const clone = JSON.parse(JSON.stringify(inputObj)); function helper(target, action) { // iterate the entries of the action for (const [key, value] of Object.entries(action)) { // if the key is of action type // perform the action switch (key) { // add a new value case '_push_': return [...target, ...value]; // replace the entry case '_replace_': return value; // merge the values case '_merge_': if (!(target instanceof Object)) { throw Error("bad merge"); } return {...target, ...value}; // add the transformed value case '_transform_': return value(target); // for normal values default: // if it is an array if (target instanceof Array) { // create a copy const res = [...target]; // update the value res[key] = update(target[key], value); // return after update return res; } // if it is an object else { // recursively call the same function // and update the value return { ...target, [key]: update(target[key], value) } } } }; }; // perform the operation const output = helper(clone, action); // freeze the output deepFreeze(output); //return it return output; };
Test Case 1: _push_
const inputArr = [1, 2, 3, 4] const outputArr = update( inputArr, {_push_: [5, 6, 7]} ); console.log(outputArr); // [1,2,3,4,5,6,7]
Test Case 2: _replace_
const state = { a: { b: { c: 1 } }, d: 2 }; // freeze the object deepFreeze(state); const newState = update( state, {a: {b: { c: {_replace_: 3}}}} ); // does not updates // as output is frozen newState.a.b.c = 10; console.log(newState); /* { "a": { "b": { "c": 3 } }, "d": 2 } */
Test Case 3: _merge_
const state = { a: { b: { c: 1 } }, d: 2 }; // freeze the object deepFreeze(state); const newState = update( state, {a: {b: { _merge_ : {e: 5 }}}} ); // does not updates // as output is frozen newState.a.b.e = 10; console.log(newState); /* { "a": { "b": { "c": 1, "e": 5 } }, "d": 2 } */
Test Case 4: _transform_
const state = {a: { b: 2}}; // freeze the object deepFreeze(state); const newState = update(state, {a: { b: {_transform_: (item) => item * 2}}}); // does not updates // as output is frozen newState.a.b = 10; console.log(newState); /* { "a": { "b": 4 } } */