Last active
February 10, 2024 07:08
-
-
Save elliotlarson/c6c3ba1971e6eb8a7d048f0a0fe6caac to your computer and use it in GitHub Desktop.
JavaScript Immutable Operations on Arrays and Objects
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
// 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); | |
}); | |
}); | |
}); | |
}); |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
// 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