Replace array entry with spread syntax in one line of code? [duplicate]
Asked Answered
A

8

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})
Amendment answered 14/8, 2017 at 11:43 Comment(0)
C
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.

Catchweight answered 14/8, 2017 at 11:49 Comment(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.Amendment
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?Subternatural
Please provide a more generic solution for unknown index.Encrimson
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?Edveh
if want to replace the item at index 4, this answer is incorrect, please refer to my answer for further explanation.Galluses
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)]);Meed
You should not access this.state from within setState. Use setState with a function instead: this.setState(prevState => { images: [ ...prevState.images.slice(0, 4), updatedImage, ...prevState.images.slice(5), ] })Mack
S
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.

Sara answered 14/8, 2017 at 11:47 Comment(6)
Really nice answer! But as you said, it's a bit tortured, making the code somewhat unreadableAmendment
@Kokodoko: But rather more efficient than spreading two temporary arrays. :-)Sara
Maybe I shouldn't use an array in the first place and avoid this complexity altogether?Amendment
If I use a variable instead of the number like this, will it work?Antofagasta
@vuquanghoang: Yes, if you use a computed property name. :-) I've updated the answer to show that, good suggestion!Sara
@SebastianSimon - Yeah, it's cool that's at Stage 3 now.Sara
T
12

You can use map:

const newImages = this.state.images
  .map((image, index) => index === 4 ? updatedImage : image)
Trillby answered 14/8, 2017 at 11:48 Comment(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.Amendment
@Amendment I think this will still be faster in this case. I might be missing something, but have a look here: jsperf.com/replace-array-entryTrillby
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.Amendment
I get totally different results with Chrome 60: speard - 13,149, map - 31,702, Object.assign - 12,744.Trillby
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!Amendment
I tested across all major browsers, and this method is the fastest. shorturl.at/aqNV3Kidnap
E
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);
Embellish answered 12/5, 2020 at 23:3 Comment(3)
Nice take, but is there any efficiency worries when converting an array to an object and back?Nebulize
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!Kidnap
However, across all major browsers, this method is the slowest, and I have a feeling it is because of that order preservation.Kidnap
K
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
 ]
Keto answered 8/8, 2018 at 13:37 Comment(0)
G
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)]})
Galluses answered 26/10, 2019 at 9:53 Comment(0)
H
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)]})
Hurds answered 5/10, 2021 at 13:43 Comment(0)
C
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);
Callisthenics answered 29/1, 2022 at 21:12 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.