Skip to content

Instantly share code, notes, and snippets.

@elliotlarson
Last active February 10, 2024 07:08
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 1 You must be signed in to fork a gist
  • Save elliotlarson/c6c3ba1971e6eb8a7d048f0a0fe6caac to your computer and use it in GitHub Desktop.
Save elliotlarson/c6c3ba1971e6eb8a7d048f0a0fe6caac to your computer and use it in GitHub Desktop.
JavaScript Immutable Operations on Arrays and Objects
// This is a Jest spec that explores some of the different ways to alter
// arrays and objects without mutating state. I'm trying different approaches
// available using:
//
// * Vanilla JS
// * Immutable.js
// * Lodash
// * Rambda
//
// The motivation for this is largely to work with a Redux store.
import R from "rambda";
import _ from "lodash";
import { Map, List } from "immutable";
import * as I from "../helpers/imu";
describe("immutable Array operations", () => {
let characters;
beforeEach(() => {
characters = ["Walter", "Jeffrey", "Donald"];
});
describe("add item to an array", () => {
let expectedResult, newCharacter;
beforeEach(() => {
newCharacter = "Maude";
expectedResult = ["Walter", "Jeffrey", "Donald", "Maude"];
});
describe("with vanilla JS spread operator", () => {
it("returns the expected result", () => {
const result = [...characters, newCharacter];
expect(result).toEqual(expectedResult);
});
});
describe("with vanilla JS concat", () => {
it("returns the expected result", () => {
const result = characters.concat(newCharacter);
expect(result).toEqual(expectedResult);
});
});
describe("with lodash concat", () => {
it("returns the expected result", () => {
const result = _.concat(characters, newCharacter);
expect(result).toEqual(expectedResult);
});
});
describe("with Ramda append", () => {
it("returns the expected result", () => {
const result = R.append(newCharacter, characters);
expect(result).toEqual(expectedResult);
});
});
describe("with Immutable.js push", () => {
it("returns the expected result", () => {
const immutableCharacters = List(characters);
const result = immutableCharacters.push(newCharacter);
expect(result.toArray()).toEqual(expectedResult);
});
});
describe("with Immutable.js set", () => {
it("returns the expected result", () => {
const immutableCharacters = List(characters);
const result = immutableCharacters.set(
immutableCharacters.size,
newCharacter,
);
expect(result.toArray()).toEqual(expectedResult);
});
});
describe("with imu add", () => {
it("returns the expected result", () => {
const result = I.add(characters, newCharacter);
expect(result).toEqual(expectedResult);
});
});
});
describe("update an item in an array", () => {
let expectedResult, characterToUpdate, updatedName;
beforeEach(() => {
characterToUpdate = "Jeffrey";
updatedName = "The Dude";
expectedResult = ["Walter", "The Dude", "Donald"];
});
describe("with vanilla JS map", () => {
it("returns the expected result", () => {
const result = characters.map(c => {
if (c === characterToUpdate) {
return updatedName;
}
return c;
});
expect(result).toEqual(expectedResult);
});
});
describe("with vanilla JS findIndex, spread operator, and slice", () => {
it("returns the expected result", () => {
const updateIndex = characters.findIndex(c => c === characterToUpdate);
const result = [
...characters.slice(0, updateIndex),
updatedName,
...characters.slice(updateIndex + 1),
];
expect(result).toEqual(expectedResult);
});
});
describe("with Ramda findIndex and update", () => {
it("returns the expected result", () => {
const updateIndex = R.findIndex(R.equals(characterToUpdate))(
characters,
);
const result = R.update(updateIndex, updatedName, characters);
expect(result).toEqual(expectedResult);
});
});
describe("with Immutable.js findIndex and splice", () => {
it("returns the expected result", () => {
const immutableCharacters = List(characters);
const updateIndex = immutableCharacters.findIndex(
c => c === characterToUpdate,
);
const result = immutableCharacters.splice(updateIndex, 1, updatedName);
expect(result.toArray()).toEqual(expectedResult);
});
});
describe("with Immutable.js findIndex and set", () => {
it("returns the expected result", () => {
const immutableCharacters = List(characters);
const updateIndex = immutableCharacters.findIndex(
c => c === characterToUpdate,
);
const result = immutableCharacters.set(updateIndex, updatedName);
expect(result.toArray()).toEqual(expectedResult);
});
});
describe("with imu set", () => {
it("returns the expected result", () => {
const result = I.set(characters, characterToUpdate, updatedName);
expect(result).toEqual(expectedResult);
});
});
});
describe("remove an item from an array", () => {
let expectedResult, characterToRemove;
beforeEach(() => {
characterToRemove = "Donald";
expectedResult = ["Walter", "Jeffrey"];
});
describe("with vanillla JS filter", () => {
it("returns the expected result", () => {
const result = characters.filter(c => c !== characterToRemove);
expect(result).toEqual(expectedResult);
});
});
describe("with vanillla JS findIndex and slice", () => {
it("returns the expected result", () => {
const removeIndex = characters.findIndex(c => c === characterToRemove);
const result = [
...characters.slice(0, removeIndex),
...characters.slice(removeIndex + 1),
];
expect(result).toEqual(expectedResult);
});
});
describe("with Rambda reject", () => {
it("returns the expected result", () => {
const result = R.reject(R.equals(characterToRemove), characters);
expect(result).toEqual(expectedResult);
});
});
describe("with Immutable.js remove", () => {
it("returns the expected result", () => {
const immutableCharacters = List(characters);
const removeIndex = immutableCharacters.findIndex(
c => c === characterToRemove,
);
const result = immutableCharacters.remove(removeIndex);
expect(result.toArray()).toEqual(expectedResult);
});
});
describe("with imu del", () => {
it("returns the expected result", () => {
const result = I.del(characters, characterToRemove);
expect(result).toEqual(expectedResult);
});
});
});
});
describe("immutable Object operations", () => {
let characters;
beforeEach(() => {
characters = {
1: { firstName: "Jeffrey", lastName: "Lebowski" },
2: { firstName: "Walter", lastName: "Sobchak" },
3: { firstName: "Donald", lastName: "Kerabatsos" },
};
});
describe("add a key to an object", () => {
let expectedResult;
beforeEach(() => {
expectedResult = {
1: { firstName: "Jeffrey", lastName: "Lebowski" },
2: { firstName: "Walter", lastName: "Sobchak" },
3: { firstName: "Donald", lastName: "Kerabatsos" },
4: { firstName: "Maude", lastName: "Lebowski" },
};
});
describe("with vanilla JS spread operator", () => {
it("returns the expected results", () => {
const newId = 4;
const newCharacter = { firstName: "Maude", lastName: "Lebowski" };
const result = { ...characters, [newId]: newCharacter };
expect(result).toEqual(expectedResult);
});
});
describe("with Immutable.js set", () => {
it("returns the expected results", () => {
const newId = 4;
const newCharacter = { firstName: "Maude", lastName: "Lebowski" };
const immutableCharacters = Map(characters);
const result = immutableCharacters.set(newId, newCharacter);
expect(result.toObject()).toEqual(expectedResult);
});
});
describe("with imu add", () => {
it("returns the expected results", () => {
const newId = 4;
const newCharacter = { firstName: "Maude", lastName: "Lebowski" };
const result = I.add(characters, newId, newCharacter);
expect(result).toEqual(expectedResult);
});
});
});
describe("update an object key's value", () => {
let expectedResult;
beforeEach(() => {
expectedResult = {
1: { firstName: "The Dude", lastName: "Lebowski" },
2: { firstName: "Walter", lastName: "Sobchak" },
3: { firstName: "Donald", lastName: "Kerabatsos" },
};
});
describe("with vanilla JS spread operator", () => {
it("returns the expected results", () => {
const updateId = 1;
const updatedcharacter = {
firstName: "The Dude",
lastName: "Lebowski",
};
const result = { ...characters, [updateId]: updatedcharacter };
expect(result).toEqual(expectedResult);
});
});
describe("with Immutable.js set", () => {
it("returns the expected results", () => {
const immutableCharacters = Map(characters);
const updateId = 1;
const updatedcharacter = {
firstName: "The Dude",
lastName: "Lebowski",
};
const result = immutableCharacters.set(`${updateId}`, updatedcharacter);
expect(result.toObject()).toEqual(expectedResult);
});
});
describe("with imu set", () => {
it("returns the expected results", () => {
const updateId = 1;
const updatedCharacter = {
firstName: "The Dude",
lastName: "Lebowski",
};
const result = I.set(characters, updateId, updatedCharacter);
expect(result).toEqual(expectedResult);
});
});
});
describe("delete an object's key", () => {
let expectedResult, deleteId;
beforeEach(() => {
deleteId = 3;
expectedResult = {
1: { firstName: "Jeffrey", lastName: "Lebowski" },
2: { firstName: "Walter", lastName: "Sobchak" },
};
});
describe("with vanilla JS spread operator", () => {
it("returns the expected results", () => {
// if the key is a string we can skip type casting to string
// const { [deleteId]: deleted, ...result } = characters;
const { [`${deleteId}`]: deleted, ...result } = characters;
expect(result).toEqual(expectedResult);
});
});
describe("with vanilla JS reduce", () => {
it("returns the expected results", () => {
const result = Object.keys(characters).reduce((newObj, id) => {
if (id !== `${deleteId}`) {
return { ...newObj, [id]: characters[id] };
}
return newObj;
}, {});
expect(result).toEqual(expectedResult);
});
});
describe("with lodash omit", () => {
it("returns the expected results", () => {
// note that we can pass in an integer key here and it handles it without needing to type cast
const result = _.omit(characters, deleteId);
expect(result).toEqual(expectedResult);
});
});
describe("with Ramda omit", () => {
it("returns the expected results", () => {
// can't pass in an integer key here so we need to type cast it to a string
const result = R.omit(`${deleteId}`, characters);
expect(result).toEqual(expectedResult);
});
});
describe("with Immutable.js", () => {
it("returns the expected results", () => {
const immutableCharacters = Map(characters);
// can't pass in an integer key here so we need to type cast it to a string
const result = immutableCharacters.delete(`${deleteId}`);
expect(result.toObject()).toEqual(expectedResult);
});
});
describe("with imu del", () => {
it("returns the expected results", () => {
const result = I.del(characters, deleteId);
expect(result).toEqual(expectedResult);
});
});
});
});
// A tiny libary for making immutable changes to arrays and objects
export const add = (target, ...args) => {
if (Array.isArray(target)) {
return aAdd(target, ...args);
} else {
return oAdd(target, ...args);
}
};
const aAdd = (array, newValue) => {
return [...array, newValue];
};
const oAdd = (obj, newKey, newValue) => {
return { ...obj, [newKey]: newValue };
};
export const set = (target, ...args) => {
if (Array.isArray(target)) {
return aSet(target, ...args);
} else {
return oSet(target, ...args);
}
};
const aSet = (array, oldValue, newValue) => {
return array.map(ai => (ai === oldValue ? newValue : ai));
};
const oSet = (object, updateKey, updateValue) => {
return {
...object,
[`${updateKey}`]: updateValue,
};
};
export const del = (target, ...args) => {
if (Array.isArray(target)) {
return aDel(target, ...args);
} else {
return oDel(target, ...args);
}
};
const aDel = (array, deleteValue) => {
return array.filter(ai => ai !== deleteValue);
};
const oDel = (obj, deleteKey) => {
const { [`${deleteKey}`]: deleted, ...result } = obj;
return result;
};
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment