46

I'm replacing an item in a react state array by using the ... spread syntax. This works:

let newImages = [...this.state.images]
newImages[4] = updatedImage
this.setState({images:newImages})

Would it be possible to do this in one line of code? Something like this? (this doesn't work obviously...)

this.setState({images: [...this.state.images, [4]:updatedImage})
Patrick Roberts
  • 49,224
  • 10
  • 102
  • 153
Kokodoko
  • 26,167
  • 33
  • 120
  • 197

8 Answers8

57

use Array.slice

this.setState({
  images: [
    ...this.state.images.slice(0, 4),
    updatedImage,
    ...this.state.images.slice(5),
  ],
});

Edit from original post: changed the 3 o a 4 in the second parameter of the slice method since the second parameter points to the member of the array that is beyond the last one kept, it now correctly answers the original question.

neaumusic
  • 10,027
  • 9
  • 55
  • 83
Amir Ghezelbash
  • 2,195
  • 15
  • 24
  • This answer shows the best understanding of the ...spread operator, and it doesn't use any other fancy secret javascript magic, so I'll mark this one as the answer. – Kokodoko Aug 14 '17 at 12:17
  • 4
    How would you do this when the index is unknown? if n=0 you would get slice(0, -1) for n-1. Is there an elegant way? – user3808307 May 04 '19 at 19:39
  • 1
    Please provide a more generic solution for unknown index. – devmiles.com Jun 03 '19 at 10:21
  • 1
    If the index is unknown, you have to start doing a search, and trying to do it in one line is rarely a good idea. You'd have one really long line that's not very readable. Why not write a helper function to get the index, and then use this snippet? – William Jun 10 '19 at 20:53
  • 2
    if want to replace the item at index 4, this answer is incorrect, please refer to my answer for further explanation. – V-SHY Oct 26 '19 at 10:07
  • If you don't know the index of the object you want to update, but have its new updated version, you can do this: const updatedObject = {.....}; const index = array.indexOf(array.find(item => item.id === updatedObject.id)); setArray([...array.slice(0, index - 1), updatedObject, ...array.slice(index)]); – Nikolai May 26 '21 at 14:28
31

Once the change array by copy proposal is widely supported (it's at Stage 3, so should be finding its way into JavaScript engines), you'll be able to do this with the new with method:

// Using a Stage 3 proposal, not widely supported yet as of Nov 17 2022
this.setState({images: this.state.images.with(4, updatedImage)});

Until then, Object.assign does the job:

this.setState({images: Object.assign([], this.state.images, {4: updatedImage}));

...but involves a temporary object (the one at the end). Still, just the one temp object... If you do this with slice and spreading out arrays, it involve several more temporary objects (the two arrays from slice, the iterators for them, the result objects created by calling the iterator's next function [inside the ... handle], etc.).

It works because normal JS arrays aren't really arrays1 (this is subject to optimization, of course), they're objects with some special features. Their "indexes" are actually property names meeting certain criteria2. So there, we're spreading out this.state.images into a new array, passing that into Object.assign as the target, and giving Object.assign an object with a property named "4" (yes, it ends up being a string but we're allowed to write it as a number) with the value we want to update.

Live Example:

const a = [0, 1, 2, 3, 4, 5, 6, 7];
const b = Object.assign([], a, {4: "four"});
console.log(b);

If the 4 can be variable, that's fine, you can use a computed property name (new in ES2015):

let n = 4;
this.setState({images: Object.assign([], this.state.images, {[n]: updatedImage}));

Note the [] around n.

Live Example:

const a = [0, 1, 2, 3, 4, 5, 6, 7];
const index = 4;
const b = Object.assign([], a, {[index]: "four"});
console.log(b);

1 Disclosure: That's a post on my anemic little blog.

2 It's the second paragraph after the bullet list:

An integer index is a String-valued property key that is a canonical numeric String (see 7.1.16) and whose numeric value is either +0 or a positive integer ≤ 253-1. An array index is an integer index whose numeric value i is in the range +0 ≤ i < 232-1.

So that Object.assign does the same thing as your create-the-array-then-update-index-4.

T.J. Crowder
  • 1,031,962
  • 187
  • 1,923
  • 1,875
12

You can use map:

const newImages = this.state.images
  .map((image, index) => index === 4 ? updatedImage : image)
CD..
  • 72,281
  • 25
  • 154
  • 163
  • Hmyeah but if my array has 1000s of entries, I'll have to map through them all each time I want to update one entry. – Kokodoko Aug 14 '17 at 12:22
  • @Kokodoko I think this will still be faster in this case. I might be missing something, but have a look here: https://jsperf.com/replace-array-entry – CD.. Aug 14 '17 at 12:32
  • 1
    Thanks for building this test, that's awesome! When I run it, it seems that Object.assign is fastest with 168.000 ops/second, closely followed by spread with 146.000 ops/second. Map is by far the slowest with only 27.000 ops/second. – Kokodoko Aug 14 '17 at 12:39
  • 1
    I get totally different results with Chrome 60: speard - 13,149, map - 31,702, Object.assign - 12,744. – CD.. Aug 14 '17 at 12:46
  • 1
    You're right! This is very weird. I tested in Chrome and Safari. On Chrome, `map` is much faster than in Safari. On Safari, `spread` and `assign` are almost **10 times as fast** as on Chrome! – Kokodoko Aug 15 '17 at 14:17
  • I tested across all major browsers, and this method is the fastest. shorturl.at/aqNV3 – smac89 Nov 23 '21 at 21:51
9

You can convert the array to objects (the ...array1), replace the item (the [1]:"seven"), then convert it back to an array (Object.values) :

array1 = ["one", "two", "three"];
array2 = Object.values({...array1, [1]:"seven"});
console.log(array1);
console.log(array2);
furnaceX
  • 567
  • 2
  • 8
  • 15
  • Nice take, but is there any efficiency worries when converting an array to an object and back? – Dror Bar Aug 03 '20 at 13:25
  • The only question I had was if Object.values preserves the order of the array. After reading the [docs](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Object/values), I am happy to report that it does! – smac89 Nov 23 '21 at 21:36
  • However, across all major browsers, this method is the slowest, and I have a feeling it is because of that order preservation. – smac89 Nov 23 '21 at 21:47
3

Here is my self explaning non-one-liner

 const wantedIndex = 4;
 const oldArray = state.posts; // example

 const updated = {
     ...oldArray[wantedIndex], 
     read: !oldArray[wantedIndex].read // attributes to change...
 } 

 const before = oldArray.slice(0, wantedIndex); 
 const after = oldArray.slice(wantedIndex + 1);

 const menu = [
     ...before,  
     updated,
     ...after
 ]
webmaster
  • 1,960
  • 24
  • 29
2

I refer to @Bardia Rastin solution, and I found that the solution has a mistake at the index value (it replaces item at index 3 but not 4).

If you want to replace the item which has index value, index, the answer should be

this.setState({images: [...this.state.images.slice(0, index), updatedImage, ...this.state.images.slice(index + 1)]})

this.state.images.slice(0, index) is a new array has items start from 0 to index - 1 (index is not included)

this.state.images.slice(index) is a new array has items starts from index and afterwards.

To correctly replace item at index 4, answer should be:

this.setState({images: [...this.state.images.slice(0, 4), updatedImage, ...this.state.images.slice(5)]})
Leniel Maccaferri
  • 100,159
  • 46
  • 371
  • 480
V-SHY
  • 3,925
  • 4
  • 31
  • 47
1

first find the index, here I use the image document id docId as illustration:

const index = images.findIndex((prevPhoto)=>prevPhoto.docId === docId)
this.setState({images: [...this.state.images.slice(0,index), updatedImage, ...this.state.images.slice(index+1)]})
Julio Spinelli
  • 587
  • 3
  • 16
0

I have tried a lot of using the spread operator. I think when you use splice() it changes the main array. So the solution I discovered is to clone the array in new variables and then split it using the spread operator. The example I used.

var cart = [];

function addItem(item) {
    let name = item.name;
    let price = item.price;
    let count = item.count;
    let id = item.id;

    cart.push({
        id,
        name,
        price,
        count,
    });

    return;
}

function removeItem(id) {
    let itemExist = false;
    let index = 0;
    for (let j = 0; j < cart.length; j++) {
        if (cart[j].id === id) { itemExist = true; break; }
        index++;
    }
    if (itemExist) {
        cart.splice(index, 1);
    }
    return;
}

function changeCount(id, newCount) {
    let itemExist = false;
    let index = 0;
    for (let j = 0; j < cart.length; j++) {
        console.log("J: ", j)
        if (cart[j].id === id) {
            itemExist = true;
            index = j;
            break;
        }
    }
    console.log(index);
    if (itemExist) {
        let temp1 = [...cart];
        let temp2 = [...cart];
        let temp3 = [...cart];
        cart = [...temp1.splice(0, index),
            {
                ...temp2[index],
                count: newCount
            },
            ...temp3.splice(index + 1, cart.length)
        ];
    }

    return;
}

addItem({
    id: 1,
    name: "item 1",
    price: 10,
    count: 1
});
addItem({
    id: 2,
    name: "item 2",
    price: 11,
    count: 1
});
addItem({
    id: 3,
    name: "item 3",
    price: 12,
    count: 2
});
addItem({
    id: 4,
    name: "item 4",
    price: 13,
    count: 2
});

changeCount(4, 5);
console.log("AFTER CHANGE!");
console.log(cart);