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
}
}
*/