.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.