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 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.
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.
// 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.
// 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.
// 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.
// 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.
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.