How to update element inside List with ImmutableJS?
Asked Answered
T

7

135

Here is what official docs said

updateIn(keyPath: Array<any>, updater: (value: any) => any): List<T>
updateIn(keyPath: Array<any>, notSetValue: any, updater: (value: any) => any): List<T>
updateIn(keyPath: Iterable<any, any>, updater: (value: any) => any): List<T>
updateIn(keyPath: Iterable<any, any>, notSetValue: any, updater: (value: any) => any): List<T>

There is no way normal web developer (not functional programmer) would understand that!

I have pretty simple (for non-functional approach) case.

var arr = [];
arr.push({id: 1, name: "first", count: 2});
arr.push({id: 2, name: "second", count: 1});
arr.push({id: 3, name: "third", count: 2});
arr.push({id: 4, name: "fourth", count: 1});
var list = Immutable.List.of(arr);

How can I update list where element with name third have its count set to 4?

Temple answered 12/4, 2015 at 13:1 Comment(10)
Looks like typescript to me…Ible
I don't know how it's look like, but documentation is terrible facebook.github.io/immutable-js/docs/#/List/updateTemple
Serious upvote for the dig at the documentation. If the goal of that syntax is to make me feel dumb/inadequate, definite success. If the goal, however, is to convey how Immutable works, well...Yasmineyasu
I have to agree, I find the documentation for immutable.js to be seriously frustrating. The accepted solution for this uses a method findIndex() that I don't even see at all in the docs.Atelectasis
Actually findIndex() is a native Array method [1, 2].findIndex(i => { i === 1 }) //returns 0Pumpernickel
I'd rather they give an example for each function instead of those things.Loomis
Agreed. The documentation must have been written by some scientist in a bubble, not by somebody who wants to show some simple examples. The documentation needs some documentation. For example, I like the underscore documentation, much easier to see practical examples.Spear
The documentation is written in Typescript.Accessory
@Yasmineyasu the docs are not written to make anyone feel dumb or inadequate. They are written to precisely communicate to programmers the argument types and return types of the methods. It's not really possible to communicate precisely except by using precise symbols, and all precise symbols must be learnt to be understood (for example, algebra). This is why (at least now, if not when this question was asked) the types are supplemented with example code and English explanation. But Immutable.JS is designed for advanced use cases, not basic web development.Polygon
The docs are written for experienced IM users, who will rarely exist because few can get past the initial learning curve. The problem with the documentation is omission of important facts. For instance, you change a subclassed Record, and the result is a new Record of the same subclass. But you change a subclassed Map, and changes return a generic Map without your subclass methods. This fact makes a big difference for use with Redux, but I see it nowhere in the documentation. Tutorials give this kind of information, and their absence illustrates how the documentation is only half there.Demo
T
126

The most appropriate case is to use both findIndex and update methods.

list = list.update(
  list.findIndex(function(item) { 
    return item.get("name") === "third"; 
  }), function(item) {
    return item.set("count", 4);
  }
); 

P.S. It's not always possible to use Maps. E.g. if names are not unique and I want to update all items with the same names.

Temple answered 15/4, 2015 at 16:7 Comment(7)
If you need duplicate names, use multimaps - or maps with tuples as values.Ible
Won't this inadvertently update the last element of the list if there is no element with "three" as the name? In that case findIndex() would return -1, which update() would interpret as the index of the last element.Jonette
No, -1 is not the last elementTemple
@Sam Storie is right. From the docs: "index may be a negative number, which indexes back from the end of the List. v.update(-1) updates the last item in the List." facebook.github.io/immutable-js/docs/#/List/updateAntaeus
@SamStorie In that case, just check if index >= 0 before you call list.updateCelinecelinka
I couldn't call item.set because for me item was not Immutable type, I had to map the item first, call set and then convert it back to object again. So for this example, function(item) { return Map(item).set("count", 4).toObject(); }Kandacekandahar
Not a one liner, but for completeness: const index = list.findIndex(item => item.get('name') === 'third'); list = (index < 0) ? list : list.update(index, item => item.set("count", 4));Remand
M
40

With .setIn() you can do the same:

let obj = fromJS({
  elem: [
    {id: 1, name: "first", count: 2},
    {id: 2, name: "second", count: 1},
    {id: 3, name: "third", count: 2},
    {id: 4, name: "fourth", count: 1}
  ]
});

obj = obj.setIn(['elem', 3, 'count'], 4);

If we don’t know the index of the entry we want to update. It’s pretty easy to find it using .findIndex():

const indexOfListToUpdate = obj.get('elem').findIndex(listItem => {
  return listItem.get('name') === 'third';
});
obj = obj.setIn(['elem', indexOfListingToUpdate, 'count'], 4);

Hope it helps!

Mcgriff answered 30/9, 2016 at 9:46 Comment(2)
you're missing { } inside fromJS( ), without the braces it isn't valid JS.Polygon
I wouldn't call the returned object 'arr' as it is an object. only arr.elem is an arrayTridentine
F
24
var index = list.findIndex(item => item.name === "three")
list = list.setIn([index, "count"], 4)

Explanation

Updating Immutable.js collections always return new versions of those collections leaving the original unchanged. Because of that, we can't use JavaScript's list[2].count = 4 mutation syntax. Instead we need to call methods, much like we might do with Java collection classes.

Let's start with a simpler example: just the counts in a list.

var arr = [];
arr.push(2);
arr.push(1);
arr.push(2);
arr.push(1);
var counts = Immutable.List.of(arr);

Now if we wanted to update the 3rd item, a plain JS array might look like: counts[2] = 4. Since we can't use mutation, and need to call a method, instead we can use: counts.set(2, 4) - that means set the value 4 at the index 2.

Deep updates

The example you gave has nested data though. We can't just use set() on the initial collection.

Immutable.js collections have a family of methods with names ending with "In" which allow you to make deeper changes in a nested set. Most common updating methods have a related "In" method. For example for set there is setIn. Instead of accepting an index or a key as the first argument, these "In" methods accept a "key path". The key path is an array of indexes or keys that illustrates how to get to the value you wish to update.

In your example, you wanted to update the item in the list at index 2, and then the value at the key "count" within that item. So the key path would be [2, "count"]. The second parameter to the setIn method works just like set, it's the new value we want to put there, so:

list = list.setIn([2, "count"], 4)

Finding the right key path

Going one step further, you actually said you wanted to update the item where the name is "three" which is different than just the 3rd item. For example, maybe your list is not sorted, or perhaps there the item named "two" was removed earlier? That means first we need to make sure we actually know the correct key path! For this we can use the findIndex() method (which, by the way, works almost exactly like Array#findIndex).

Once we've found the index in the list which has the item we want to update, we can provide the key path to the value we wish to update:

var index = list.findIndex(item => item.name === "three")
list = list.setIn([index, "count"], 4)

NB: Set vs Update

The original question mentions the update methods rather than the set methods. I'll explain the second argument in that function (called updater), since it's different from set(). While the second argument to set() is the new value we want, the second argument to update() is a function which accepts the previous value and returns the new value we want. Then, updateIn() is the "In" variation of update() which accepts a key path.

Say for example we wanted a variation of your example that didn't just set the count to 4, but instead incremented the existing count, we could provide a function which adds one to the existing value:

var index = list.findIndex(item => item.name === "three")
list = list.updateIn([index, "count"], value => value + 1)
Furcula answered 2/12, 2016 at 20:16 Comment(1)
This answer was amazing and the best documentation i've seen yet for immutableJSVidda
I
19

Here is what official docs said… updateIn

You don't need updateIn, which is for nested structures only. You are looking for the update method, which has a much simpler signature and documentation:

Returns a new List with an updated value at index with the return value of calling updater with the existing value, or notSetValue if index was not set.

update(index: number, updater: (value: T) => T): List<T>
update(index: number, notSetValue: T, updater: (value: T) => T): List<T>

which, as the Map::update docs suggest, is "equivalent to: list.set(index, updater(list.get(index, notSetValue)))".

where element with name "third"

That's not how lists work. You have to know the index of the element that you want to update, or you have to search for it.

How can I update list where element with name third have its count set to 4?

This should do it:

list = list.update(2, function(v) {
    return {id: v.id, name: v.name, count: 4};
});
Ible answered 13/4, 2015 at 14:32 Comment(8)
"That's not how lists work". Maybe. But that's how business logic works, and my work as programmer is to translate business logic into data structure logic. And your answer does not satisfy this logic. So, you are saying that I should know index first. Does that mean that I can't do it in one method call? Should I use findIndex and only then update?Temple
No, your choice of data structure does not satisfy the business logic :-) If you want to access elements by their name, you should use a map instead of a list.Ible
@bergi, really? So if I have, say, a list of students in a class, and I want to do CRUD operations on this student data, using a Map over a List is the approach you would suggest? Or do you only say that because OP said "select item by name" and not "select item by id"?Babble
@DavidGilbertson: CRUD? I'd suggest a database :-) But yes, an id->student map sounds more appropriate than a list, unless you've got an order on them and want to select them by index.Ible
@bergi I guess we'll agree to disagree, I get plenty of data back from APIs in the form of arrays of things with IDs, where I need to update those things by ID. This is a JS Array, and in my mind, should therefore be an immutableJS list.Babble
@DavidGilbertson: I think those APIs take JSON arrays for sets - which are explicitly unordered.Ible
@Ible what if you don't know what the index is, what alternatives are there? Basically, a conditional update.Haun
@TheNastyOne: Take a look at VitaliiKorsakov's answer - you can always search for the index. Or you just .map over all items and conditionally return new values. Or follow the advice from my answer and choose a different data structure :-)Ible
N
12

Use .map()

list = list.map(item => 
   item.get("name") === "third" ? item.set("count", 4) : item
);

var arr = [];
arr.push({id: 1, name: "first", count: 2});
arr.push({id: 2, name: "second", count: 1});
arr.push({id: 3, name: "third", count: 2});
arr.push({id: 4, name: "fourth", count: 1});
var list = Immutable.fromJS(arr);

var newList = list.map(function(item) {
    if(item.get("name") === "third") {
      return item.set("count", 4);
    } else {
      return item;
    }
});

console.log('newList', newList.toJS());

// More succinctly, using ES2015:
var newList2 = list.map(item => 
    item.get("name") === "third" ? item.set("count", 4) : item
);

console.log('newList2', newList2.toJS());
<script src="https://cdnjs.cloudflare.com/ajax/libs/immutable/3.8.1/immutable.js"></script>
Nates answered 20/10, 2016 at 15:9 Comment(1)
Thanks for that - so many terrible convoluted answers here, the real answer is "if you want to change a list, use map". That's the whole point of persistent immutable data structures, you don't "update" them, you make a new structure with the updated values in it; and it's cheap to do so because the unmodified list elements are shared.Irrational
B
2

I really like this approach from the thomastuts website:

const book = fromJS({
  title: 'Harry Potter & The Goblet of Fire',
  isbn: '0439139600',
  series: 'Harry Potter',
  author: {
    firstName: 'J.K.',
    lastName: 'Rowling'
  },
  genres: [
    'Crime',
    'Fiction',
    'Adventure',
  ],
  storeListings: [
    {storeId: 'amazon', price: 7.95},
    {storeId: 'barnesnoble', price: 7.95},
    {storeId: 'biblio', price: 4.99},
    {storeId: 'bookdepository', price: 11.88},
  ]
});

const indexOfListingToUpdate = book.get('storeListings').findIndex(listing => {
  return listing.get('storeId') === 'amazon';
});

const updatedBookState = book.setIn(['storeListings', indexOfListingToUpdate, 'price'], 6.80);

return state.set('book', updatedBookState);
Borscht answered 30/4, 2019 at 11:22 Comment(0)
S
-2

You can use map:

list = list.map((item) => { 
    return item.get("name") === "third" ? item.set("count", 4) : item; 
});

But this will iterate over the entire collection.

Shun answered 18/10, 2017 at 18:30 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.