ABSimon
ABSimon

Reputation: 671

How to update a complex array?

I have this structure in state

allVotes: [
  {
    id: "vote_1",
    title: "How is your day?",
    description: "Tell me: how has your day been so far?",
    choices: [
      { id: "choice_1", title: "Good", count: 7 },
      { id: "choice_2", title: "Bad", count: 12 },
      { id: "choice_3", title: "Not sure yet", count: 1 },
    ],
  },
  {
    id: "vote_2",
    title: "Programming languages",
    description: "What is your preferred language?",
    choices: [
      { id: "choice_1", title: "JavaScript", count: 5 },
      { id: "choice_2", title: "Java", count: 9 },
      { id: "choice_3", title: "Plain english", count: 17 },
    ],
  },
],

and need to update count. This works (in a Controller Component)

registerVote(vote, choice) {
  this.setState(prevState => {
    return {
      allVotes: prevState.allVotes.map(
        pVote =>
          pVote.id !== vote.id
            ? pVote
            : {
                ...pVote,
                choices: pVote.choices.map(
                  pChoice =>
                    pChoice.id !== choice.id
                      ? pChoice
                      : {
                          ...pChoice,
                          count: pChoice.count + 1,
                        },
                ),
              },
      ),
    };
  });
}

but it looks very complicated and I am not quite sure if this is the right way. Is there better way?

Upvotes: 2

Views: 210

Answers (2)

skyboyer
skyboyer

Reputation: 23705

You can decomposite your component into smaller ones. Say: Form, Votes, Vote.

class Vote extends React.Component {
    increment = () => {
        this.setState(prevState => ({
            count: prevState.count + 1
        }), this.onItemChange);
    }

    onItemChange = () => {
        this.props.onItemChange(this.state, this.props.id);
    }
}


class Votes extends React.Component {
    onItemChangedCallback = (itemToUpdate, idToUpdate) => {
        this.setState(prevState => ({
            allVotes: prevState.allVotes.map(vote => vote.id === idToUpdate? {...itemToUpdate}: vote; )
        }), this.onVotesChangeCallback);
    }

    onVotesChangeCallback = () => {
        this.props.onVotesChange(this.state.allVotes)
    }

    render() {
        return (<div>this.state.allVotes.map(vote => <Vote id={vote.id} key={vote.id} vote={vote} onItemChange={this.onItemChangedCallback} />)</div>);
    }
}

Besides it means more classes/components this fits "single responsibility" principle: so Votes handles moving elements inside the list, deletion and adding new but not timestamp value, voters' amount or voters list. Vote handles single item. And so on. Better to test, easier to read, easier to reuse in different context.

[UPD] after more thinking on that I believe that it's better passing unique id from Votes to Vote rather than positional index(as well as key props suggests). And later use it id to figure out what collection's element should be replaced. Updated code snippet accordingly.

[UPD2] I guess Vote can be functional/stateless component but I better keep it class-based to keep idea easier to follow

Upvotes: 2

Jonas Wilms
Jonas Wilms

Reputation: 138267

Thats actually the way to do if you want to keep immutability, however you could simplify it a bit by using some helper functions:

 const change = (key, fn) => obj => ({ ...obj, [key]: fn(obj[key]) });
 const changeOne = (filter, fn) => arr => arr.map(el => filter(el) ? fn(el) : el);

 this.setState(change("allVotes", changeOne(v => v.id === vote.id, change("count", c => c + 1))));

Upvotes: 2

Related Questions