177

I wonder if there is a simpler method in lodash to replace an item in a JavaScript collection? (Possible duplicate but I did not understand the answer there:)

I looked at their documentation but could not find anything

My code is:

var arr = [{id: 1, name: "Person 1"}, {id:2, name:"Person 2"}];
// Can following code be reduced to something like _.XX(arr, {id:1}, {id:1, name: "New Name"});
_.each(arr, function(a, idx){
  if(a.id === 1){
    arr[idx] = {id:1, name: "Person New Name"};
    return false;
  }
});

_.each(arr, function(a){
  document.write(a.name);
});

Update: The object I'm trying to replace with has many properties like

{id: 1, Prop1: ..., Prop2:..., and so on}

Solution:

Thanks to dfsq but I found a proper solution within lodash that seems to work fine and is pretty neat and I put it in a mixin as well since I've this requirement at many places. JSBin

var update = function(arr, key, newval) {
  var match = _.find(arr, key);
  if(match)
    _.merge(match, newval);
  else
    arr.push(newval);    
};

_.mixin({ '$update': update });

var arr = [{id: 1, name: "Person 1"}, {id:2, name:"Person 2"}];

_.$update(arr, {id:1}, {id:1, name: "New Val"});


document.write(JSON.stringify(arr));

Faster Solution As pointed out by @dfsq, following is way faster

var upsert = function (arr, key, newval) {
    var match = _.find(arr, key);
    if(match){
        var index = _.indexOf(arr, _.find(arr, key));
        arr.splice(index, 1, newval);
    } else {
        arr.push(newval);
    }
};
Community
  • 1
  • 1
Vishal Seth
  • 4,948
  • 7
  • 26
  • 28
  • 7
    I think you can use match as the second param to _.indexOf on liine 4 of your "Faster Solution" as well, no need to recalculate that value there, which should make things a bit faster. – davertron Jul 21 '16 at 14:32
  • 2
    Even faster: use `_.findIndex` for match. – Julian K Aug 23 '18 at 23:56
  • 2
    Just to expand on what @JulianK and @davertron said, using `_.findIndex` instead of `_.find` will let you drop both the second `_.find` and the `_.indexOf`. You're iterating through the array 3 times when all you need is 1. – Justin Morgan - On strike Sep 10 '18 at 14:54

16 Answers16

235

In your case all you need to do is to find object in array and use Array.prototype.splice() method, read more details here:

var arr = [{id: 1, name: "Person 1"}, {id:2, name:"Person 2"}];

// Find item index using _.findIndex (thanks @AJ Richardson for comment)
var index = _.findIndex(arr, {id: 1});

// Replace item at index using native splice
arr.splice(index, 1, {id: 100, name: 'New object.'});

// "console.log" result
document.write(JSON.stringify( arr ));
<script src="//cdnjs.cloudflare.com/ajax/libs/lodash.js/2.4.1/lodash.min.js"></script>
Kate Orlova
  • 3,225
  • 5
  • 11
  • 35
dfsq
  • 191,768
  • 25
  • 236
  • 258
  • 1
    Well your solution is going to cost more in terms of performance, since `indexOf` is going to be very fast (it will use native browsers Array.prototype.indexOf). But anyway, glad you find a solution which works for you. – dfsq Dec 24 '14 at 21:25
  • 15
    Why not use `_.findIndex`? Then you don't need to use `_.indexOf`. – AJ Richardson Oct 19 '16 at 20:12
  • It makes a lot more sense to use `find` and not `findIndex`, actually. In most cases when you want to update an object you just want to update one or two properties. It doesn't make sense to recreate the object on the fly when only one or two properties are supposed to be updated. That said, I'm not saying you should use `indexOf`. I guess what I want to say is that the other answer is better. – ataravati Nov 27 '20 at 16:46
64

Seems like the simplest solution would to use ES6's .map or lodash's _.map:

var arr = [{id: 1, name: "Person 1"}, {id: 2, name: "Person 2"}];

// lodash
var newArr = _.map(arr, function(a) {
  return a.id === 1 ? {id: 1, name: "Person New Name"} : a;
});

// ES6
var newArr = arr.map(function(a) {
  return a.id === 1 ? {id: 1, name: "Person New Name"} : a;
});

This has the nice effect of avoiding mutating the original array.

Spencer
  • 878
  • 8
  • 5
  • 11
    But you're creating a new array each time... Worth to note. – kboom Feb 11 '17 at 11:33
  • 11
    The only alternative to not creating a new array is mutating the existing one, though. Also, creating a new array will likely have no impact in terms of performance. Upvote from me. – Aurelio Jul 17 '18 at 08:41
  • @kboom sometimes you don't want to mutate the existing array but to create a new one. – Envil Feb 05 '21 at 20:18
41

[ES6] This code works for me.

let result = array.map(item => item.id === updatedItem.id ? updatedItem : item)
mark333...333...333
  • 1,270
  • 1
  • 11
  • 26
shebik
  • 653
  • 9
  • 10
  • 1
    1. you are creating a new instance of array, so it is not true "replacing" an item. 2. you will lost your `updatedItem` if array does not include an item with the same `id`. – evilive Oct 07 '19 at 06:41
  • this is solution for 'update' not 'upsert' (the question was "is there a function in lodash to replace MATCHED item") and yes, it creates a copy of array, so don't use it if you need to work with same array (I didn't) – shebik Oct 23 '19 at 15:04
22
function findAndReplace(arr, find, replace) {
  let i;
  for(i=0; i < arr.length && arr[i].id != find.id; i++) {}
  i < arr.length ? arr[i] = replace : arr.push(replace);
}

Now let's test performance for all methods:

// TC's first approach
function first(arr, a, b) {
  _.each(arr, function (x, idx) {
    if (x.id === a.id) {
      arr[idx] = b;
      return false;
    }
  });
}

// solution with merge
function second(arr, a, b) {
  const match = _.find(arr, a);
  if (match) {
    _.merge(match, b);
  } else {
    arr.push(b);
  }
}

// most voted solution
function third(arr, a, b) {
  const match = _.find(arr, a);
  if (match) {
    var index = _.indexOf(arr, _.find(arr, a));
    arr.splice(index, 1, b);
  } else {
    arr.push(b);
  }
}

// my approach
function fourth(arr, a, b){
  let l;
  for(l=0; l < arr.length && arr[l].id != a.id; l++) {}
  l < arr.length ? arr[l] = b : arr.push(b);
}

function test(fn, times, el) {
  const arr = [], size = 250;
  for (let i = 0; i < size; i++) {
    arr[i] = {id: i, name: `name_${i}`, test: "test"};
  }

  let start = Date.now();
  _.times(times, () => {
    const id = Math.round(Math.random() * size);
    const a = {id};
    const b = {id, name: `${id}_name`};
    fn(arr, a, b);
  });
  el.innerHTML = Date.now() - start;
}

test(first, 1e5, document.getElementById("first"));
test(second, 1e5, document.getElementById("second"));
test(third, 1e5, document.getElementById("third"));
test(fourth, 1e5, document.getElementById("fourth"));
<script src="https://cdnjs.cloudflare.com/ajax/libs/lodash.js/4.14.1/lodash.min.js"></script>
<div>
  <ol>
    <li><b id="first"></b> ms [TC's first approach]</li>
    <li><b id="second"></b> ms [solution with merge]</li>
    <li><b id="third"></b> ms [most voted solution]</li>
    <li><b id="fourth"></b> ms [my approach]</li>
  </ol>
<div>
evilive
  • 1,781
  • 14
  • 20
  • 7
    Downvoted because being negative with people ("is this some kind of joke") turns them off from learning. Imagine if I finished this with "being a smart person I'd expect you to not be lazy about your emotions and think about that". – Aditya M P May 06 '17 at 08:50
  • 6
    I didn't want to hurt anybody, but I wonder how the solution that worst than original topic creator's approach got so much votes. What rules the people who gave their vote for that? And I dissapointed that people blindly trust the most voted answer and don't have critical thinking. – evilive May 08 '17 at 06:21
  • 1
    @evilive Valid points, but I don't see how those require you to come across as though everyone that previously provided answers/votes are idiots. The factual parts of this answer are great, the rest has the air of a barely contained superiority complex. That doesn't help anyone. You can easily make your points without the excessive emotional response. – Vala May 18 '17 at 16:47
  • 2
    It's worth to note though that your solution and TC's solution are only filtering by ids. That's the first reason that those two are running faster. The other two are allowing you to pass whatever part of the object you need for filtering which might be more preferable as an upsert function. – Aram Dec 26 '17 at 00:57
15

If you're just trying to replace one property, lodash _.find and _.set should be enough:

var arr = [{id: 1, name: "Person 1"}, {id: 2, name: "Person 2"}];

_.set(_.find(arr, {id: 1}), 'name', 'New Person');
Andrei Gavrilov
  • 151
  • 1
  • 2
12

You can also use findIndex and pick to achieve the same result:

  var arr  = [{id: 1, name: "Person 1"}, {id:2, name:"Person 2"}];
  var data = {id: 2, name: 'Person 2 (updated)'};
  var index = _.findIndex(arr, _.pick(data, 'id'));
  if( index !== -1) {
    arr.splice(index, 1, data);
  } else {
    arr.push(data);
  }
JVitela
  • 2,472
  • 2
  • 22
  • 20
8

As the time passes you should embrace a more functional approach in which you should avoid data mutations and write small, single responsibility functions. With the ECMAScript 6 standard, you can enjoy functional programming paradigm in JavaScript with the provided map, filter and reduce methods. You don't need another lodash, underscore or what else to do most basic things.

Down below I have included some proposed solutions to this problem in order to show how this problem can be solved using different language features:

Using ES6 map:

const replace = predicate => replacement => element =>
  predicate(element) ? replacement : element
 
const arr = [ { id: 1, name: "Person 1" }, { id:2, name:"Person 2" } ];
const predicate = element => element.id === 1
const replacement = { id: 100, name: 'New object.' }

const result = arr.map(replace (predicate) (replacement))
console.log(result)

Recursive version - equivalent of mapping:

Requires destructuring and array spread.

const replace = predicate => replacement =>
{
  const traverse = ([head, ...tail]) =>
    head
    ? [predicate(head) ? replacement : head, ...tail]
    : []
  return traverse
}
 
const arr = [ { id: 1, name: "Person 1" }, { id:2, name:"Person 2" } ];
const predicate = element => element.id === 1
const replacement = { id: 100, name: 'New object.' }

const result = replace (predicate) (replacement) (arr)
console.log(result)

When the final array's order is not important you can use an object as a HashMap data structure. Very handy if you already have keyed collection as an object - otherwise you have to change your representation first.

Requires object rest spread, computed property names and Object.entries.

const replace = key => ({id, ...values}) => hashMap =>
({
  ...hashMap,       //original HashMap
  [key]: undefined, //delete the replaced value
  [id]: values      //assign replacement
})

// HashMap <-> array conversion
const toHashMapById = array =>
  array.reduce(
    (acc, { id, ...values }) => 
    ({ ...acc, [id]: values })
  , {})
  
const toArrayById = hashMap =>
  Object.entries(hashMap)
  .filter( // filter out undefined values
    ([_, value]) => value 
  ) 
  .map(
    ([id, values]) => ({ id, ...values })
  )

const arr = [ { id: 1, name: "Person 1" }, { id:2, name:"Person 2" } ];
const replaceKey = 1
const replacement = { id: 100, name: 'New object.' }

// Create a HashMap from the array, treating id properties as keys
const hashMap = toHashMapById(arr)
console.log(hashMap)

// Result of replacement - notice an undefined value for replaced key
const resultHashMap = replace (replaceKey) (replacement) (hashMap)
console.log(resultHashMap)

// Final result of conversion from the HashMap to an array
const result = toArrayById (resultHashMap)
console.log(result)
Przemysław Zalewski
  • 3,836
  • 1
  • 23
  • 23
5

Came across this as well and did it simply that way.

const persons = [{id: 1, name: "Person 1"}, {id:2, name:"Person 2"}];
const updatedPerson = {id: 1, name: "new Person Name"}
const updatedPersons = persons.map(person => (
  person.id === updated.id
    ? updatedPerson
    : person
))

If wanted we can generalize it

const replaceWhere = (list, predicate, replacement) => {
  return list.map(item => predicate(item) ? replacement : item)
}

replaceWhere(persons, person => person.id === updatedPerson.id, updatedPerson)
rmmjohann
  • 141
  • 1
  • 8
3

Using lodash unionWith function, you can accomplish a simple upsert to an object. The documentation states that if there is a match, it will use the first array. Wrap your updated object in [ ] (array) and put it as the first array of the union function. Simply specify your matching logic and if found it will replace it and if not it will add it

Example:

let contacts = [
     {type: 'email', desc: 'work', primary: true, value: 'email prim'}, 
     {type: 'phone', desc: 'cell', primary: true, value:'phone prim'},
     {type: 'phone', desc: 'cell', primary: false,value:'phone secondary'},
     {type: 'email', desc: 'cell', primary: false,value:'email secondary'}
]

// Update contacts because found a match
_.unionWith([{type: 'email', desc: 'work', primary: true, value: 'email updated'}], contacts, (l, r) => l.type == r.type && l.primary == r.primary)

// Add to contacts - no match found
_.unionWith([{type: 'fax', desc: 'work', primary: true, value: 'fax added'}], contacts, (l, r) => l.type == r.type && l.primary == r.primary)
Jeffrey
  • 1,068
  • 2
  • 15
  • 25
3
var arr= [{id: 1, name: "Person 1"}, {id:2, name:"Person 2"}];
var index = _.findIndex(arr, {id: 1});
arr[index] = {id: 100, name: 'xyz'}
Amit Chhatbar
  • 93
  • 1
  • 9
2

If the insertion point of the new object does not need to match the previous object's index then the simplest way to do this with lodash is by using _.reject and then pushing new values in to the array:

var arr = [
  { id: 1, name: "Person 1" }, 
  { id: 2, name: "Person 2" }
];

arr = _.reject(arr, { id: 1 });
arr.push({ id: 1, name: "New Val" });

// result will be: [{ id: 2, name: "Person 2" }, { id: 1, name: "New Val" }]

If you have multiple values that you want to replace in one pass, you can do the following (written in non-ES6 format):

var arr = [
  { id: 1, name: "Person 1" }, 
  { id: 2, name: "Person 2" }, 
  { id: 3, name: "Person 3" }
];

idsToReplace = [2, 3];
arr = _.reject(arr, function(o) { return idsToReplace.indexOf(o.id) > -1; });
arr.push({ id: 3, name: "New Person 3" });
arr.push({ id: 2, name: "New Person 2" });


// result will be: [{ id: 1, name: "Person 1" }, { id: 3, name: "New Person 3" }, { id: 2, name: "New Person 2" }]
richt
  • 731
  • 7
  • 11
2

If you want to make a function and keep it "lodash-ey", you can make a wrapper function that works with callbacks. It makes the function more general use.

To write this try something like

function findAllAndReplace(array, replacement, callback){
    return array.map( element => callback(element) ? replacement : element )
}

To find and replace by key, just make your callback very simple. (itemInArray) => itemInArray.keyOnItem

But if you want more advanced functionality you can incorporate it with barely any extra effort. Here are some examples.

  1. (Simple) Find the item with id 2, replace it to have an id: 7
const items = [{id: 1}, {id: 2}, {id: 3}, {id: 4}, {id: 5}]

findAllAndReplace( items, {id: 7}, item => item.id === 2 )
  1. (Slightly More Complex) Find 28 year old named John, and replace him with a 28 year old named Jon

const people = [
    {
        name: "John",
        age: 20
    },
    {
        name: "John",
        age: 28
    },
    {
        name: "Jim",
        age: 28
    },
]


findAllAndReplace(
    people, // all the people
    { name: "Jon", age: 28 }, // Replacement value
    (person) => person.name === "jon" && person.age === 21 // callback function
)

Also, the method above will find all instances that match and replace them, but if you just want to do it for one you could do something like below.

function findOneAndReplace(array, replacement, callback){
    const splitIndex = array.findIndex(callback)
    
    // This if statement can be ommitted, but might 
    // be handy depending on your use case
    if(splitIndex < 0){
        throw new Error("Swap Element not found")
    }

    const leadingTerms = array.slice(0, splitIndex)
    const trailingTerms = array.slice(splitIndex + 1, array.length)
    return [...leadingTerms, replacement, ...trailingTerms]
)

note: It might be useful to make your function break if it doesn't find a matching element, but if you don't want that feature you can cut those lines of code out.

jjhiggz
  • 49
  • 4
1

Not bad variant too)

var arr = [{id: 1, name: "Person 1"}, {id: 2, name: "Person 2"}];

var id = 1; //id to find

arr[_.find(arr, {id: id})].name = 'New Person';
Ivan Pirus
  • 1,026
  • 11
  • 21
0

If you're looking for a way to immutably change the collection (as I was when I found your question), you might take a look at immutability-helper, a library forked from the original React util. In your case, you would accomplish what you mentioned via the following:

var update = require('immutability-helper')
var arr = [{id: 1, name: "Person 1"}, {id:2, name:"Person 2"}]
var newArray = update(arr, { 0: { name: { $set: 'New Name' } } })
//=> [{id: 1, name: "New Name"}, {id:2, name:"Person 2"}]
Aaron_H
  • 1,623
  • 1
  • 12
  • 26
0

You can do it without using lodash.

let arr = [{id: 1, name: "Person 1"}, {id: 2, name: "Person 2"}];
let newObj = {id: 1, name: "new Person"}

/*Add new prototype function on Array class*/
Array.prototype._replaceObj = function(newObj, key) {
  return this.map(obj => (obj[key] === newObj[key] ? newObj : obj));
};

/*return [{id: 1, name: "new Person"}, {id: 2, name: "Person 2"}]*/
arr._replaceObj(newObj, "id") 
Sunny
  • 89
  • 5
0

Immutable, suitable for ReactJS:

Assume:

cosnt arr = [{id: 1, name: "Person 1"}, {id:2, name:"Person 2"}];

The updated item is the second and name is changed to Special Person:

const updatedItem = {id:2, name:"Special Person"};

Hint: the lodash has useful tools but now we have some of them on Ecmascript6+, so I just use map function that is existed on both of lodash and ecmascript6+:

const newArr = arr.map(item => item.id === 2 ? updatedItem : item);
AmerllicA
  • 29,059
  • 15
  • 130
  • 154