is there a function in lodash to replace matched item
Asked Answered
R

17

181

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);
    }
};
Racial answered 24/12, 2014 at 20:6 Comment(3)
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.Vacillation
Even faster: use _.findIndex for match.Townsman
Just to expand on what @JulianK and @Vacillation 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.Spawn
P
237

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>
Pore answered 24/12, 2014 at 20:24 Comment(3)
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.Pore
Why not use _.findIndex? Then you don't need to use _.indexOf.Winnah
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.Palmate
L
68

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.

Lorenzalorenzana answered 23/1, 2017 at 5:30 Comment(3)
But you're creating a new array each time... Worth to note.Mall
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.Unsearchable
@Mall sometimes you don't want to mutate the existing array but to create a new one.Damales
H
41

[ES6] This code works for me.

let result = array.map(item => item.id === updatedItem.id ? updatedItem : item)
Haggis answered 29/5, 2018 at 8:12 Comment(2)
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.Baez
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)Haggis
B
21
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>
Baez answered 2/8, 2016 at 7:59 Comment(4)
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".Logo
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.Baez
@Baez 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.Expressionism
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.Fiat
H
16

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');
Hibiscus answered 16/5, 2018 at 15:15 Comment(1)
This is the best because some of us only want to update one key value. This does that. ThanksGraeco
W
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);
  }
Whoso answered 28/8, 2015 at 13:49 Comment(0)
F
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)
Fencible answered 21/5, 2017 at 15:36 Comment(0)
N
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)
Nummulite answered 26/2, 2020 at 10:49 Comment(0)
C
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)
Churr answered 9/11, 2018 at 15:24 Comment(0)
T
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'}
Trimurti answered 11/9, 2019 at 9:46 Comment(0)
S
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" }]
Southpaw answered 31/3, 2017 at 13:0 Comment(1)
this method changes the array sortingInessive
G
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.

Genniegennifer answered 19/7, 2021 at 22:45 Comment(0)
T
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';
Tice answered 2/8, 2019 at 9:46 Comment(0)
G
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"}]
Grindstone answered 26/4, 2017 at 23:2 Comment(0)
L
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") 
Leap answered 7/9, 2018 at 23:59 Comment(0)
S
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);
Sharpen answered 20/2, 2020 at 1:33 Comment(0)
G
0

Maybe someone will like my solution:

export function findAndReplace<T>(arr: T[], newObj: T, compareKeyName: keyof T) {
  return arr?.map?.(item => {
    return item?.[compareKeyName] === newObj?.[compareKeyName] ? { ...item, ...newObj } : item;
  });
}

use:

interface Info {id: number, name: string}
const arr:Info[] = [{id: 1, name: "Person 1"}, {id: 2, name: "Person 2"}];
const newObj: Info = {id: 1, name: "new Person"}
const result = findAndReplace<Info>(arr, newObj, "id") // [{id: 1, name: "new Person"}, {id: 2, name: "Person 2"}];
Giralda answered 28/1, 2024 at 1:40 Comment(0)

© 2022 - 2025 — McMap. All rights reserved.