.map is suddenly not a function?

.map() is Suddenly Not a Function? [Solved]

Have you ever been working on a project, only to be jolted out of your flow state by an error message like this one?

Uncaught TypeError: this.props.ingredients.map is not a function

You’ve used .map() a bunch of times, so why is it suddenly not working? And of course it’s a function – in fact, the .map() method is one of JavaScript’s most useful higher order functions. So why does the error message say that it isn’t?

This article should answer all of those questions, and show how to this type of error in a real application.

The .map() method

.map() is an array method that goes through an array and applies a function to each element.

While the way it iterates through an array is similar to the .forEach() method, the important difference is that .map() returns a new array at the end.

const nums = [1, 2, 3, 4];

const doubles = nums.map(num => {
  return num * 2;
});

console.log(doubles); // [2, 4, 6, 8]

The .map is not a function error

This error is usually due to the fact that .map() is an array method, and does not work with other data types like strings or objects.

Let’s say you have a string of items and you want to return each as an <li> element that you’ll append to the page later.

If you try to call .map() on the string, you’ll get the .map is not a function error message:

const items = 'baseball bat, cap, yo-yo, fireworks';

items.map(item => console.log(item)); // Uncaught TypeError: items.map is not a function

How to fix the error

Because .map() is an array method, you’ll need to turn the data you want to work with into an array.

Using the example above, since items is a comma separated string, the .split() method is just what we need to turn it into an array of strings.

Then, just use the .map() method on the new array:

const items = 'baseball bat, cap, yo-yo, fireworks';

const itemsArr = items.split(',');
console.log(itemsArr); // [ 'baseball bat', ' cap', ' yo-yo', ' fireworks' ]

const nessItems = itemsArr.map(item => `<li>${item}</li>`);
console.log(nessItems); // ['<li>baseball bat</li>', '<li> cap</li>', '<li> yo-yo</li>', '<li> fireworks</li>']

Notice that every element after 'baseball bat' in itemsArr has extra white space at the beginning. This may or may not matter depending on how you’ll use the data.

But if you’d like to clean them up, inside the .map() method is a great place to do that:

const items = 'baseball bat, cap, yo-yo, fireworks';

const itemsArr = items.split(',');
console.log(itemsArr); // [ 'baseball bat', ' cap', ' yo-yo', ' fireworks' ]

const nessItems = itemsArr.map(item => `<li>${item.trim()}</li>`);
console.log(nessItems); // ['<li>baseball bat</li>', '<li>cap</li>', '<li>yo-yo</li>', '<li>fireworks</li>']

Now let’s take a look at a real application to see how this knowledge comes into play.

Debugging the .map is not a function error in a real app

Imagine you have this simple recipe box app:

import React from 'react';
import ReactDOM from 'react-dom';
import Card from 'react-bootstrap/Card';
import ButtonGroup from 'react-bootstrap/ButtonGroup';
import Button from 'react-bootstrap/Button';
import Modal from 'react-bootstrap/Modal';

class CollapsibleRecipe extends React.Component {
  constructor(props) {
    super(props);
    this.state = {
      open: false,
    };
  }

  render() {
    const titleRows = (
      <div>
        <a onClick={() => this.setState({ open: !this.state.open })}>
        <div>
          {this.props.food}
        </div>
        </a>
        <ButtonGroup>
          <Button variant='success' size='sm'>Add to shopping list</Button>
          <Button variant='danger' size='sm'>Delete Recipe</Button>
        </ButtonGroup>
      </div>
    );

    const ingredients = this.props.ingredients.map(ingredient => {
      return <li>{ingredient}</li>;
    });

    const cardStyle = {
      minWidth: '250px',
      maxWidth: '20%',
      borderColor: 'rgb(42, 42, 42)',
      borderWidth: '5px',
      borderRadius: '10px',
      margin: '2%',
      display: 'flex',
      justifyContent: 'center',
    };

    const titleStyle = {
      fontSize: '1.5em',
      textAlign: 'center',
      fontWeight: 'bold',
    }

    const imgStyle = {
      borderRadius: '10px',
      marginBottom: '2%',
    }

    return (
      <Card style={cardStyle}>
        <Card.Body>
          <Card.Title style={titleStyle}>{titleRows}</Card.Title>
          <Card.Img src={this.props.image} style={imgStyle} />
          <ul>
            {ingredients}
          </ul>
        </Card.Body>
      </Card>
    );
  }
}

class AddToList extends React.Component {
  constructor(props) {
    super(props);
    this.state = {
      showModal: false,
    };
  }

  handleClick() {
    this.setState({ showModal: true });
  }

  close() {
    this.setState({ showModal: false });
  }

  updateRecipes() {
    if ($('#title').val() && $('#ingredients').val()) {
      const recipe = {
        name: $('#title').val(),
        ingredients: $('#ingredients').val(),
      };
      if ($('#image').val()) {
        recipe['image'] = $('#image').val();
      }

      this.props.update(recipe);
      this.close();
    } else {
      alert('Hold up! You gotta fill in the necessary boxes!');
    }
  }

  render() {
    $('body').click(function(event) {
      if (!$(event.target).closest('#openModal').length && !$(event.target).is('#openModal')) {
        $('.modalDialog').hide();
      }
    });

    const myModal = (
      <Modal
        show={this.state.showModal}
        onHide={() => this.close()}
        bssize='large'
        aria-labelledby='contained-modal-title-lg'
      >
        <Modal.Header closeButton>
          <Modal.Title id='contained-modal-title-lg'>Add a new recipe</Modal.Title>
        </Modal.Header>
        <Modal.Body>
          <form>
            <h3>Name of Dish</h3>
            <input type='text' label='Recipe' placeholder='Recipe Name' id='title' />
            <h3>Ingredients</h3>
            <input
              type='textarea'
              label='Ingredients'
              placeholder='Enter Ingredients(commas to separate)'
              id='ingredients'
            />
            <h3>Image</h3>
            <input type='textarea' label='Image' placeholder='Enter a URL to an image(optional)' id='image' />
          </form>
        </Modal.Body>
        <Modal.Footer>
          <Button bsstyle='success' id='addRec' onClick={() => this.updateRecipes()}>
            Add Recipe
          </Button>
        </Modal.Footer>
      </Modal>
    );

    const addBtnStyle = {
      marginLeft: '2%'
    }

    return (
      <div>
        <button onClick={() => this.handleClick()} style={addBtnStyle}>
          +
        </button>
        {myModal}
      </div>
    );
  }
}

class FullBox extends React.Component {
  constructor(props) {
    super(props);
    this.state = {
      recipes: [
        {
          name: 'Baklava',
          ingredients: ['Flower', 'Baking soda', 'Pistachios', 'Honey', 'Puff Pastry', 'Love'],
          image: 'https://images.unsplash.com/photo-1598110750624-207050c4f28c?ixlib=rb-1.2.1&ixid=eyJhcHBfaWQiOjEyMDd9&auto=format&fit=crop&w=650&q=80',
        },
      ],
    };
    this.updateStatefulRecipes = this.updateStatefulRecipes.bind(this);
  }

  updateStatefulRecipes(recipe) {
    const newArr = this.state.recipes.concat(recipe);

    this.setState({
      recipes: newArr,
    });
  }

  render() {
    const localRecipes = this.state.recipes.map(item => {
      return (
        <CollapsibleRecipe
          key={item['name']}
          name={item['name']}
          ingredients={item['ingredients']}
          image={item['image']}
        />
      );
    });

    return (
      <div>
        {localRecipes}
        <AddToList update={this.updateStatefulRecipes} recipes={this.state.recipes} />
      </div>
    );
  }
}

ReactDOM.render(<FullBox />, document.getElementById('root'));

Which produces the following:


But when you click the “+” button and add a new recipe, you get the following error: Uncaught TypeError: this.props.ingredients.map is not a function

The call to this.props.ingredients.map is in the CollapsibleRecipe component:

const ingredients = this.props.ingredients.map(ingredient => {
  return <li>{ingredient}</li>;
});

This looks right, but it’s still throwing an error for some reason. And strangely, the Baklava recipe renders correctly.

If you log this.props to the console and take a look at the value for ingredients, you’ll see that it’s a string, not an array like .map() expects:

...
console.log(this.props.ingredients);

/*

{
  name: 'Tacos',
  ingredients: 'Meat, Cheese, Tortillas, Cilantro, Limes',
  image: 'https://images.unsplash.com/photo-1552332386-f8dd00dc2f85?ixlib=rb-1.2.1&ixid=eyJhcHBfaWQiOjEyMDd9&auto=format&fit=crop&w=1351&q=80'
}

*/

Like before, the trick is to turn the comma separated string into an array of strings.

If you go into the updateStatefulRecipes function, you’ll see some lines that receive a recipe from the form and add it to the array of recipes in the current state:

updateStatefulRecipes(recipe) {
  const newArr = this.state.recipes.concat(recipe);

  this.setState({
    recipes: newArr,
  });
}

Before concatenating the incoming recipe to the current recipes, you need to transform the string of ingredients into an array. Here’s one way to do that:

updateStatefulRecipes(recipe) {
  let newRecipe = { ...recipe };
  newRecipe.ingredients = newRecipe.ingredients.split(',');
  const newArr = this.state.recipes.concat(newRecipe);

  this.setState({
    recipes: newArr,
  });
}

And <li> elements ignore extra white space, you could update the .map() method in the CollapsibleRecipe component to clean up each ingredient string it’s operating on:

const ingredients = this.props.ingredients.map(ingredient => {
  return <li>{ingredient.trim()}</li>;
});

So with those changes, your code should look like this:

import React from 'react';
import ReactDOM from 'react-dom';
import Card from 'react-bootstrap/Card';
import ButtonGroup from 'react-bootstrap/ButtonGroup';
import Button from 'react-bootstrap/Button';
import Modal from 'react-bootstrap/Modal';

class CollapsibleRecipe extends React.Component {
  constructor(props) {
    super(props);
    this.state = {
      open: false,
    };
  }

  render() {
    const titleRows = (
      <div>
        <a onClick={() => this.setState({ open: !this.state.open })}>
        <div>
          {this.props.food}
        </div>
        </a>
        <ButtonGroup>
          <Button variant='success' size='sm'>Add to shopping list</Button>
          <Button variant='danger' size='sm'>Delete Recipe</Button>
        </ButtonGroup>
      </div>
    );

    const ingredients = this.props.ingredients.map(ingredient => {
      return <li>{ingredient.trim()}</li>;
    });

    const cardStyle = {
      minWidth: '250px',
      maxWidth: '20%',
      borderColor: 'rgb(42, 42, 42)',
      borderWidth: '5px',
      borderRadius: '10px',
      margin: '2%',
      display: 'flex',
      justifyContent: 'center',
    };

    const titleStyle = {
      fontSize: '1.5em',
      textAlign: 'center',
      fontWeight: 'bold',
    }

    const imgStyle = {
      borderRadius: '10px',
      marginBottom: '2%',
    }

    return (
      <Card style={cardStyle}>
        <Card.Body>
          <Card.Title style={titleStyle}>{titleRows}</Card.Title>
          <Card.Img src={this.props.image} style={imgStyle} />
          <ul>
            {ingredients}
          </ul>
        </Card.Body>
      </Card>
    );
  }
}

class AddToList extends React.Component {
  constructor(props) {
    super(props);
    this.state = {
      showModal: false,
    };
  }

  handleClick() {
    this.setState({ showModal: true });
  }

  close() {
    this.setState({ showModal: false });
  }

  updateRecipes() {
    if ($('#title').val() && $('#ingredients').val()) {
      const recipe = {
        name: $('#title').val(),
        ingredients: $('#ingredients').val(),
      };
      if ($('#image').val()) {
        recipe['image'] = $('#image').val();
      }

      this.props.update(recipe);
      this.close();
    } else {
      alert('Hold up! You gotta fill in the necessary boxes!');
    }
  }

  render() {
    $('body').click(function(event) {
      if (!$(event.target).closest('#openModal').length && !$(event.target).is('#openModal')) {
        $('.modalDialog').hide();
      }
    });

    const myModal = (
      <Modal
        show={this.state.showModal}
        onHide={() => this.close()}
        bssize='large'
        aria-labelledby='contained-modal-title-lg'
      >
        <Modal.Header closeButton>
          <Modal.Title id='contained-modal-title-lg'>Add a new recipe</Modal.Title>
        </Modal.Header>
        <Modal.Body>
          <form>
            <h3>Name of Dish</h3>
            <input type='text' label='Recipe' placeholder='Recipe Name' id='title' />
            <h3>Ingredients</h3>
            <input
              type='textarea'
              label='Ingredients'
              placeholder='Enter Ingredients(commas to separate)'
              id='ingredients'
            />
            <h3>Image</h3>
            <input type='textarea' label='Image' placeholder='Enter a URL to an image(optional)' id='image' />
          </form>
        </Modal.Body>
        <Modal.Footer>
          <Button bsstyle='success' id='addRec' onClick={() => this.updateRecipes()}>
            Add Recipe
          </Button>
        </Modal.Footer>
      </Modal>
    );

    const addBtnStyle = {
      marginLeft: '2%'
    }

    return (
      <div>
        <button onClick={() => this.handleClick()} style={addBtnStyle}>
          +
        </button>
        {myModal}
      </div>
    );
  }
}

class FullBox extends React.Component {
  constructor(props) {
    super(props);
    this.state = {
      recipes: [
        {
          name: 'Baklava',
          ingredients: ['Flower', 'Baking soda', 'Pistachios', 'Honey', 'Puff Pastry', 'Love'],
          image: 'https://images.unsplash.com/photo-1598110750624-207050c4f28c?ixlib=rb-1.2.1&ixid=eyJhcHBfaWQiOjEyMDd9&auto=format&fit=crop&w=650&q=80',
        },
      ],
    };
    this.updateStatefulRecipes = this.updateStatefulRecipes.bind(this);
  }

  updateStatefulRecipes(recipe) {
    let newRecipe = { ...recipe };
    newRecipe.ingredients = newRecipe.ingredients.split(',');
    const newArr = this.state.recipes.concat(newRecipe);

    this.setState({
      recipes: newArr,
    });
  }

  render() {
    const localRecipes = this.state.recipes.map(item => {
      return (
        <CollapsibleRecipe
          key={item['name']}
          name={item['name']}
          ingredients={item['ingredients']}
          image={item['image']}
        />
      );
    });

    return (
      <div>
        {localRecipes}
        <AddToList update={this.updateStatefulRecipes} recipes={this.state.recipes} />
      </div>
    );
  }
}

ReactDOM.render(<FullBox />, document.getElementById('root'));


Also, feel free to check out the live app and see if you can improve it.

2 Likes

One way to debug this is to go back down the chain from where it’s telling you the error has occured. If map is not a function, it might be because ingredients isn’t an array. Open your debugger and inspect ingredients or do a console.log to see if it’s defined and coming back as an array.

2 Likes

Here ingredients field would be a string and not an array.

@prohorova

That’s not the same ingredients that the console is complaining about. The console is now complaining about this statement

let ingredients = this.props.ingredients.map((item) => {
return (<ListGroupItem key={item}>{item}</ListGroupItem>)
});

Which works before I attempt to add a new recipe. I think this is because that my state gets updated but my components don’t rerender or update.

You’re adding a recipe with ingredients field being a string. A string doesn’t have a map function hence the error.

1 Like

Because your ingredient is in string form thats why error occured