2

I have a react component called App that contains 2 components: Form and Table. And both of them are controlled component.

In the form, there is an input element and a button.
The input element has an onChange attribute; whenever the value changes it changes the value in the App's state.
The button has an onClick attribute that is provided by the App component; Whenever the button is clicked, the state's firstNames (which is an array) will be added with the state value of firstname.

The problem is when I clicked the button, it will throw an error saying that I didn't pass in an array and that it doesn't have a map method, even though in the call back, the updated state does show an array.

Below is the code:

function Form(props) {
  return (
    <form>
      <label>
        Item: <input value={props.value} onChange={props.handleChange} />
      </label>
      <button onClick={props.handleClick}>Submit</button>
    </form>
  );
}

function Table(props) {
  let firstNames = props.names.map((item, index) => {
    return (
      <tr key={index}>
        <td>{item}</td>
      </tr>
    );
  });
  return (
    <table>
      <thead>
        <tr>
          <th>First Name</th>
        </tr>
      </thead>
      <tbody>{firstNames}</tbody>
    </table>
  );
}

class App extends React.Component {
  constructor(props) {
    super(props);
    this.state = {
      inputField: "",
      firstNames: ["Joey", "Chloe"],
    };
    this.handleChange = this.handleChange.bind(this);
    this.handleClick = this.handleClick.bind(this);
  }
  handleChange(event) {
    this.setState({ inputField: event.target.value });
  }
  handleClick() {
    this.setState(
      {
        firstNames: this.state.firstNames.push(this.state.inputField),
      },
      console.log(this.state.firstNames)
    );
  }
  render() {
    return (
      <div>
        <Form
          value={this.state.inputField}
          handleChange={this.handleChange}
          handleClick={this.handleClick}
        />
        <Table names={this.state.firstNames} />
      </div>
    );
  }
}

ReactDOM.render(<App />, document.getElementById("root"));
Eddie Lam
  • 579
  • 1
  • 5
  • 22

1 Answers1

1

push mutates the original array, but it returns the length of the updated array, therefor, after the initial push, your firstNames inside state will be a number, which doesn't have map

You shouldn't mutate state variables, you should create a new array instead when adding a new name, for example, like this:

this.setState({
    firstNames: [...this.state.firstNames, this.state.inputField]
})

The full sample code would then look something like this:

function Form(props) {
  return (
    <form onSubmit={props.handleClick}>
      <label>
        Item: <input value={props.value} onChange={props.handleChange} />
      </label>
      <button>Submit</button>
    </form>
  );
}

function Table(props) {
  let firstNames = props.names.map((item, index) => {
    return (
      <tr key={index}>
        <td>{item}</td>
      </tr>
    );
  });
  return (
    <table>
      <thead>
        <tr>
          <th>First Name</th>
        </tr>
      </thead>
      <tbody>{firstNames}</tbody>
    </table>
  );
}

class App extends React.Component {
  constructor(props) {
    super(props);
    this.state = {
      inputField: "",
      firstNames: ["Joey", "Chloe"],
    };
    this.handleChange = this.handleChange.bind(this);
    this.handleClick = this.handleClick.bind(this);
  }
  handleChange(event) {
    this.setState({ inputField: event.target.value });
  }
  handleClick( e ) {
    event.preventDefault();
    this.setState(
      {
        firstNames: [...this.state.firstNames, this.state.inputField],
        inputField: ''
      }, () => console.log(this.state.firstNames) );
  }
  render() {
    return (
      <div>
        <Form
          value={this.state.inputField}
          handleChange={this.handleChange}
          handleClick={this.handleClick}
        />
        <Table names={this.state.firstNames} />
      </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"></div>

Following 2 things I still updated in your code:

Added the type="button" so that a submit doesn't happen

<button type="button" onClick={props.handleClick}>Submit</button>

Changed the callback of the setState which you used for logging

this.setState({
  firstNames: [...this.state.firstNames, this.state.inputField],
  inputField: ''
}, () => console.log(this.state.firstNames) );

When you did it in your original way, the console.log would be trigger before you could be sure that the setState call has actually happened.

Another note perhaps could be that using index for keys can lead to problems afterwards (be it sorting or removing items), a key should be unique. In this code, it's merely a hint

Icepickle
  • 12,689
  • 3
  • 34
  • 48
  • Great! Thank you so much! Now another problem is when I hit enter, the input field is cleared but the value does not get added onto the table. How do we usually deal with problem like that? – Eddie Lam Jun 10 '20 at 22:51
  • I updated the handle for it, you just add the `onSubmit` on the form (and then you can ofcourse revert the button to the standard `submit` button, and forget about the `onClick` event there – Icepickle Jun 10 '20 at 22:54
  • You're awesome! Thank you so much! – Eddie Lam Jun 10 '20 at 23:00