reactjslistformscontrolled-component

props.names.map is not a function


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"));

Solution

  • 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