1

I have an array of JavaScript objects that I holding in React State, and on a click, I change the property of one of the objects in the array.

I got the following to work without mutating state, but my current setState() syntax also adds the same object to the end of the array again.

How can I simply change the state of one of the objects in my array of objects in state, without adding another object and without mutating state?

import React, { useState } from 'react';

interface IFlashcard {
    noun: string;
    article: string;
    show: boolean;
}

const initialFlashcards = [
    {
        noun: 'Dependency',
        article: 'die Dependency, die Dependencys',
        show: false
    },
    {
        noun: 'Kenntnis',
        article: 'die Kenntnis, die Kenntnisse',
        show: false
    },
    {
        noun: 'Repository',
        article: 'das Repository, die Repositorys',
        show: false
    },
    {
        noun: 'Kenntnis',
        article: 'die Kenntnis, die Kenntnisse',
        show: false
    }
];

function LanguageFlashcards() {
    const [flashcards, setFlashcards] = useState(initialFlashcards);

    const toggleFlashcard = (flashcard: IFlashcard) => {
        flashcard.show = !flashcard.show;
        setFlashcards([...flashcards, flashcard]);
    }

    return (
        <>
            <h2>Language Flashcards</h2>
            <ul>
                {flashcards.map((flashcard: IFlashcard) => {
                    return (
                        <>
                            <li><span onClick={() => toggleFlashcard(flashcard)}>{flashcard.noun}</span>
                                {flashcard.show && (
                                    <>
                                        : {flashcard.article}
                                    </>
                                )}
                            </li>
                        </>
                    )
                })}
            </ul>
        </>
    );
}

export default LanguageFlashcards;
Ronak Shah
  • 377,200
  • 20
  • 156
  • 213
Edward Tanguay
  • 189,012
  • 314
  • 712
  • 1,047

4 Answers4

4

Your example is in fact mutating state here:

flashcard.show = !flashcard.show;

At this point, flashcard refers directly to an object in state, so altering one of its properties is a mutation.

You need a way to identify the objects in state so that you can extract one, clone it individually, and then insert it back into a cloned state array in its original position. Without changing any of your data, you could do this by passing the array position of the flashcard when you call toggleFlashcard.

{flashcards.map((flashcard: IFlashcard, i: number) => {
    return (
        <>
            <li><span onClick={() => toggleFlashcard(i)}>{flashcard.noun}</span>
                {flashcard.show && (
                    <>
                        : {flashcard.article}
                    </>
                )}
            </li>
        </>
    )
})}

Now the toggleFlashcard event handler should look something like this:

const toggleFlashcard = (i: number) => {
    const clonedCard = {...flashcards[i]};
    clonedCard.show = !clonedCard.show;
  
    const clonedState = [...flashcards];
    clonedState[i] = clonedCard;
  
    setFlashcards(clonedState);
}
lawrence-witt
  • 8,094
  • 3
  • 13
  • 32
1

If you don't want to mutate anything, please try this solution.

const toggleFlashcard = (flashcard: IFlashcard) => {
  const flashcardIndex = flashcards.findIndex(f => f === flashcard);
  const newFlashcards = [...flashcards];
  newFlashcards[flashcardIndex]= { ...flashcard, show: !flashcard.show };
  setFlashcards(newFlashcards);
};

And this is not related to the main topic but the key attribute is missing here.

{flashcards.map((flashcard: IFlashcard, index: number) => {
...
<li key={index}><span onClick={() => toggleFlashcard(flashcard)}>{flashcard.noun}</span>
                              

If you don't specify the key attribute, you will see React warnings.

  • [Mutating state in React is a dangerous anti-pattern.](https://stackoverflow.com/q/37755997/1218980) I wouldn't recommend this as the first solution to any answer about React. – Emile Bergeron Apr 08 '21 at 21:17
  • 1
    Yeah. you're right. That's why I provided another solution which we don't mutate the state. @EmileBergeron –  Apr 08 '21 at 21:27
  • 1
    A small improvement would be to avoid cloning every object, and only cloning the one that's changing (and the array obviously). – Emile Bergeron Apr 08 '21 at 21:33
  • 1
    I updated my answer according to your comment. @EmileBergeron It will avoid cloning every object. –  Apr 08 '21 at 21:50
-1

To do what you want you could do something like this

const toggleFlashcard = (flashcardIndex: number) => {
    const flashcardsDeepCopy: IFlashcard[] = JSON.parse(JSON.stringify(flashcards));
    flashcardsDeepCopy[flashcardIndex].show = !flashcard.show;
    setFlashcards(flashcardsDeepCopy);
}

In jsx you need to pass the index

<ul>
  {flashcards.map((flashcard: IFlashcard, index) => {
    return (
      <>
        <li><span onClick={() => toggleFlashcard(index)}>{flashcard.noun}</span>
          {flashcard.show && (
            <>
              : {flashcard.article}
            </>
          )}
        </li>
      </>
    )
  })}
</ul>
Serhiy Mamedov
  • 1,080
  • 5
  • 11
-1

Your problem is with your toggleFlashcard function you think you aren't mutating the original state but you are given that javascript passes your object by reference. If you were to just do

const toggleFlashcard = (flashcard: IFlashcard) => {
        flashcard.show = !flashcard.show;
        setFlashcards([...flashcards]);
    }

It would work however that isn't really react practice. What you would need to do is return a brand new array without modifying the original object. youll need some sort of identifier to filter out which flash card is which. maybe appending the index of the item to the renderer or pass in something from the backend.

Steven Burnett
  • 288
  • 2
  • 12