How to map API with multiple objects [Spree API V2 & ReactJS]
Asked Answered
G

1

7

I'm building a webshop, using ReactJS for the front-end and Spree (Ruby) for the back-end.

Spree offers an API solution to connect the front-end and the back-end with one and other.

I'm trying to display products with product images, but Spree's API is setup in a specific way that product images and products aren't in the same object.

The API response is:

 {
    (holds products)data: [],
    (Holds product images)included:[],
 }

My goal is to create an ul with the product information and product image displayed.

I've tried to map my API link which

           this.state.arrays.map((product) => 
              product.data
            )

Which responds with the data object, but I cant for example do product.data.name because it returns an undefined response

DATA RESPONSE IN THE LOG

ProductsList.js:28 PL 
[undefined]
Index.js:42 productsData 
{}
ProductsList.js:28 PL 
[Array(5)]
0: Array(5)
0: {id: "5", type: "image", attributes: {…}}
1: {id: "4", type: "image", attributes: {…}}
2: {id: "1", type: "image", attributes: {…}}
3: {id: "3", type: "image", attributes: {…}}
4: {id: "2", type: "image", attributes: {…}}
length: 5
__proto__: Array(0)
length: 1
__proto__: Array(0)

Product Index page

import React, { Component } from 'react';
import ReactDOM from 'react-dom';
import PropTypes from "prop-types";
import ProductsList from "./products/ProductsList";
import axios from 'axios';



const REACT_VERSION = React.version;
const include = '?include=images';
const API = 'https://stern-telecom-react-salman15.c9users.io/api/v2/storefront/products' + include;

const styles = {
  card: {
    maxWidth: 345,
  },
  media: {
    height: 140,
  },
};

class Index extends React.Component {
    constructor(props){
        super(props);
        this.state = {
            products: [],
            productsData: {},
            isLoading: false,
            error: null,
    };
  }
  componentDidMount() {
    this.setState({ isLoading: true });
    axios.get(API)
      .then(result => this.setState({
        products: result.data.data,
        productsData: result.data,
        isLoading: false,
      }))
      .catch(error => this.setState({
        error,
        isLoading: false
      }));
      // console.log(
      //   'productsData', 
      //   this.state.productsData

      //   )
  }
  render() {
    const { products, productsData,isLoading, error } = this.state;

    if (error) {
      return <p>{error.message}</p>;
    }
     if (isLoading) {
      return <p>Loading ...</p>;
    }
    return (
      <React.Fragment>
          <h1>React version: {REACT_VERSION}</h1>
          <ProductsList products={this.state.productsData}/>
      </React.Fragment>
    );
  }
}

ProductsList.propTypes = {
  greeting: PropTypes.string
};

export default Index

ProductList Page

import React from "react"
import PropTypes from "prop-types"

import { withStyles } from '@material-ui/core/styles';
import Card from '@material-ui/core/Card';
import CardActionArea from '@material-ui/core/CardActionArea';
import CardActions from '@material-ui/core/CardActions';
import CardContent from '@material-ui/core/CardContent';
import CardMedia from '@material-ui/core/CardMedia';
import Button from '@material-ui/core/Button';
import Typography from '@material-ui/core/Typography';

const url = "https://stern-telecom-react-salman15.c9users.io"

class ProductsList extends React.Component {
  constructor(props) {
    super(props);
    const { products } = this.props;
    const arrays = Object.values( {products} );
    this.state = {
      products,
      arrays
    };
  }
  render () {
    return (
      <React.Fragment>
        <ul>
          <p>Shop Products</p>
          {
          // console.log(
          //   'PL',
            // this.state.arrays.map((product) => 
            //   product.data
            // )
          // )
          this.state.arrays.map(product =>
            <li key={product.objectID}>
            <Card>
                  <CardActionArea>
                    <CardMedia
                      image= {url + ''}
                      title={product.data.attributes.name}
                    />
                    <CardContent>
                      <Typography gutterBottom variant="h5" component="h2">
                       {product.data.attributes.name}
                      </Typography>
                      <Typography component="p">
                        {product.data.attributes.description}
                      </Typography>
                    </CardContent>
                  </CardActionArea>
                  <CardActions>
                    <Button size="small" color="primary">
                     {product.data.attributes.display_price} 
                    </Button>
                    <Button size="small" color="primary">
                      add to cart
                    </Button>
                  </CardActions>
                </Card>
            </li>
            )
          }
        </ul>
      </React.Fragment>
    );
  }
}

ProductsList.propTypes = {
  greeting: PropTypes.string
};
export default ProductsList

What I expect to get as Result is product information and image

Gentille answered 29/3, 2019 at 15:37 Comment(4)
what is this.state.arraysSuperabundant
Hi, @Superabundant I added it to my code. The AP Object get's passed on the the productList page and turned into a variable called arrays. Which I turn in a state componentGentille
Can you post a sample of the json data you receive from the APi? It is very hard to see what problem you are facing without having that infoMatusow
@Matusow I've added the data I receiveGentille
C
4

The manipulation after you're fetching json data is wrong. The returned result is a json object with a data property which is the array you want to pass and get the products.

You either pass the products to <ProductsList> component:

const { products, images, isLoading, error } = this.state;
...
<ProductsList products={products} images={images}/>

and then use it directly:

class ProductsList extends React.Component {
  constructor(props) {
    super(props);
    const { products, images } = this.props;
    this.state = {
      products,
      images
    };
    ...
  }
  ...
}

or use props.products.data to directly get the products array inside ProductsList constructor:

class ProductsList extends React.Component {
  constructor(props) {
    super(props);
    const products = this.props.products.data;
    const images = this.props.products.included;
    ...
  }
  ...
}

there is no need of using const arrays = Object.values({ products }); because you already have an array with the products:

...
products: result.data.data,   // products is an array with products
images: result.data.included, // images is an array with all posible images
productsData: result.data,    // productsData.data is an array with products
...

Also, the product object does not contain any attribute named data:

<Typography gutterBottom variant="h5" component="h2">
  {product.data.attributes.name}
</Typography>
<Typography component="p">
  {product.data.attributes.description}
</Typography>

you have to access its' properties directly like this:

<Typography gutterBottom variant="h5" component="h2">
  {product.attributes.name}
</Typography>
<Typography component="p">
  {product.attributes.description}
</Typography>

EDIT

Here is a CodeSandbox project with your code simpified and without calling the Axios request (because it's restrticted) and having the data in a JSON file instead. You should also initialize isLoading to true, or make the Index component to not render before it has some data:

class Index extends React.Component {
  constructor(props){
    super(props);
    this.state = {
      ...
      isLoading: true,
    }
  }
}

Here is an updated screenshot with it working:

CodeSandbox Screenshot

And the simplified <ProductsList/> component:

import React from "react";

const url = "https://stern-telecom-react-salman15.c9users.io";

class ProductsList extends React.Component {
  constructor(props) {
    super(props);

    const { products, images } = this.props;
    //const arrays = Object.values( {products} );
    this.state = {
      products,
      images
      //arrays
    };
  }
  render() {
    const { products, images } = this.state;
    return (
      <React.Fragment>
        <p>Shop Products</p>
        {console.log("PL", products, images)
        // this.state.arrays.map(product =>
        //   <li key={product.objectID}>

        //   </li>
        //   )
        }
        <ul>
          {products.map(product => (
            <li key={product.key}>
              <h4>{product.attributes.name}</h4>
              <p>Description: {product.attributes.description}</p>
              <p>Price: {product.attributes.display_price} </p>
              <p>Images:</p>
              <div>
                {product.relationships.images.data.map(({ id }) => {
                  let image = images.find(image => image.id == id);
                  return image ? (
                    <img src={`${url}/${image.attributes.styles[1].url}`}/>
                  ) : null;
                })}
              </div>
            </li>
          ))}
        </ul>
      </React.Fragment>
    );
  }
}

export default ProductsList;

EDIT 2

To add images it's a very simple task. You just have to combine products array with images and display the images. Check the updated <ProductsList/> component. Of course you have to pass both products and images to <ProductsList/> (const images = productsData.included;). Check the updated CodeSandbox, <ProductsList/> component and screenshot.

EDIT 3

Regarding the images; each image has a styles property which is an array of different sizes:

"included": [
{
  "id": "5",
  "type": "image",
  "attributes": {
    "viewable_type": "Spree::Variant",
    "viewable_id": 4,
    "styles": [
      {
        "url": "...",
        "width": "48",
        "height": "48"
      },
      {
        "url": "...",
        "width": "100",
        "height": "100"
      },
      {
        "url": "...",
        "width": "240",
        "height": "240"
      },
      {
        "url": "...",
        "width": "600",
        "height": "600"
      }
    ]
  }
}
...
]

in order to map the images to each product, we have to map all the images stored in each product by using product.relationships.images.data which is an array of object with id and type properties. For each image in the product images, we search through the images array using let image = images.find(image => image.id == id) and if we find an image then we use one of the four available sizes or maybe all of the available sizes (48px, 100px, 240px, 600px); I choose image.attributes.styles[1].url, so I display the second element of the available image sizes, which is the 100px size image:

product.relationships.images.data.map(({ id }) => {
  let image = images.find(image => image.id == id);
  return image ? (
    <img src={`${url}/${image.attributes.styles[1].url}`}/>
  ) : null;
})

EDIT 4

If you need to get one image per product, then you can use a function that checks whether an image exists and then gets the image from the images array:

// Here we're using an inline function to get the product image
// You can also create a normal class function and use that instead

{product.relationships.images.data.length > 0 &&
  (() => {
    // { id } is the destructure of product.relationships.images.data[0]
    // which means it extract the property id to a stand alone variable
    const { id } = product.relationships.images.data[0];
    const image = images.find(image => image.id == id);
    return image ? (
      <img src={`${url}/${image.attributes.styles[1].url}`} />
    ) : null;
  })()
}

This is an inline function that isolates its' contents and executes immediately:

(() => { ... })()

You can read more about Destructuring assignment ({ id } = object).

Centrepiece answered 1/4, 2019 at 15:15 Comment(13)
Thanks for your reply Christos, I was testing productsData an an object { } and products as an array [ ] Passing on productsData as an object to the products list en changing it to an array seemed to make the data readable when logging product.data it does show a data feed of multiple array's. But when I try to map an array the response is undefinedGentille
@Gentille if you read carefully my answer from the beggining to the end, you'll see that you don't do it right. You don't have to convert anything using Object.values; the JSON data already have an array with the products (data.data). The data you're getting to log in ProductsList.js:28 PL [Array(5)] are actually the images included member not the data: [...] which has the products.Centrepiece
Thanks for the edit, the only problem is that I don't only need the data array, but also the images from included. To create a product list including images. Right now the array list doesn't have images so I have to combine both data from data and includeGentille
@Gentille it's really simple and a matter of filtering. Check my edits and code to see how you'll do it.Centrepiece
Hi Chris, your last edit worked perfectly, Thank you! Could you explain a bit more what you did regarding finding the image?Gentille
Also another question, It now returns all images, what If I only want to return a single imageGentille
@Gentille check my edit to see the images explanation. What do you mean you want one image? If you want to display just one image (just the fiorst for example), then you don't use and enumarate all the items of product.relationships.images.data but just get the first one if it actually has >= 1 images. Can edit and write an example if you like.Centrepiece
Chris, amazing. Wish I could give you 50 more points. What I meant to say is, what If I only want to show the first image of each product with size 600px would I then do let image = images.find(image => image.id == id >= 1) ; and show product <img src={${url}/${image.attributes.styles[3].url}} /> Gentille
@Gentille thank you. You don't really have to do image.id == id >= 1; image.id == id just checks if there is the same id and it's a boolean comparison, just leave it as is .find(image => image.id == id). That is exactly how you'd display the 600px image, image.attributes.styles[3].url selects the fourth styles element (styles[3]) which is the 600x600 image.Centrepiece
Sure, check the updated code in CodeSandbox example. The links of the API and the images do not work anymore because I assume you've changed something to the endpoint you have had, but it doesn'ty matter, you'll see how you can use only one image for each product.Centrepiece
Yes, I my workspace was being archived, so the development server was offline. However it indeed works. I see you're not mapping the array anymore, but you're actually (() have an empty function. Could you care to explain how that works? You're also using { id } to get it from the product.relationships.images.data[0] also new for me.Gentille
Check my new edit, you'll see the explanation of both destructuring assignment and inline function.Centrepiece
Thanks Chris, not only did this solve my problem, but I've also learned more about destructing of objects and filtering through multiple arraysGentille

© 2022 - 2024 — McMap. All rights reserved.