Reputation: 12433
I want to have a component with a list of products and a list of listItems which renders each product, but it is giving me the error:
TypeError: Cannot read property 'products' of undefined
new ProductList
src/components/products/ProductList.js:11
Here is my code:
import React, { Component } from "react";
import productData from "./model";
import Product from "./Product"
class ProductList extends Component {
constructor(props) {
super(props);
this.state = {
products: productData,
listItems: this.state.products.map(product => (
<Product key={product.name.toString()} product={product} />
))
};
}
addProduct = (product) => {
this.state.products.push(product);
this.setState({
products: this.state.products
});
};
render() {
return (
<div>
<ul>{this.state.listItems}</ul>
<button onClick={e => this.addProduct({name: "some product", price: 54})}>Add Product</button>
</div>
);
}
}
export default ProductList;
model.js:
interface Product {
name: string;
price: number;
}
let productData: Array<Product> = [
{ name: "Sledgehammer", price: 125.75 },
{ name: "Axe", price: 190.5 },
{ name: "Bandsaw", price: 562.13 },
{ name: "Chisel", price: 12.9 },
{ name: "Hacksaw", price: 18.45 }
];
export default productData
When I change to:
listItems: this.products.map(product => (
<Product key={product.name.toString()} product={product} />
))
I get:
TypeError: Cannot read property 'map' of undefined
What am I doing wrong?
Upvotes: 0
Views: 1061
Reputation: 6668
I think you wanted to map over productsData
and not this.state
while initializing the state
listItems: productsData.map(product => (
<Product key={product.name.toString()} product={product} />
))
You can probably move the listItems out from state and just recreate it in the render method
render() {
const listItems = this.state.products.map(product => (
<Product key={product.name.toString()} product={product} />
))
return (
<div>
<ul>{listItems}</ul>
<button onClick={e => this.addProduct({name: "some product", price: 54})}>Add Product</button>
</div>
);
}
or recalculate the listItems state in addProduct method -
addProduct = (product) => {
const newProducts = this.state.products.concat(product)
this.setState({
products: newProducts,
listItems: newProducts.map(product => (
<Product key={product.name.toString()} product={product} />
))
});
};
Upvotes: 1
Reputation: 341
In the first snippet of code, you have:
this.state = {
products: productData,
listItems: this.state.products.map(product => (
<Product key={product.name.toString()} product={product} />
))
};
At that point, this.state
is undefined
and is in the process of being assigned a value. The subsequent self-referential call this.state.products
is trying to find a products attribute on this.state
, which is undefined
.
In the second snippet of code, you have:
this.state = {
products: productData,
listItems: this.products.map(product => (
<Product key={product.name.toString()} product={product} />
))
};
For this.products
, is a reference to the products attribute on an instance ProductList, which also does not exist.
Try something like this:
import React, { Component } from "react";
import productData from "./model";
import Product from "./Product";
class ProductList extends Component {
constructor(props) {
super(props);
this.state = {
products: productData
};
}
addProduct = product => {
const { products } = this.state;
this.setState({
products: [...products, product]
});
};
render() {
const { products } = this.state;
return (
<div>
<ul>
{products.map(product => (
<Product key={product.name.toString()} product={product} />
))}
</ul>
<button
onClick={e => this.addProduct({ name: "some product", price: 54 })}
>
Add Product
</button>
</div>
);
}
}
export default ProductList;
As a general rule of thumb, state should hold the data model for your view. The html we generate, like <Product />
, should be a result of state and props, and should live in the render function.
Upvotes: 0