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?

Vitalii Korsakov
  • 45,737
  • 20
  • 72
  • 90
  • Looks like typescript to me… – Bergi Apr 12 '15 at 21:01
  • 47
    I don't know how it's look like, but documentation is terrible http://facebook.github.io/immutable-js/docs/#/List/update – Vitalii Korsakov Apr 13 '15 at 06:03
  • 75
    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... – Owen Jan 04 '16 at 22:53
  • 9
    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. – RichardForrester Feb 25 '16 at 09:15
  • Actually findIndex() is a native Array method [1, 2].findIndex(i => { i === 1 }) //returns 0 – wazzaday Apr 05 '16 at 14:06
  • 4
    I'd rather they give an example for each function instead of those things. – RedGiant Aug 01 '16 at 15:54
  • 5
    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. – ReduxDJ Dec 02 '16 at 00:52
  • 1
    The documentation is written in Typescript. – zero_cool Feb 08 '17 at 01:13
  • 1
    @Owen 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. – Andy Nov 14 '17 at 03:24
  • 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. – OsamaBinLogin Jun 14 '20 at 22:52

7 Answers7

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.

Vitalii Korsakov
  • 45,737
  • 20
  • 72
  • 90
  • 1
    If you need duplicate names, use multimaps - or maps with tuples as values. – Bergi Apr 20 '15 at 22:08
  • 5
    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. – Sam Storie Nov 25 '15 at 13:42
  • 1
    No, -1 is not the last element – Vitalii Korsakov Nov 25 '15 at 16:30
  • 9
    @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." https://facebook.github.io/immutable-js/docs/#/List/update – Gabriel Ferraz Feb 25 '16 at 20:05
  • @SamStorie In that case, just check if index >= 0 before you call list.update – Simon Polak Apr 13 '16 at 19:05
  • 1
    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(); } – syclee Aug 08 '16 at 01:36
  • 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));` – Donald Mar 13 '20 at 16:34
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!

Albert Olivé Corbella
  • 4,061
  • 7
  • 48
  • 66
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)
Lee Byron
  • 1,864
  • 13
  • 8
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};
});
Bergi
  • 630,263
  • 148
  • 957
  • 1,375
  • "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`? – Vitalii Korsakov Apr 15 '15 at 13:19
  • 14
    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. – Bergi Apr 15 '15 at 13:33
  • 1
    @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"? – David Gilbertson Nov 16 '15 at 20:32
  • 2
    @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. – Bergi Nov 16 '15 at 20:36
  • 1
    @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. – David Gilbertson Nov 16 '15 at 20:53
  • 1
    @DavidGilbertson: I think those APIs take JSON arrays for sets - which are explicitly unordered. – Bergi Nov 16 '15 at 22:05
  • @Bergi what if you don't know what the index is, what alternatives are there? Basically, a conditional update. – TheNastyOne Mar 22 '16 at 18:54
  • @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 :-) – Bergi Mar 22 '16 at 19:57
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>
Meistro
  • 3,664
  • 2
  • 28
  • 33
  • 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. – Korny Oct 22 '17 at 19:20
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);
Dryden Williams
  • 1,175
  • 14
  • 22
-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.

Aldo Porcallo
  • 142
  • 1
  • 4