BeniaminoBaggins
BeniaminoBaggins

Reputation: 12433

React JS state that references other state property

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

Answers (2)

Mukesh Soni
Mukesh Soni

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

Jeremy Yip
Jeremy Yip

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

Related Questions