@pack/hydrogen: Hydrogen Integration SDK
The @pack/hydrogen package provides clients and utilities to get Pack running on your Hydrogen storefront.
Requirements
@pack/hydrogen 3.x and 4.x target Hydrogen on React Router 7. Coming from a Remix-based storefront? Follow the React Router 7 migration guide first. Upgrading from 3.x? See Upgrading to @pack/hydrogen 4.0.
| Dependency | Version |
|---|---|
@shopify/hydrogen | ^2026.1.0 |
react-router | ^7.12.0 |
react | ^18.3.0 |
| Node.js | >=20.0.0 |
@shopify/remix-oxygen and @remix-run/* packages are no longer peer dependencies.
Quick Start
Install the client with a package manager:
npm install @pack/hydrogen
Adding Pack TS types
In the root of your project, import the Pack type in your env.d.ts file and add it to the React Router AppLoadContext.
env.d.ts
/// <reference types="vite/client" />
/// <reference types="react-router" />
/// <reference types="@shopify/oxygen-workers-types" />
/// <reference types="@shopify/hydrogen/react-router-types" />
import type {Storefront, HydrogenCart} from '@shopify/hydrogen'
import type {Pack} from '@pack/hydrogen'
declare module 'react-router' {
export interface AppLoadContext {
storefront: Storefront
cart: HydrogenCart
env: Env
pack: Pack
}
}
Setting Pack context in server.ts
Import these into your server.ts file.
server.ts
// Virtual entry point for the app
import * as serverBuild from 'virtual:react-router/server-build'
import {
createRequestHandler,
getStorefrontHeaders,
} from '@shopify/hydrogen/oxygen'
import {
createPackClient,
PackSession,
PackTestSession,
handleRequest,
} from '@pack/hydrogen'
Initialize a PackSession and a PackTestSession alongside your cache and Hydrogen session. PackSession is how the Pack client knows whether to pull published or draft (preview) content. PackTestSession stores the visitor's A/B test assignment.
server.ts
const [cache, session, packSession, packTestSession] = await Promise.all([
caches.open('hydrogen'),
AppSession.init(request, [env.SESSION_SECRET]),
PackSession.init(request, [env.SESSION_SECRET]),
PackTestSession.init(request, [env.SESSION_SECRET]),
])
PackSession.init takes an optional third argument for visitor consent: getConsent reads the answer from another consent tool's cookie, and unknownConsent ('granted' by default, 'denied', or a function of the request) sets how visitors who haven't answered are treated. See Consent Management.
Create a Pack client and wrap the React Router request handler with Pack's handleRequest. The client is passed into your loader context so you can query Pack content from loaders.
server.ts
const pack = createPackClient({
cache,
waitUntil,
token: env.PACK_SECRET_TOKEN,
storeId: env.PACK_STOREFRONT_ID,
session: packSession,
testSession: packTestSession,
contentEnvironment: env.PUBLIC_PACK_CONTENT_ENVIRONMENT,
})
/**
* Create a React Router request handler and pass
* Hydrogen's Storefront client to the loader context.
*/
const response = await handleRequest(
pack,
request,
createRequestHandler({
build: serverBuild,
mode: process.env.NODE_ENV,
getLoadContext: () => ({
cache,
waitUntil,
session,
storefront,
cart,
env,
pack,
}),
}),
)
handleRequest(pack, request, requestHandler) runs Pack analytics around your request handler, then commits the Pack session and test session cookies onto the response. Since 4.0 it also answers POST /pack/consent, which PackProvider and setPackConsent() from @pack/react use to record the visitor's consent. Always use it instead of calling the React Router request handler directly.
createPackClient options
- Name
cache- Type
- Cache
- Description
Required. A Cache API instance, e.g.
await caches.open('hydrogen').
- Name
waitUntil- Type
- ExecutionContext['waitUntil']
- Description
Required. The runtime
waitUntilused for background cache updates.
- Name
session- Type
- PackSession
- Description
Required. The Pack session created with
PackSession.init(). Stores preview mode state and customizer context.
- Name
testSession- Type
- PackTestSession
- Description
Required. The test session created with
PackTestSession.init(). Stores the visitor's A/B test assignment.
- Name
storeId- Type
- string
- Description
Your Pack storefront ID (
PACK_STOREFRONT_ID). Typed as optional, butcreatePackClientthrowsERR_HY_MISSING_STORE_IDwhen it is missing.
- Name
token- Type
- string
- Description
Your Pack secret token (
PACK_SECRET_TOKEN). Required unlessdefaultThemeDatais provided, otherwisecreatePackClientthrowsERR_HY_MISSING_TOKEN.
- Name
apiUrl- Type
- string
- Description
Optional. Overrides the Pack GraphQL API URL. When unset, preview mode and development use
https://app.packdigital.com/graphqland production uses the@pack/clientdefault CDN (https://apicdn.packdigital.com/graphql).
- Name
contentEnvironment- Type
- string
- Description
Optional. The content environment handle. Defaults to the primary environment. In preview mode, the environment passed by the Customizer takes precedence.
- Name
i18n- Type
- { language: string; country: string }
- Description
Optional. When set,
languageandcountryare added to the variables of everypack.querycall.
- Name
getCustomerContext- Type
- ({ request }) => CustomerContext | undefined | Promise<CustomerContext | undefined>
- Description
Optional. Tells Pack whether the visitor is signed in, for audiences that check it. Return
{ isLoggedIn }from a session read such as Hydrogen'scustomerAccount.isLoggedIn(); addshopifyCustomerIdonly if you need it. Called at most once per request, and only when an audience needs it. Without it, audiences that check sign-in are refused. Added in 4.0. See Targeting signed-in customers.
- Name
customerContextTimeoutMs- Type
- number
- Description
Optional. How long to wait for
getCustomerContext, in milliseconds. Defaults to300. If the hook throws or times out, audiences that check sign-in are refused for that request.
- Name
defaultThemeData- Type
- { data: Record<string, any> }
- Description
Optional. Static content used when no
tokenis provided. Queries resolve from this data instead of calling the Pack API.
- Name
request- Type
- Request
- Description
Deprecated. The request is now captured from the first
handleRequestcall.
The pack context object
createPackClient returns a Pack object, available as context.pack in loaders and actions.
- Name
query- Type
- <T>(query: string, options?: { variables?, cache?, test? }) => Promise<{ data, error, packTestInfo? }>
- Description
Runs a Pack GraphQL query. Published content is cached (
maxAge1 hour,staleWhileRevalidate23 hours,staleIfError1 day) unless you pass acachestrategy. Preview mode always bypasses the cache and requests theCURRENTversion.
- Name
isPreviewModeEnabled- Type
- () => boolean
- Description
Whether the current session is in preview mode.
- Name
getPackContextData- Type
- () => { packStoreId, packSessionId, packAbTest, packAbTests, packIsPreviewMode, packCustomizerMeta, packConsent, packConsentAllowed }
- Description
Values to spread into your root loader return. The A/B test hooks read
packAbTest,packAbTests, andpackSessionIdfrom root loader data, andPackTestProviderreadspackConsentAllowed.
- Name
abTest- Type
- Test | null | undefined
- Description
The visitor's oldest A/B test assignment, or the selected test when previewing in the Customizer. A visitor can be in several tests; use
abTestsfor all of them.
- Name
abTests- Type
- Test[]
- Description
Every test the visitor is assigned to, oldest first, including tests that don't apply to the current page. Added in 4.0.
- Name
consent- Type
- PackConsent
- Description
The visitor's consent for
analytics,marketing,preferences, andsaleOfData, eachgranted,denied, orunknown. Added in 4.0.
- Name
consentAllowed- Type
- PackConsentAllowed
- Description
Whether each purpose may be used, after applying
unknownConsent. Added in 4.0.
- Name
session- Type
- PackSession
- Description
The Pack session, e.g.
pack.session.get('customizerMeta').
- Name
testSession- Type
- PackTestSession
- Description
The A/B test session.
app/root.tsx
import type {Route} from './+types/root'
export async function loader({context}: Route.LoaderArgs) {
const {pack} = context
return {
...pack.getPackContextData(),
isPreviewModeEnabled: pack.isPreviewModeEnabled(),
customizerMeta: pack.session.get('customizerMeta'),
}
}
Creating endpoint for preview mode
Create an /api/edit route on your storefront so the Customizer and preview links can display your draft content.
To do this, create an api.edit.tsx file in your Hydrogen project's /app/routes folder and add the following code to it.
/app/routes/api.edit.tsx
import {previewModeAction, previewModeLoader} from '@pack/hydrogen'
import type {ActionFunction, LoaderFunction} from 'react-router'
export const action: ActionFunction = previewModeAction
export const loader: LoaderFunction = previewModeLoader
A GET to this route validates the edit token and enables preview mode. A POST exits preview mode and redirects to the submitted slug.
Exports
import {
// Client and request handling
createPackClient,
handleRequest,
PackSession,
PackTestSession,
previewModeAction,
previewModeLoader,
usePackCookies, // deprecated
// A/B testing
PackTestProvider,
PackTestRoute,
PackTestContext,
usePackTestContext,
useAbTest,
useAbTests,
useAbTestId,
useAbTestHandle,
useAbTestSessionId,
useAbTestVariantId,
useAbTestVariantHandle,
// Audiences
buildIdentityBundle,
registerConditionEvaluator,
getConditionEvaluator,
isConditionRegistered,
resolveRulesTree,
} from '@pack/hydrogen'
import type {
Pack,
TestAssignment,
TestAssignments,
PackConsent,
GetConsent,
CustomerContext,
GetCustomerContext,
IdentityBundle,
BuildIdentityBundleOptions,
ConditionEvaluator,
AudienceResolution,
MatchedCondition,
AudienceRulesTree,
AudienceRuleNode,
RuleGroupNode,
RuleConditionNode,
RuleGroupOperator,
} from '@pack/hydrogen'
Client and request handling
| Export | Description |
|---|---|
createPackClient | Creates the Pack client passed to your loader context. |
handleRequest | Wraps your React Router request handler, runs Pack analytics, and commits Pack session cookies. |
PackSession | Cookie session (__pack) for preview mode and customizer context. Create with PackSession.init(request, secrets). |
PackTestSession | Cookie session (__pack_test) for A/B test assignment. Create with PackTestSession.init(request, secrets). |
previewModeAction | Route action that exits preview mode. Export from api.edit.tsx. |
previewModeLoader | Route loader that validates the edit token and enters preview mode. Export from api.edit.tsx. |
usePackCookies | Deprecated in 4.0. Records the visitor's analytics consent when you pass hasUserConsent; without it, records nothing. domain is ignored. Use setPackConsent() from @pack/react instead. |
A/B testing
See A/B test implementation for setup details.
| Export | Description |
|---|---|
PackTestProvider | Provider that tracks exposures. Props: testExposureCallback, hasUserConsent. Since 4.0, hasUserConsent defaults to the visitor's analytics consent (packConsentAllowed), or false if the root loader doesn't expose it. Since 4.1, an explicit undefined holds exposures, and a testExposureCallback that returns false leaves the exposure unrecorded, so it is offered again (personalizations on the next page load; from 4.2, A/B tests on a later page where they show, within 24 hours of assignment). Don't check consent inside the callback. See "Reporting exposures" in step 4 of A/B test implementation. |
PackTestRoute | Component to render in each tested route. Fires exposure for the route's test. Optional impressionSelector. |
PackTestContext | The React context used by PackTestProvider. |
usePackTestContext | Reads PackTestContext. |
useAbTest | useAbTest(testHandle?) returns the named test if the visitor is in it, otherwise null. Without a handle, returns the visitor's oldest test. |
useAbTests | Returns every test the visitor is in, plus any personalizations they match on the current request. Added in 4.0. |
useAbTestId | Returns the test ID. Takes the same optional test handle. |
useAbTestHandle | Returns the test handle. Takes the same optional test handle. |
useAbTestSessionId | Returns the Pack session ID. |
useAbTestVariantId | Returns the assigned variant ID. Takes the same optional test handle. |
useAbTestVariantHandle | Returns the assigned variant handle. Takes the same optional test handle. |
The A/B test hooks read from the root route's loader data. useAbTest, the
hooks built on it, and useAbTestSessionId throw if
...pack.getPackContextData() is not part of the root loader return;
useAbTests returns []. Since 4.0 a visitor can be in several tests, so pass
the test's handle, e.g. useAbTestVariantHandle('homepage-hero').
Audiences
Added in @pack/hydrogen 3.3.0. Test audiences are evaluated server-side during test assignment, so most storefronts do not call these directly.
| Export | Description |
|---|---|
buildIdentityBundle | Builds an IdentityBundle (cookies, consent, device, geo, URL, referrer, optional Shopify customer) from a request. |
registerConditionEvaluator | Registers a ConditionEvaluator for a condition type. |
getConditionEvaluator | Returns the evaluator registered for a condition type, if any. |
isConditionRegistered | Returns whether an evaluator exists for a condition type. |
resolveRulesTree | Evaluates an AudienceRulesTree (AND/OR/NOT groups with condition leaves) against an identity bundle. Resolves to an AudienceResolution (matches, matchedConditions). |