48

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})
0

8 Answers 8

58

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.

7
  • 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
    Commented Aug 14, 2017 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? Commented May 4, 2019 at 19:39
  • 1
    Please provide a more generic solution for unknown index. Commented Jun 3, 2019 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
    Commented Jun 10, 2019 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
    Commented Oct 26, 2019 at 10:07
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.

6
  • 1
    Really nice answer! But as you said, it's a bit tortured, making the code somewhat unreadable
    – Kokodoko
    Commented Aug 14, 2017 at 12:20
  • 2
    @Kokodoko: But rather more efficient than spreading two temporary arrays. :-) Commented Aug 14, 2017 at 12:21
  • Maybe I shouldn't use an array in the first place and avoid this complexity altogether?
    – Kokodoko
    Commented Aug 14, 2017 at 12:26
  • If I use a variable instead of the number like this, will it work? Commented May 23, 2018 at 20:39
  • 1
    @vuquanghoang: Yes, if you use a computed property name. :-) I've updated the answer to show that, good suggestion! Commented May 24, 2018 at 7:05
12

You can use map:

const newImages = this.state.images
  .map((image, index) => index === 4 ? updatedImage : image)
6
  • 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
    Commented Aug 14, 2017 at 12:22
  • @Kokodoko I think this will still be faster in this case. I might be missing something, but have a look here: jsperf.com/replace-array-entry
    – CD..
    Commented Aug 14, 2017 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
    Commented Aug 14, 2017 at 12:39
  • 1
    I get totally different results with Chrome 60: speard - 13,149, map - 31,702, Object.assign - 12,744.
    – CD..
    Commented Aug 14, 2017 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
    Commented Aug 15, 2017 at 14:17
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);

3
  • Nice take, but is there any efficiency worries when converting an array to an object and back?
    – Dror Bar
    Commented Aug 3, 2020 at 13:25
  • The only question I had was if Object.values preserves the order of the array. After reading the docs, I am happy to report that it does!
    – smac89
    Commented Nov 23, 2021 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
    Commented Nov 23, 2021 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
 ]
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)]})
0
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)]})
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);

Not the answer you're looking for? Browse other questions tagged or ask your own question.