In Magento, we can get related, upsell, and cross-sell products on the product detail page using admin settings. But in PWA Studio, that feature is not available right now. Magento provides GraphQL to get linked products for the current product. So in PWA, we can create a custom component to fetch related, upsell, and cross-sell products of current products.
In this blog, we get an idea of how to fetch linked products data of the current product using GraphQL and display it on the product detail page. For that we have created a small example to display related, upsell and cross-sell products on the product detail page as follows:
Create a GraphQL file
File Path: src/components/LinkedProducts/linkedProducts.gql.js
import { gql } from '@apollo/client';
export const GET_LINKED_PRODUCTS = gql`
query getRelatedProducts($sku: String!) {
products(filter: { sku: { eq: $sku } }) {
items {
id
uid
name
related_products {
id
uid
name
small_image {
label
url
}
url_key
url_suffix
price_range {
minimum_price {
regular_price {
currency
value
}
}
}
}
upsell_products {
id
uid
name
small_image {
label
url
}
url_key
url_suffix
price_range {
minimum_price {
regular_price {
currency
value
}
}
}
}
crosssell_products {
id
uid
name
small_image {
label
url
}
url_key
url_suffix
price_range {
minimum_price {
regular_price {
currency
value
}
}
}
}
}
}
}
`;
Create a component file
File Path: src/components/LinkedProducts/linkedProducts.js
import React from "react";
import { useQuery } from '@apollo/client';
import { FormattedMessage } from 'react-intl';
import { Link } from 'react-router-dom';
import { GET_LINKED_PRODUCTS } from "./linkedProducts.gql";
import { fullPageLoadingIndicator } from "@magento/venia-ui/lib/components/LoadingIndicator";
import resourceUrl from '@magento/peregrine/lib/util/makeUrl';
import ErrorView from "@magento/venia-ui/lib/components/ErrorView";
import Image from "@magento/venia-ui/lib/components/Image";
import Price from "@magento/venia-ui/lib/components/Price";
import {UNCONSTRAINED_SIZE_KEY} from "@magento/peregrine/lib/talons/Image/useImage";
const LinkedProducts = props => {
const { productSku } = productDetails.sku;
const { loading, error, data } = useQuery(GET_LINKED_PRODUCTS, {
variables: { sku: productSku },
fetchPolicy: 'cache-and-network',
nextFetchPolicy: 'cache-first'
});
if (!data) {
if (loading) {
return fullPageLoadingIndicator;
}
if (error) {
return <ErrorView message={error.message} />;
}
}
const product = data.products.items.length > 0 ? data.products.items[0] : null;
const relatedProductsData = product ? product.related_products : null;
const relatedProducts = relatedProductsData.length > 0 ? relatedProductsData : null;
const upsellProductsData = product ? product.upsell_products : null;
const upsellProducts = upsellProductsData.length > 0 ? upsellProductsData : null;
const crosssellProductsData = product ? product.crosssell_products : null;
const crosssellProducts = crosssellProductsData.length > 0 ? crosssellProductsData : null;
const relatedItems = relatedProducts ? relatedProducts.map((item) => {
const productLink = resourceUrl(`/${item.url_key}${item.url_suffix || ''}`);
return (
<div className={classes.root}>
<Link
to={productLink}
className={classes.images}
>
<Image
alt={item.small_image.label}
classes={{
image: classes.image,
root: classes.imageContainer
}}
height={IMAGE_HEIGHT}
resource={item.small_image.url}
widths={IMAGE_WIDTHS}
/>
</Link>
<Link
to={productLink}
className={classes.name}
>
<span>{item.name}</span>
</Link>
<div className={classes.price}>
<Price
value={item.price_range.minimum_price.regular_price.value}
currencyCode={item.price_range.minimum_price.regular_price.currency}
/>
</div>
</div>
);
}) : null;
const upsellItems = upsellProducts ? upsellProducts.map(... Do the same thing as relatedItems ...) : null;
const crosssellItems = crosssellProducts ? crosssellProducts.map(... Do the same thing as relatedItems ...) : null;
return (
<div className={classes.productsContainer}>
<h3 className={classes.heading}>
<FormattedMessage
id={'linkedProducts.related'}
defaultMessage={'Related Products'}
/>
</h3>
<div>{relatedItems}</div>
<h3 className={classes.heading}>
<FormattedMessage
id={'linkedProducts.upsell'}
defaultMessage={'Upsell Products'}
/>
</h3>
<div>{upsellItems}</div>
<h3 className={classes.heading}>
<FormattedMessage
id={'linkedProducts.crosssell'}
defaultMessage={'Crosssell Products'}
/>
</h3>
<div>{crosssellProducts}</div>
</div>
);
};
export default LinkedProducts;
Render a component
File Path: src/components/LinkedProducts/index.js
export {default} from './linkedProducts';
Add a custom component in Product Detail Page
File Path: src/targets/local-intercept.js
const { Targetables } = require('@magento/pwa-buildpack');
module.exports = targets => {
const targetables = Targetables.using(targets);
const ProductDetails = targetables.reactComponent(
'@magento/venia-ui/lib/components/ProductFullDetail/productFullDetail.js'
);
const LinkedProducts = ProductDetails.addImport(
"LinkedProducts from 'example-concept/src/components/LinkedProducts/linkedProducts'"
);
ProductDetails
.insertAfterJSX('<Form />', `<${LinkedProducts} />`)
.setJSXProps('LinkedProducts', {
'classes': '{classes}',
'productDetails': '{productDetails}',
'options': '{options}',
'mediaGalleryEntries': '{mediaGalleryEntries}'
});
};
Run PWA storefront
Execute the following command in the terminal to build and watch your example package:
yarn watch:example
Open the storefront URL and got to the product detail page. You can see the related, upsell, and cross-sell products like the following screenshots:



We hope this blog may understandable and useful to you. You can email us at mage2developer@gmail.com if we missed anything or want to add any suggestions. We will respond to you as soon as possible. Happy to help 🙂

