Blog Ecommerce 8 min read

Headless Shopify Hydrogen Image Fallbacks with React

Add image fallbacks to Shopify Hydrogen storefronts when product featuredImage is null, using onError handlers and generated placeholder URLs in React components.

Shopify Hydrogen imageHeadless ShopifyHydrogen ReactProduct image fallbackStorefront API
Headless Shopify Hydrogen Image Fallbacks with React

Shopify Hydrogen uses the Storefront API's GraphQL schema, where `Product.featuredImage` is a nullable `Image` type. Products created without images — draft products, imported SKUs, or products awaiting photography — return `featuredImage: null`. A Hydrogen component that accesses `product.featuredImage.url` without a null guard crashes with a TypeError at runtime.

The fix is a utility function and a React component that both check for null and substitute a generated fallback URL. The `@shopify/hydrogen` package's `Image` component accepts a fallback src, but you still need to handle the null case before the component receives data.

Storefront API types

The featuredImage nullable type in Storefront GraphQL

Shopify's Storefront API returns product data with `featuredImage: Image | null`. The `Image` type has `url`, `width`, `height`, and `altText` properties. When `featuredImage` is null, accessing any property on it throws a TypeError. TypeScript with strict null checks will flag this at compile time, but JavaScript projects or incorrectly typed GraphQL queries can miss it.

The `images` connection on Product returns all uploaded images. An empty `edges` array here confirms no images exist. Some Hydrogen codebases use `images.edges[0]?.node.url` as an alternative to `featuredImage.url` — both require null safety.

Product variants also have an image field (`ProductVariant.image`). A variant may have its own image or inherit the product's featured image. Null check variant images independently of the product-level fallback.

GraphQL query

Request image dimensions in your fragment

Request `width` and `height` alongside `url` in your GraphQL fragment. These values let you pass matching dimensions to the fallback URL, ensuring no layout shift occurs when the fallback renders instead of the real image.

Define a reusable fragment for image fields so dimensions are always included. A missing width or height in the query forces you to hardcode fallback dimensions, which can diverge from the actual image slot.

Implementation text
fragment ProductImageFields on Image {
  url
  altText
  width
  height
}

fragment ProductCard on Product {
  id
  title
  handle
  featuredImage {
    ...ProductImageFields
  }
  priceRange {
    minVariantPrice { amount currencyCode }
  }
}

Utility function

Build a null-safe image URL helper

Write a utility that accepts a nullable Storefront API image and the expected dimensions, then returns either the real URL or a fallback URL. Keep the fallback URL deterministic so it is stable for caching and predictable for tests.

Implementation tsx
// app/lib/product-image.ts
import type { Image } from '@shopify/hydrogen/storefront-api-types';

export function productImageUrl(
  image: Image | null | undefined,
  width: number,
  height: number,
  productTitle = '',
): string {
  if (image?.url) return image.url;
  const label = productTitle
    ? encodeURIComponent(productTitle)
    : `${width}×${height}`;
  return `https://fallback.pics/api/v1/${width}x${height}?text=${label}`;
}

React component

ProductImage component with onError handling

Use the utility function for the initial src and an onError handler for network failures after initial render. An onError on a Hydrogen Image component ensures that a product whose image URL returns a 404 (deleted assets, CDN issues) also gets a fallback.

Set onError = null inside the handler to prevent infinite reload loops. If the fallback URL also returns an error, the browser should not keep retrying indefinitely.

Implementation tsx
// app/components/ProductImage.tsx
import { Image } from '@shopify/hydrogen';
import { productImageUrl } from '~/lib/product-image';
import type { Image as ImageType } from '@shopify/hydrogen/storefront-api-types';

interface ProductImageProps {
  image: ImageType | null | undefined;
  title: string;
  width?: number;
  height?: number;
  className?: string;
}

export function ProductImage({
  image,
  title,
  width = 400,
  height = 400,
  className,
}: ProductImageProps) {
  const src = productImageUrl(image, width, height, title);
  const fallbackSrc = `https://fallback.pics/api/v1/${width}x${height}?text=${encodeURIComponent(title)}`;

  return (
    <Image
      src={src}
      alt={image?.altText ?? title}
      width={width}
      height={height}
      className={className}
      onError={(e) => {
        const target = e.currentTarget;
        target.src = fallbackSrc;
        target.onerror = null;
      }}
    />
  );
}

Variant images

Handle variant-level image fallbacks separately

Product pages in Hydrogen switch the displayed image when the user selects a different variant. The variant image (`selectedVariant.image`) may be null even when the product has a featured image. Your image component should accept either the variant image or the product featured image, with the generated fallback as the last resort.

Implementation tsx
// In a product page component
const displayImage =
  selectedVariant?.image ??
  product.featuredImage;

<ProductImage
  image={displayImage}
  title={product.title}
  width={800}
  height={800}
/>

Cart

Line item thumbnails in the Hydrogen cart

Hydrogen's cart template renders line item thumbnails. Each line item includes a `merchandise.image` field (nullable). Apply the same utility function to cart thumbnails. Cart thumbnails are smaller — typically 80×80 or 100×100 — so use the square route for simplicity.

Implementation tsx
// app/components/CartLineItem.tsx
function lineItemImageUrl(line: CartLine): string {
  const image = line.merchandise?.image;
  const title = line.merchandise?.product?.title ?? 'Item';
  if (image?.url) return image.url;
  return `https://fallback.pics/api/v1/square/80?text=${encodeURIComponent(title)}`;
}

Resources

Further reading

For React-specific image fallback patterns, the React image fallback patterns guide covers onerror lifecycle, useRef approaches, and the infinite loop prevention pattern in detail.

Implementation text
https://fallback.pics/docs/
https://fallback.pics/placeholder-image-api/
https://fallback.pics/blog/react-image-fallback-patterns/
https://fallback.pics/blog/nextjs-image-fallbacks-without-layout-shift/

Key takeaways

What to standardize before shipping

  • Product.featuredImage and ProductVariant.image are nullable in the Storefront GraphQL schema — always null-check before accessing .url.
  • Request width and height in your image fragment so fallback URLs can be dimension-matched without hardcoding.
  • Write a productImageUrl utility that returns a fallback.pics URL when the image is null or undefined.
  • Add an onError handler to the rendered Image component for network failures after initial render, and set onerror = null to prevent infinite loops.
  • Apply the same utility to cart line item thumbnails — cart is a high-trust surface where broken images affect conversion.

Production fallback layer

Use fallback.pics anywhere an image URL is accepted.

Start with one deterministic URL and standardize fallback behavior across your design system.

Read the docs