Hydrogen

Learn more about working with Hydrogen.js from Shopify, and why a Hydrogen Headless CMS with GraphQL could be the right option for your next project.

Hydrogen and Headless CMS

What is HydrogenAnchor

Hydrogen is a React-based JavaScript framework developed by Shopify. Described as a “Framework for Dynamic Commerce”, using Shopify’s Hydrogen gives you the ability to build and deliver fast, personalized shopping experiences.

Explore the official documentation or view the repo to get started with your next Hydrogen project.

To get started with a new Hydrogen app, simply run the CLI:

npx create-hydrogen-app

Why use HydrogenAnchor

Hydrogen allows for faster, personalized online shopping experiences by integrating React Server Components, smart caching, and streaming server-side rendering - unlocking fast first-renders and progressive hydration.

When paired with the globally distributed Shopify GraphQL Storefront API and commerce components, it’s a blazingly fast framework.

Hydrogen provides better builds with hot reloading, built-in ESM, and a dev environment powered by Typescript, Vite, and TailwindCSS.

View Hydrogen in action by exploring Shopify’s implementation

Headless CMS for Hydrogen AppsAnchor

Why use a Headless CMS for hydrogen projects? Often companies want to combine the capabilities of a Headless CMS or a Content API with the eCommerce functionality that Shopify offers.

When handling product inventories and global shopfronts with Shopify, your content needs to be served equally fast. Hygraph provides you with instant GraphQL Content APIs for GraphQL Queries and GraphQL Mutations, making it an incredibly performant combination with Shopify’s globally distributed APIs.

Working with Shopify and HygraphAnchor

Its easy to use Hygraph as a Shopify Headless CMS by integrating Shopify with UI Extensions.

UI Extensions allow you to extend the functionality of the Hygraph content editing experience by running custom applications within the CMS. UI Extensions can be used for a variety of use-cases, such as adding custom fields to let editors search and pick products from a Shopify catalog. Hygraph, GraphQL, and Shopify are a powerful combination for high-performance eCommerce platforms.

With your frontend powered by Hydrogen and Hygraph, make it easier for your content team by setting up a UI Extension within Hygraph to pick Shopify products.

Use our Shopify UI Extension example to create your own.

All you’d need are an index.tsx and a picker.tsx to create your own UI Extenion, and once you have it hosted, you can load your UI Extension into Hygraph.

Here’s our example when integrating Hygraph with Shopify:

Create an index.tsx

import * as React from "react";
import "@shopify/polaris/dist/styles.css";
import {
Wrapper as ExtensionWrapper,
useUiExtension,
ExtensionDeclaration,
FieldExtensionType,
FieldExtensionFeature,
} from "@hygraph/uix-react-sdk";
import enTranslations from "@shopify/polaris/locales/en.json";
import {
AppProvider,
TextField,
Button,
Spinner,
MediaCard,
Card,
Thumbnail,
DisplayText,
} from "@shopify/polaris";
const extensionDeclaration: ExtensionDeclaration = {
extensionType: "field",
name: "Shopify Product Picker",
fieldType: FieldExtensionType.STRING,
features: [FieldExtensionFeature.FieldRenderer],
config: {
STORE: {
type: "string",
displayName: "Store ID",
required: true,
},
ACCESS_TOKEN: {
type: "string",
displayName: "Access Token",
required: true,
},
},
};
export default function ShopifyExtension({ extensionUid }) {
console.log({ extensionUid });
if (typeof extensionUid !== "string") return <p> missing extension UID</p>;
return (
<ExtensionWrapper uid={extensionUid} declaration={extensionDeclaration}>
<ShopifyProductInput />
</ExtensionWrapper>
);
}
/// ok let's make an extension out of this
function ShopifyProductInput() {
const {
value,
onChange,
extension: {
config: { STORE, ACCESS_TOKEN },
},
} = useUiExtension();
React.useEffect(() => {
let listener;
const listenForItem = async function () {
const postRobot = (await import("post-robot")).default;
listener = postRobot.on("selectItem", (event) => {
const id = event.data.id;
setTimeout(() => onChange(id), 100);
return true;
});
};
listenForItem();
return () => {
listener.cancel();
};
}, []);
const openPicker = React.useCallback(() => {
const windowFeatures =
"menubar=yes,resizable=yes,scrollbars=yes,status=yes,width=320,height=640";
const pickerWindow = window.open(
"shopify/picker",
"Shopify_Picker",
windowFeatures
);
let configInterval;
const sendConfig = async () => {
const postRobot = (await import("post-robot")).default;
postRobot
.send(pickerWindow, "config", { STORE, ACCESS_TOKEN })
.then(function (result) {
console.log({ result });
if (result) clearInterval(configInterval);
});
};
configInterval = setInterval(sendConfig, 200);
}, []);
return (
<AppProvider i18n={enTranslations}>
<div>
<TextField
value={value}
label={undefined}
onChange={onChange}
connectedRight={<Button onClick={openPicker}>open picker</Button>}
/>
<ProductPreview
productId={value}
store={STORE}
accessToken={ACCESS_TOKEN}
/>
</div>
</AppProvider>
);
}
function useThrottledfunction(callback, delay) {
const savedCallback = React.useRef();
const savedArgs = React.useRef([]);
const timeoutId = React.useRef(null);
React.useEffect(() => (savedCallback.current = callback), [callback]);
const dummyFunction = React.useCallback(
(...args) => {
if (timeoutId.current) {
clearTimeout(timeoutId.current);
}
timeoutId.current = setTimeout(() => {
//@ts-ignore
savedCallback.current(...savedArgs.current);
}, delay);
savedArgs.current = args;
},
[callback]
);
return dummyFunction;
}
function ProductPreview({
productId,
store,
accessToken,
}: {
productId: string;
store: string;
accessToken: string;
}) {
const [state, setState] = React.useState({
loading: false,
error: null,
data: null,
});
const fetchProduct = React.useCallback(
(productId) => {
if (typeof window !== "undefined") {
setState({ loading: true, error: null, data: null });
window
.fetch(
`/api/extensions/shopify/product/${encodeURIComponent(productId)}`,
{
method: "GET",
headers: {
"X-Shopify-Store": store,
"X-Shopify-Access-Token": accessToken,
},
}
)
.then((res) => {
if (res.ok) {
res.json().then((data) => {
if (data.data.product)
setState({
loading: false,
error: null,
data: data?.data?.product,
});
else
setState({
loading: false,
error: new Error("Product not found"),
data: null,
});
});
} else {
setState({
loading: false,
error: new Error(res.statusText),
data: null,
});
}
})
.catch((error) => setState({ loading: false, error, data: null }));
}
},
[store, accessToken]
);
const throttledFetchProduct = useThrottledfunction(fetchProduct, 100);
React.useEffect(() => {
if (productId.startsWith("gid://shopify/Product/"))
throttledFetchProduct(productId);
else
setState({
loading: false,
error: null,
data: null,
});
}, [productId]);
return state.loading ? (
<Card>
<Spinner />
</Card>
) : (
<React.Fragment>
{state.data && (
<div style={{ padding: "12px" }}>
<Card>
<div style={{ display: "flex" }}>
<Thumbnail
source={state.data.featuredImage?.transformedSrc}
alt="product image"
size="medium"
/>
<DisplayText size="small">{state.data.title}</DisplayText>
</div>
</Card>
</div>
)}
{state.error && <p style={{ color: "red" }}>{state.error.message}</p>}
</React.Fragment>
);
}

And configure your picker.tsx

import * as React from "react";
import "@shopify/polaris/dist/styles.css";
import enTranslations from "@shopify/polaris/locales/en.json";
import {
AppProvider,
Page,
ResourceList,
ResourceItem,
Thumbnail,
Spinner,
Badge,
} from "@shopify/polaris";
export default function ShopifyPicker({ extensionUid }) {
const [config, setConfig] = React.useState({
STORE: null,
ACCESS_TOKEN: null,
});
React.useEffect(() => {
let listener;
const listenForItem = async function () {
const postRobot = (await import("post-robot")).default;
listener = postRobot.on("config", (event) => {
const { STORE, ACCESS_TOKEN } = event.data;
setTimeout(() => setConfig({ STORE, ACCESS_TOKEN }), 100);
return true;
});
};
listenForItem();
return () => {
listener.cancel();
};
}, []);
if (typeof config.STORE !== "string") return <p></p>;
return (
<ProductPicker store={config.STORE} accessToken={config.ACCESS_TOKEN} />
);
}
function ProductPicker({ store, accessToken }) {
const { data, loading, error } = useFetchProducts({
store,
accessToken,
});
return (
<AppProvider i18n={enTranslations}>
<Page title="Product picker">
{loading && (
<React.Fragment>
<Spinner />
</React.Fragment>
)}
{data && (
<ResourceList
items={data}
renderItem={(item: any) => {
const { id, title, status, featuredImage } = item;
return (
<ResourceItem
id={id}
onClick={async () => {
const PostRobot = (await import("post-robot")).default;
PostRobot.send(window.opener, "selectItem", { id }).then(
function () {
window.close();
}
);
}}
media={
<Thumbnail
source={featuredImage?.transformedSrc}
alt="avatar"
size="small"
/>
}
>
{title}{" "}
<Badge
status={
status === "ACTIVE"
? "success"
: status === "DRAFT"
? "warning"
: undefined
}
>
{status}
</Badge>
</ResourceItem>
);
}}
/>
)}
</Page>
</AppProvider>
);
}
function useFetchProducts({ store, accessToken }) {
const [state, setState] = React.useState({
loading: true,
error: null,
data: null,
});
React.useEffect(() => {
if (typeof window !== "undefined") {
window
.fetch("/api/extensions/shopify/products", {
method: "GET",
headers: {
"X-Shopify-Store": store,
"X-Shopify-Access-Token": accessToken,
},
})
.then((res) => {
if (res.ok) {
return res.json();
} else {
throw new Error(
`Failed fetching products: ${res.status} ${res.body}`
);
}
})
.then((data) => {
setState({
loading: false,
error: null,
data: data.data.products.edges.map((e) => e.node),
});
})
.catch((error) => setState({ loading: false, error, data: null }));
}
}, []);
return state;
}

That’s it! You’re set for your content team to work with Hygraph and Shopify.