How to remove an object from an array in Immutable? - javascript

Given a state like this:
state = {
things: [
{ id: 'a1', name: 'thing 1' },
{ id: 'a2', name: 'thing 2' },
],
};
How can I create a new state where ID "a1" is removed? It's easy enough to push new items:
return state.set(state.get('things').push(newThing));
But I can't figure out how to search for and remove an object by its id property. I tried this:
return state.set('tracks',
state.get('tracks').delete(
state.get('tracks').findIndex(x => x.get('id') === 'a2')
)
)
But it seems messy, plus it only works if the item is found, because if findIndex returns -1, that's a valid value for delete.

You can use Array#filter.
return state.set('things', state.get('things').filter(o => o.get('id') !== 'a1'));

When you are using filter it iterates all cycle -> one effective way is finding index => slice and using splitter ...
const index = state.findIndex(data => data.id === action.id);
return [...state.slice(0, index), ...state.slice(index + 1)];

Alternatively, as you are "searching and then deleting"...
var itemIndex = this.state.get("tracks").findIndex(x => x.get('id') === 'a2');
return itemIndex > -1 ? this.state.deleteIn(["tracks", itemIndex]) : this.state;
This will ensure the state is not mutated when there are no changes.

Found this thread while looking for a solution to a similar task.
Solved it with update method:
return state.update('things', (things) => things.filter((t) => t.id !== action.things.id))
any idea/comment which one is better/preferred?

You can do that even without immutable.js with following function.
function arrayFilter(array, filter) {
let ret = array
let removed = 0
for (let index = 0; index < array.length; index++) {
const passed = filter(array[index], index, array)
if (!passed) {
ret = [...ret.slice(0, index - removed), ...ret.slice(index - removed + 1)]
removed++
}
}
return ret
}

ImmutableJS working with nested arrays
Immutablejs is great but at the same time makes things more complicated in some edge cases, particularly when working with nested arrays.
Sometimes it is easier to take it back to JS in a general sense for this particular issue.
// 1. get a copy of the list into normal JavaScript
const myList = state.getIn(['root', 'someMap', 'myList']).toJS()
// 2. remove item in list using normal JavaScript and/or anything else
myList.splice(deleteIndex, 1)
// 3. return the new state based on mutated myList
return state
.mergeDeep({ root: { someMap: { myList: undefined } }})
.mergeDeep({ root: { someMap: { myList } }})
Unfortunately, step 3 is necessary to specifically set to undefined because if you simply set myList directly as an array value, ImmutableJS will do a comparison of values between the current list and only modify them creating strange behavior.
The justification for this is to simplify the mental overhead. I do not recommend doing this in a loop, rather manipulate the pure JS array in a loop if you must but should be prior to step 3.

Related

Remove an item form an array in react

I am making a Website OS in React.js, and I have a problem removing an item from an array in handleWindowRemove, it removes all other items.
This is the array code:
const [openedWindows, setOpenedWindows] = useState([
]);
const handleWindowAdd = () => {
setOpenedWindows([...openedWindows, { openedWindow: "" }]);
}
const handleWindowRemove = (index) => {
const list = [...openedWindows];
const i = openedWindows.indexOf(index);
list.shift(i, 1);
setOpenedWindows(list);
//alert(index);
}
Github Repository
Shift method is supposed to remove the first element of the array and does not take any arguments. You could use the splice method instead.
However, I find filter method works best here:
const handleWindowRemove = (index) => {
const updatedList = openedWindows.filter((el, idx) => idx !== index);
setOpenedWindows(updatedList);
}
I think you are looking for Array.prototype.splice.
Array.shift removes the first item of an array.
Here is a corrected example:
const [openedWindows, setOpenedWindows] = useState([
]);
const handleWindowAdd = () => {
setOpenedWindows([...openedWindows, { openedWindow: "" }]);
}
const handleWindowRemove = (index) => {
const list = [...openedWindows];
const i = openedWindows.indexOf(index);
list.splice(i, 1);
setOpenedWindows(list);
//alert(index);
}
If you are dealing with a list of array elements, using an index for
identifying an element to perform some operation, especially delete
is not recommended at all.
Since you are passing 'index' as an argument for 'handleWindowRemove', I am assuming you are using 'index' as a key while rendering the list of elements. Whenever you perform an delete operation on element using "splice" or any other method using 'index', it will result in change of index of other elements present in Array too. If you are using index as key while rendering list of array elements, everytime you delete an item in Array using index, index of remaning elements will change and cause re-rendering of almost all the elements present in array. This is not the recommended approach since it will re-render the elements that are not modified.
Better approach would be having 'id' properties for all the objects of Array. In this approach, 'id' can be used as a key while rendering and also as an identifier while deleting the object from the array. Due to this, other window objects won't be affected. Hence re-rendering of unmodified objects will not happen.
const openedWindows = [{'id': # identifier, 'windowObject': ...}]

React - Deleting rest of the array instead of just one item

Here's the link:
https://codesandbox.io/s/heuristic-heisenberg-9cxb9
I have this method: deleteItem
This code:
return {
monsters: prevState.monsters
.slice(0, deleteItemPosition)
.concat(
prevState.monsters.slice(
deleteItemPosition + 1,
prevState.monsters.length
)
)
};
This is the code I use to remove an item from array on position deleteItemPosition, because I can't use monsters.splice(deleteItemPosition, 1) because of immutability.
So why does my monsters array get cut off from deleteItemPosition to the end?
Try it yourself, entering some number 0-5 into "index to delete"
If I update line
let deleteItemPosition = this.state.deleteItemPosition;
and I hardcode it to, let say
let deleteItemPosition = 3;
Then I notice the item on position 3 gets removed, as I wanted.
Your deleteItem function could be simplified like this, also makes sure that no state updates are skipped:
deleteItem = () => {
this.setState(prevState => {
return { monsters: prevState.monsters.filter((_, i) => i !== +prevState.deleteItemPosition)};
})
};
The functional update is recommended as your new state(new monsters array) depends on the previous state.
Update:
You can use destructuring to avoid using prevState all the time. And you need to convert the deleteItemPosition to a number because the input's value is a string.
deleteItem = () => {
this.setState(({monsters, deleteItemPosition}) => {
return { monsters: monsters.filter((_, i) => i !== +deleteItemPosition)};
})
};
Just make a shallow copy of the monsters array, apply Array.prototype.splice method for deleting your item and return the copied monsters array.
const copyMonsters = [...prevState.monsters];
copyMonsters.splice(deleteItemPosition, 1);
return {
monsters: copyMonsters
}
Put this code inside the setState function in your case.

Using map reduce etc, how would you find the first item matching a certain criteria in a nested array, and stop once found?

How would you find the first item matching a certain criteria in a nested array, and stop once found?
In a 1D array, this is what the Array.find function is for, but how would you do it for a 2D array, and, even neater, for n-dimension array?
Also, I'm trying to come up with a neat solution using es6 and array functions such as find, map, reduce etc, rather than using more traditional loops and variables to maintain state (see one such old-school solution below).
The data may look something like this
const data = [
{arr: [{val:6,name:'aaa'},{val:4,name:'bbb'},{val:8,name:'ccc'}]},
{arr: [{val:3,name:'mmm'},{val:5,name:'nnn'},{val:9,name:'ppp'},{val:5,name:'ooo'}]}
]
I'm hoping I can do something similar to array.find (and its predicate / testing function), but I need to go deeper and find eg the first item with val=5. For the data above, I'd expect to get the item with name 'nnn' (not 'ooo'), and have the process end once the first item is found. Similar to Array.find, I want to avoid processing the rest of the data once a matching item is found.
One boring old way to do it would be something like this, with a loop, but that's... boring, and not as neat as the lovely array functions :)
let found
// loop through all data entries in the outer array
for (const d of data) {
// attempt to find a matching item in the inner array.
// using array.find means we stop at the first match, yay!
const theItem = d.arr.find(item => {
return myPredicate(item)
})
// we also need to break out of the loop. ugh!
if (theItem) {
found = theItem
break
}
}
// return what we found (may be undefined)
return found
Now, I realise that I can do something with find() and some(), say, similar to the answer here ES6 - Finding data in nested arrays, but the problem is that using find on the outer array means that we get back the first item of the outer data array, whereas I want an item from the inner arr array.
const outer = data.find(d => {
return d.arr.some(item => {
return myPredicate(item)
})
})
I would then have to process outer AGAIN to find the item in outer.arr, something like
outer.arr.find(item => myPredicate(item))
This doesn't sit well with me, as the call to some(...) has already gone through and found the matching inner item!
I thought this would be straight forward, and maybe it is, but for one reason or another I got stuck on this little challenge.
I've also looked at the nice traverse library (https://www.npmjs.com/package/traverse), but again that seems to be more about traversing through a whole tree rather than stopping and returning once a particular node is found.
Anyone up for a challenge? ;)
The easiest (though slightly ugly) solution would be to assign the matching item to an outer variable when found:
let foundNested;
data.some(subarr => (
subarr.some((item) => {
if (myPredicate(item)) {
foundNested = item;
return true;
}
});
});
You might use .reduce to avoid assigning to an outer variable:
const myPredicate = ({ val }) => val === 5;
const data = [
{arr: [{val:6,name:'aaa'},{val:4,name:'bbb'},{val:8,name:'ccc'}]},
{arr: [{val:3,name:'mmm'},{val:5,name:'nnn'},{val:9,name:'ppp'},{val:5,name:'ooo'}]}
];
const found = data.reduce((a, { arr }) => (
a ||
arr.find(myPredicate)
), null);
console.log(found);
Problem is, the reduce won't short-circuit - it'll fully iterate over the outer array regardless. For true short-circuiting, I think I'd prefer using a for..of loop:
const data = [
{arr: [{val:6,name:'aaa'},{val:4,name:'bbb'},{val:8,name:'ccc'}]},
{arr: [{val:3,name:'mmm'},{val:5,name:'nnn'},{val:9,name:'ppp'},{val:5,name:'ooo'}]}
];
function findNested(outerArr, myPredicate) {
for (const { arr } of outerArr) {
for (const item of arr) {
if (myPredicate(item)) {
return item;
}
}
}
}
const myPredicate = ({ val }) => val === 5;
console.log(findNested(data, myPredicate));
You'll want to write your own find function that doesn't take a predicate but a result-producing callback:
function find(iterable, callback) {
for (const value of iterable) {
const result = callback(value);
if (result !== undefined)
return result;
}
}
With that, you can write
const data = [
{arr: [{val:6,name:'aaa'},{val:4,name:'bbb'},{val:8,name:'ccc'}]},
{arr: [{val:3,name:'mmm'},{val:5,name:'nnn'},{val:9,name:'ppp'},{val:5,name:'ooo'}]}
];
console.log(find(data, ({arr}) => find(arr, o => o.val == 5 ? o : undefined)));
Alternatively, if you want to get all results, flatMap is the perfect tool:
data.flatMap(({arr}) => arr.filter(({val}) => val == 5));
Sure, why not. I'm up for it. This can probably be improved upon. But this will work. Let's say you are trying to find an object with id of 5 in a multidimensional array.
const arr = [[[{id: 1}], [{id: 2}]], [[{id: 3}]], [[{id: 4}], [{id: 5}], [{id: 6}]]]
function findObject (obj) {
if (Array.isArray(obj)) {
const len = obj.length
for (let i = 0; i < len; i++) {
const found = findObject(obj[i])
if (found) {
return found
}
}
} else if (obj.id === 5) { // Put your search condition here.
return obj
}
}
const obj = findObject(arr)
console.log('obj: ', obj)
This seems to work, but, in my opinion, it's still not clean with that 'found' variable sitting outside the main block and being assigned from inside the nested find block. It's better though. Thoughts?
let found
data.find(d =>
d.arr.find(item => {
found = myPredicate(item) ? item : void 0
return found !== void 0
}) !== void 0
)
return found

Javascript - find latest object depending of value

I've tried to find a way to search the latest value of object, and what I found almost all using reverse() or by using i-- in for loop, but I want to avoid it somehow
I can archive it using two var like this:
var a = [{a:true},{a:true},{a:false}]
var b = a.filter(el=>el.a == true)
console.log(b[b.length-1])
Is there a way to use only one var like this?
var a = [{a:true},{a:true},{a:false}]
a.latestValue(el=>el.a == true)
use find to get only one match.
If you don't like the order, you can reverse it, too.
var a = [{
a:true,
id: 1
},{
a:true,
id: 2,
},{
a:false,
id: 3
}]
const latestValue = a.find(el => el.a === true)
const lastValue = a.reverse().find(el => el.a === true)
console.log(latestValue);
console.log(lastValue);
You're basically looking for something like .find, except a .find that starts at the last item and iterates backwards, rather than starting at the first item and iterating forwards. Although there are built-in functions like lastIndexOf (similar to indexOf, except starts searching from the last element) and reduceRight (same, but for reduce), no such thing exists for .find, so your best option is to write your own function. It's easy enough to write, doesn't mutate the original array (like .reverse() does) and doesn't require creating an intermediate array:
function findRight(arr, callback) {
for (let i = arr.length - 1; i--; i >= 0) {
if (callback(arr[i], i, arr)) return arr[i];
}
}
var a = [{id: 1, a:true},{id: 2, a:true},{id: 3, a:false}];
console.log(
findRight(a, el => el.a === true)
);
I guess it would be possible to (ab)use reduceRight for this, though I wouldn't recommend it:
var a = [{id: 1, a:true},{id: 2, a:true},{id: 3, a:false}];
console.log(
a.reduceRight((a, el) => a || (el.a && el), null)
);
I know already answered but thought it can be achieved in a different way, So here is my solution
You can use JavaScript array map function to get the index of latest value like this
NOTE : I have modified your array to contain more elements
var a = [{a:true},{a:true},{a:false},{a:false},{a:false},{a:true},{a:true},{a:false}];
var latestIndexOfTrue = a.map(function(e) { return e.a; }).lastIndexOf(true)
console.log(latestIndexOfTrue);
/* above will give you the last index of the value you want (here i have tried with
* value true) and it will give you the index as 6 */
if you want whole object then you can get it with bellow code
console.log(a[latestIndexOfTrue]);

Filter an array and create a new array

I have a very simple requirement for filter some objects from a larger array and create a sub-array. Right now, I'm doing it as follows, but I'm wondering whether I could do inline and avoid the for-each.
var branches = $filter('filter')(vm.locations, { 'fkLocationTypeId': 5 });
vm.branchList = [];
angular.forEach(branches, function (obj) {
vm.branchList.push({ id: obj.Id, label: obj.locationDisplayName });
});
Since you want to both filter the array and modify the retained items, you can use filter() in combination with map(), both native array methods
vm.branchList = vm.locations
.filter(function(location) {
return location.fkLocationTypeId === 5;
})
.map(function(location) {
return {
id: location.Id,
label: location.locationDisplayName
};
});
If you want to avoid iterating over the array twice you can use the reduce() method to perform both the filtering and mapping at the same time
vm.branchList = vm.locations
.reduce(function(builtList, location) {
if (location.fkLocationTypeId === 5) {
return builtList.concat({
id: location.Id,
label: location.locationDisplayName
});
}
return builtList;
}, []);
I don't think there's much wrong with your use of forEach, but you could replace it by a map operation on the filtered set.
Personally, I'd use reduce to combine both filter and map operations in one loop, but you'd probably only start seeing a performance increase when you're filtering very large sets of data.
To combine the two in one reducer:
const locToObj = loc =>
({ id: loc.Id, label: loc.locationDisplayName });
const branchReducer = (acc, loc) =>
loc.fkLocationTypeId === 5
? (acc.push(locToObj(loc)), acc)
: acc
const branches = vm.locations.reduce(branchReducer, []);

Categories

Resources