TL;DR
Implementation
const removeAtIndex = (arr, index) => {
const copy = [...arr];
copy.splice(index, 1);
return copy;
};
const toggle = (arr, item, getValue = item => item) => {
const index = arr.findIndex(i => getValue(i) === getValue(item));
if (index === -1) return [...arr, item];
return removeAtIndex(arr, index);
};
Usage
let arr = [1, 2, 3];
arr = toggle(arr, 2); // [1, 3];
arr = toggle(arr, 4); // [1, 3, 4];
Read below for explanations or simply for pointless (or not) brain-picking.
Let's get toggling
Let's go through the basic idea of the function by sketching it out.
So the idea here is having a function called toggle
that we can call to redefine our array variable.
The caller is whatever piece of code holds your array to begin with. In this piece of code, you want a certain item matching a condition toggled in your array. Basically, if the item is found in the array, it is removed; if it's not found, it's added instead.
We would call it like this:
let arr = [1, 2, 3];
arr = toggle(arr, 2); // [1, 3];
arr = toggle(arr, 4); // [1, 3, 4];
Now that the concept is understood, let's go through a primary version of the toggle
function in javascript:
const toggle = (arr, item) => {
if (arr.includes(item)) return remove(arr, item);
else return add(arr, item);
}
Pretty simple. What about the add
and remove
functions though?
Adding an item
Adding an item to an array is a piece of cake. Since we use functional programming here (mutation), and don't want the original array to be altered, let's just return the deconstructed array with the item added to the end of it.
return [...arr, item];
Removing an item
Removing an item is a little more complex, but let's keep it simple for now using filter
.
return arr.filter(i => i !== item);
Stir it up a little and we now have:
const toggle = (arr, item) => {
if (arr.includes(item)) return arr.filter(i => i !== item);
else return [...arr, item];
}
That's not really just it, though.
When dealing with objects
One problem that might come up using this implementation is when using an array of objects. Sometimes you might only want to remove the object with a certain id
for example, regardless of the value of its other fields. arr.includes
would be no help in that case.
To address this, let's give our functions an optional getValue
callback function. This callback will return the actual value we want to compare the items with (like a unique id
). Since it's optional, we'll give a default value of the item, untouched.
const toggle = (arr, item, getValue = item => item) => {
if (arr.some(i => getValue(i) === getValue(item)))
return arr.filter(i => getValue(i) !== getValue(item));
else return [...arr, item];
};
This gives us the flexibility of giving it a whole function to help compare our array items.
We could now only compare the item id
by giving it a callback function of item => item.id
.
const object1 = { id: 2, name: "Hello" };
const object2 = { id: 3, name: "Hi" };
let arr = [object1, object2];
arr = toggle(arr, object1, item => item.id);
console.log(arr); // [{ id: 3, name: "Hi" }]
By giving it a more complex callback, I can think of a couple more creative use of a function like this. That will be for another day.
For simpler arrays, we could still call it without providing the callback:
let arr = [1, 2, 3];
arr = toggle(arr, 2);
console.log(arr); // [1, 3];
Improve performance
The above works, although you might have noticed that we use the comparison with the getValue
calls twice. That means we loop through all the array twice (or almost all thanks to the some
function). This could get ugly on huge arrays.
Let's reorder this to only loop through the array once.
arr.filter
gives us back an array that is filtered if an item matching a certain condition was found. It means that if the array comes back untouched after the filter
call, it couldn't find the item we were looking for.
We can use this to our advantage to replace completely the use of arr.some
we had before, leaving us with a single loop through our array items.
const toggle = (arr, item, getValue = item => item) => {
const filtered = arr.filter(i => getValue(i) === getValue(item));
if (arr.length === filtered.length) {
// array was not filtered; item was not present; then add
return [...arr, item];
} else {
// array was filtered; item was present; then remove
return filtered;
}
}
Let's clean it up a little as I don't like clutter, and this is small enough to be readable using some of the javascript quirks.
const toggle = (arr, item, getValue = item => item) => {
const filtered = arr.filter(i => getValue(i) === getValue(item));
return arr.length === filtered.length ? [...arr, item] : filtered;
}
A side effect to note
One side effect of using the filter
function to remove an item is that it doesn't stop at the first found item that matches the condition given. If the condition given is too permissive, more than one item could be removed.
This could be seen as a benefit. For example, you could have various items with an id
of 2
in an array, and want to toggle that, so either remove them all or add one.
Most of the time though, you don't want that because it could lead to some unwanted item removals.
To address this, let's use the splice function instead to remove the item. Since splice
works with indexes, we need to find that first. We can do that using findIndex
in a similar way we used filter
.
The findIndex
function will stop at the first element matching the condition given, so it has the side benefit of not looping through the whole array unless the item is at the last index, or simply not found.
Using findIndex
means we have to once again reorder stuff a little.
For our first condition, we'll use the value returned by (-1
if not found, index
if found).
const index = arr.findIndex(i => getValue(i) === getValue(item));
if (index === -1) // remove
else // add
Then, to remove an item at this index (if not -1
), we use splice
.
const removeAtIndex = (arr, index) => {
const copy = [...arr];
copy.splice(index, 1);
return copy;
}
I created a whole function to keep the toggle
function as clean as possible, and have great separation of concerns between our utility function set.
Here is what our final toggle
looks like:
const toggle = (arr, item, getValue = item => item) => {
const index = arr.findIndex(i => getValue(i) === getValue(item));
if (index === -1) return [...arr, item];
return removeAtIndex(arr, index);
}
The reason we create a copy on the array in the removeAtIndex
function is to avoid mutation. In other words, it is to avoid altering the original array given to the toggle
function.
Got anything that can improve these functions? Let's discuss.
Top comments (0)