Kelson Batista
Kelson Batista

Reputation: 492

TypeError: Cannot read properties of undefined (reading 'map') - State returns undefined

I am trying to retrieve a array inside a state, then go for a map, but returns undefined. It seems the state is not ready but have no idea why.

I tried a lot of alternatives but even the length I cannot get, it also returns undefined. Only the data in function getProductDetails I can get length, no other place. So this is why I think is something related to the state readiness, but no idea how to solve.

Any help would be appreciated.

import React, { Component } from 'react';
import propTypes from 'prop-types';
import { Link } from 'react-router-dom';
import cartIcon from '../images/cartIcon.jpg';
import backIcon from '../images/backIcon.jpg';
import '../styles/ProductDetails.css';

export default class ProductDetails extends Component {
  constructor() {
    super();
    this.state = {
      product: {},
    };
  }

  componentDidMount() {
    this.getDetails();
  }

  async getProductDetails(item) {
    const url = `https://api.mercadolibre.com/items/${item}`;
    const response = await fetch(url);
    const data = await response.json();
    return data;
  }

  async getDetails() {
    const { match: { params: { id } } } = this.props;
    const product = await this.getProductDetails(id);
    this.setState({
      product,
    });
  }

  render() {
    const { product } = this.state;

    return (
      <section className="details">
        <div className="details__header">
          <div className="details__back">
            <Link
              to="/"
              className="back__btn"
              data-testid="shopping-back-button"
            >
              <img
                id="back-button"
                name="back-button"
                alt="Voltar"
                src={ backIcon }
                className="back__img"
              />
            </Link>
          </div>
          <div className="details__cart">
            <Link
              to="/carrinho-de-compras"
              className="cart__btn"
              data-testid="shopping-cart-button"
            >
              <img
                id="cart-button"
                name="cart-button"
                alt="Carrinho de Compras"
                src={ cartIcon }
                className="cart__img"
              />
            </Link>
          </div>
        </div>
        <div className="details__product">
          <div className="details__left">
            <p data-testid="product-detail-name">{ product.title }</p>
            <img src={ product.thumbnail } alt={ product.title } />
            <p>{ product.id }</p>
            <p>{ product.price }</p>
          </div>
          <div className="details__right">
            <ul>
              {(product.attributes)
                .map(({ name, value_name: valueName }, index) => (
                  <li key={ index }>
                    {name}
                    :
                    {valueName}
                  </li>))}
            </ul>
          </div>
        </div>
      </section>
    );
  }
}

ProductDetails.propTypes = {
  match: propTypes.shape({
    params: propTypes.shape({
      id: propTypes.string,
    }),
  }).isRequired,
};

Upvotes: 1

Views: 1128

Answers (2)

Drew Reese
Drew Reese

Reputation: 202618

The initial this.state.product value is an empty object {}, so this.state.product.attributes is undefined on the initial render and not mappable.

this.state = {
  product: {},
};

product.title, product.id, and product.price are OFC also undefined on the initial render but since you are not accessing more deeply these are rendered to the DOM as just undefined values and no error is thrown.

You have a few options to guard against the possibly null/undefined access:

  1. Use null-check/guard-clause on this.state.product.attributes

    <ul>
      {product.attributes 
        && product.attributes.map(({ name, value_name: valueName }, index) => (
          <li key={index}>
            {name}: {valueName}
          </li>
      ))}
    </ul>
    
  2. Use optional chaining operator on this.state.product.attributes

    <ul>
      {product.attributes?.map(({ name, value_name: valueName }, index) => (
        <li key={index}>
          {name}: {valueName}
        </li>
      ))}
    </ul>
    

Upvotes: 1

Cuong Vu
Cuong Vu

Reputation: 3723

That's because your this.state.product will only be available after you call this.getDetails() to fetch and set data in componentDidMount.

So the first time your component renders (aka mount), this.state.product will not be available.

To solve that issue, one common approach would be to check this.state.product in render() method like this.

  render() {
    const { product } = this.state;

    // if data is NOT available
    if (!product) {
      // either return null, or render a loading indicator, or whatever you want, while waiting for `this.state.product` to be available
      return null
    }
    
    // if data is available
    return (
      <section className="details">
        // render your component here
      </section>
    );
  }

Upvotes: 0

Related Questions