Setting up Shopify Hydrogen (React Storefront)
Hydrogen is the official React framework from Shopify for headless commerce. Built on Remix, optimized for Storefront API, deploys to Oxygen (Shopify CDN hosting) or any Node.js-compatible hosting.
Why Hydrogen, not just Next.js
Next.js + Storefront API is a working option. Hydrogen adds:
- Oxygen deployment — free global edge hosting built into Shopify (from Basic plan). No separate deployment infrastructure.
-
Remix routing — nested routes, server-side data loading via
loader, optimistic UI viaaction -
Shopify-specific primitives —
ShopifyProvider, hooks for cart, analytics, cache strategies - Cache API — cache management at Cloudflare Workers level
- Streaming SSR — Progressive HTML rendering out of the box
Project initialization
npm create @shopify/hydrogen@latest
# Choice: Demo Store / Hello World
# Deploy: Oxygen / Self-hosted
cd my-hydrogen-app
npm install
npm run dev
Environment variables (.env):
SHOPIFY_STORE_DOMAIN=my-store.myshopify.com
SHOPIFY_STOREFRONT_ACCESS_TOKEN=abc123...
SHOPIFY_PUBLIC_STORE_DOMAIN=my-store.myshopify.com
SESSION_SECRET=random-secret-string
Project structure
hydrogen-app/
├── app/
│ ├── components/ # UI components
│ ├── lib/ # utilities, Shopify client
│ ├── routes/ # Remix file routing
│ │ ├── _index.tsx # home page
│ │ ├── products.$handle.tsx # product page
│ │ ├── collections.$handle.tsx
│ │ └── cart.tsx
│ ├── styles/ # global CSS
│ └── root.tsx # root layout
├── public/
├── server.ts # Oxygen/Node entry point
└── vite.config.ts
Product route with loader
// app/routes/products.$handle.tsx
import { json, type LoaderFunctionArgs } from '@shopify/remix-oxygen';
import { useLoaderData, type MetaFunction } from '@remix-run/react';
import { getSelectedProductOptions, Analytics } from '@shopify/hydrogen';
import { AddToCartButton } from '~/components/AddToCartButton';
const PRODUCT_QUERY = `#graphql
query Product($handle: String!, $country: CountryCode, $language: LanguageCode)
@inContext(country: $country, language: $language) {
product(handle: $handle) {
id
title
handle
descriptionHtml
options {
name
values
}
selectedVariant: variantBySelectedOptions(
selectedOptions: $selectedOptions
ignoreUnknownOptions: true
caseInsensitiveMatch: true
) {
id
availableForSale
image {
url
altText
width
height
}
price {
amount
currencyCode
}
compareAtPrice {
amount
currencyCode
}
sku
title
unitPrice {
amount
currencyCode
}
}
media(first: 7) {
nodes {
... on MediaImage {
mediaContentType
image {
id
url
altText
width
height
}
}
}
}
}
}
`;
export async function loader({ params, context }: LoaderFunctionArgs) {
const { handle } = params;
const { storefront } = context;
const { product } = await storefront.query(PRODUCT_QUERY, {
variables: {
handle,
selectedOptions: getSelectedProductOptions(request),
},
});
if (!product?.id) {
throw new Response(null, { status: 404 });
}
return json({
product,
url: request.url,
});
}
export const meta: MetaFunction<typeof loader> = ({ data }) => {
return [
{ title: `${data?.product?.title ?? 'Product'}` },
{ description: data?.product?.descriptionHtml ?? '' },
];
};
export default function Product() {
const { product } = useLoaderData<typeof loader>();
return (
<div className="product">
<h1>{product.title}</h1>
<img
src={product.selectedVariant?.image?.url}
alt={product.selectedVariant?.image?.altText}
/>
<p>{product.descriptionHtml}</p>
<AddToCartButton variant={product.selectedVariant} />
</div>
);
}
Integration with Oxygen
Deployment to Oxygen (free CDN):
npm run build
npm run deploy # via Oxygen CLI
Oxygen handles caching, edge rendering, and global distribution automatically.







