Search code examples
reactjsinputstateonchangenested-object

Unable to set state on nested object reactjs


I'm having issues with updating the state of a nested object from an input.

Updated with more code. I am trying to build a multi step form. First page takes team information, second page takes player information.

Parent component:

export class MainForm extends Component {
state = {
    step: 1,
    teamName: '',
    teamManagerName: '',
    teamManagerEmail: '',
    player: [{
        firstName: '',
        lastName: '',
        email: '',
        height: ''
    }]
}

// proceed to next step
nextStep = () => {
    const { step } = this.state;
    this.setState({
        step: step + 1
    })
}

// go to previous step
prevStep = () => {
    const { step } = this.state;
    this.setState({
        step: step - 1
    })
}

// handle fields change
handleChange = input => e => {
    this.setState({[input]: e.target.value});
}

render() {
    const { step } = this.state;
    const { teamName, teamManagerName, teamManagerEmail, player: { firstName, lastName, email, height}} = this.state;
    const values = {teamName, teamManagerName, teamManagerEmail, player: { firstName, lastName, email, height}};

    switch(step) {
        case 1:
            return (

                <FormTeamDetails
                    nextStep={this.nextStep}
                    handleChange={this.handleChange}
                    values={values}
                />
                )
        case 2:
            return (
               <FormPlayerDetails
                    nextStep={this.nextStep}
                    prevStep={this.prevStep}
                    handleChange={this.handleChange}
                    values={values}
               />
            )
        case 3:
            return (
                <Confirm
                    nextStep={this.nextStep}
                    prevStep={this.prevStep}
                    values={values}
           />
            )
        case 4:
            return (
                <h1>Scuccess!</h1>
            )
    }

}

}

This next code snippet is the first page of the form and one of the child components. The handleChange function successfully does its job here.

export class FormTeamDetails extends Component {
continue = e => {
    e.preventDefault();
    this.props.nextStep();
}

render() {
    const { values, handleChange } = this.props;
    console.log(this.props)
    return (
        <Container>
            <Form>
                <FormGroup>
                    <Label for="teamName">Team Name</Label>
                    <Input 
                        type="teamName" 
                        name="teamName" 
                        onChange={handleChange('teamName')} 
                        defaultValue={values.teamName} />
                    <Label for="teamManagerName">Team Manager Name</Label>
                    <Input 
                        type="teamManagerName" 
                        name="teamManagerName" 
                        onChange={handleChange('teamManagerName')} 
                        defaultValue={values.teamManagerName}
                        />
                    <Label for="teamManagerEmail">Team Manager Email</Label>
                    <Input 
                        type="teamManagerEmail" 
                        name="teamManagerEmail" 
                        onChange={handleChange('teamManagerEmail')} 
                        defaultValue={values.teamManagerEmail} />

                    <Button 
                        label="Next"
                        // primary="true"
                        onClick={this.continue}
                        style={{float: 'right'}}>Next</Button>
                </FormGroup>
            </Form>
        </Container>

    )
}

}

This is the second page of the form, and where I'm having issues:

export class FormPlayerDetails extends Component {
continue = e => {
    e.preventDefault();
    this.props.nextStep();
}

back = e => {
    e.preventDefault();
    this.props.prevStep();
}

render() {
    const { values: { player }, handleChange, values } = this.props;

    return (
        <Container>
            <h3>Add players</h3>
            <Form>
            <Row form>
                <Col md={3}>
                <FormGroup>
                    <Input type="firstName" 
                    name="firstName" 
                    id="firstName" 
                    placeholder="First Name" 
                    defaultValue={values.player.firstName} 
                    onChange={handleChange('values.player.firstName')} 
                    />
                </FormGroup>
                </Col>
                <Col md={3}>
                <FormGroup>
                    <Input type="lastName" name="lastName" id="lastName" placeholder="Last Name"  />
                </FormGroup>
                </Col>
                <Col md={3}>
                <FormGroup>
                    <Input type="email" name="email" id="email" placeholder="Email" />
                </FormGroup>
                </Col>
            </Row>

            <Row form>
                <Col>
                <Button 
                        label="Back"
                        // primary="true"
                        onClick={this.back}
                        style={{float: 'left'}}>Back</Button>
                </Col>
                <Col>
                <Button 
                        label="Next"
                        // primary="true"
                        onClick={this.continue}
                        style={{float: 'right'}}>Next</Button>
                </Col>
            </Row>

            </Form>
        </Container>

    )
}

}

I'm unable to update the player property with the input values. Also, I would like to add multiple player input fields in this component. I would like at least 5 players to be added to the team, hence the player array in the parent component. Please let me know if I should go about this in another way. Thank you in advance!


Solution

  • Your player property in your state is an array but you are treating it as an object. So, I assume there will be one player and it will be an object. Here is one solution for your case. But it feels like you are struggling with many unnecessary things there like passing values, etc since you have already name attribute for your inputs. I don't know, maybe you are thinking about other situations.

    Here is the handleChange part:

    handleChange = input => e => {
      const { target } = e;
      if (input === "player") {
        this.setState(prev => ({
          player: {
            ...prev.player,
            [target.name]: target.value
          }
        }));
      } else {
        this.setState({ [input]: target.value });
      }
    };
    

    Here is the input part:

    <Input
      type="firstName"
      name="firstName"
      id="firstName"
      placeholder="First Name"
      defaultValue={values.player.firstName}
      onChange={handleChange("player")}
    />
    

    As you can see, I'm passing the handleChange the player property and get the name from your input. Then in the handleChange method, using the functional state and spread syntax, I'm updating the player.

    class App extends React.Component {
      state = {
        step: 1,
        teamName: "",
        player: {
          firstName: "",
          lastName: "",
          email: ""
        }
      };
    
      handleChange = input => e => {
        const { target } = e;
        if (input === "player") {
          this.setState(prev => ({
            player: {
              ...prev.player,
              [target.name]: target.value
            }
          }));
        } else {
          this.setState({ [input]: target.value });
        }
      };
    
      render() {
        return (
          <div>
            <Input handleChange={this.handleChange} />
            <div>{JSON.stringify(this.state)}</div>
          </div>
        );
      }
    }
    
    class Input extends React.Component {
      render() {
        const { handleChange } = this.props;
        return (
          <div>
            <div>
              <span>firstName</span>
              <input
                type="firstName"
                name="firstName"
                id="firstName"
                onChange={handleChange("player")}
              />
            </div>
            <div>
              <span>team</span>
              <input
                type="teamName"
                name="teamName"
                onChange={handleChange("teamName")}
              />
            </div>
          </div>
        );
      }
    }
    
    ReactDOM.render(<App />, document.getElementById("root"));
    <script src="https://cdnjs.cloudflare.com/ajax/libs/react/16.6.3/umd/react.production.min.js"></script>
    <script src="https://cdnjs.cloudflare.com/ajax/libs/react-dom/16.6.3/umd/react-dom.production.min.js"></script>
    <div id="root" />

    Here is the updated version of the players as an array (be careful about the plural name). So, in this naive version, we keep a players array with some unique id's on them. This is how we are going to target the updated player. Check each input, there is an id now and we set it to the player's one. In the handleChange function, this time we are mapping the players (so a new array is returned), then if the id matches with our player's one, we are updating its relevant property. If the id does not match, we are returning the original player without doing anything.

    The handleChange part is the one you need to focus on. We are mapping and rendering the inputs by using each player's properties in the Input component. It seems a little bit confusing but if you look into it deeply you can understand. We are using Object.entries to map the properties but before that, we are cleaning the id one since we don't want to display it.

    You can find the working snippet below.

    class App extends React.Component {
      state = {
        step: 1,
        teamName: "",
        players: [
          {
            id: 1,
            firstName: "foo",
            lastName: "foo's lastname",
            email: "foo@foo.com"
          },
          {
            id: 2,
            firstName: "bar",
            lastName: "bar's lastname",
            email: "bar@bar.com"
          }
        ]
      };
    
      handleChange = input => e => {
        const { target } = e;
        if (input === "players") {
          this.setState({
            players: this.state.players.map(player => {
              if (player.id === Number(target.id)) {
                return { ...player, [target.name]: target.value };
              }
    
              return player;
            })
          });
        } else {
          this.setState({ [input]: target.value });
        }
      };
    
      render() {
        return (
          <div>
            <Input handleChange={this.handleChange} players={this.state.players} />
            <div>
              {this.state.players.map(player => (
                <div>
                  <h3>Player {player.id}</h3>
                  <div>First Name: {player.firstName}</div>
                  <div>Last Name: {player.lastName}</div>
                  <div>Email: {player.email}</div>
                </div>
              ))}
            </div>
          </div>
        );
      }
    }
    
    class Input extends React.Component {
      render() {
        const { handleChange, players } = this.props;
        return (
          <div>
            {players.map(player => {
              const { id, ...playerWithoutId } = player;
              return (
                <div key={player.id}>
                  <h3>Player {player.id}</h3>
                  {Object.entries(playerWithoutId).map(([key, value]) => (
                    <div>
                      <span>{key}</span>
                      <input
                        name={key}
                        id={player.id}
                        onChange={handleChange("players")}
                        defaultValue={value}
                      />
                    </div>
                  ))}
                </div>
              );
            })}
          </div>
        );
      }
    }
    
    ReactDOM.render(<App />, document.getElementById("root"));
    <script src="https://cdnjs.cloudflare.com/ajax/libs/react/16.6.3/umd/react.production.min.js"></script>
    <script src="https://cdnjs.cloudflare.com/ajax/libs/react-dom/16.6.3/umd/react-dom.production.min.js"></script>
    <div id="root" />