Migrate to the latest version of Hydrogen
Starting with version 2026-10, Hydrogen is a library that you add to your framework, instead of a framework of its own. Your app owns the server entry, the router, and the build. Hydrogen gives you the parts that you compose into each request: a Storefront API client, request handlers for the routes that Shopify owns, and handler groups for the cart and customer accounts.
This guide shows you how to migrate a storefront built on Hydrogen 2026-04 or earlier to Hydrogen 2026-10. Your storefront stays on React Router. If it's still on Remix, then you also move it to React Router 7 as you go.
Anchor to What you'll learnWhat you'll learn
In this guide, you'll learn how to do the following tasks:
- Check how much of the migration your storefront needs.
- Update your dependencies, scripts, and build configuration.
- Wire the Storefront API client and request handlers into a root middleware.
- Update your routes, imports, and typed queries.
- Move each storefront feature, like the cart, customer accounts, and analytics, to the new APIs.
- Check that your migrated storefront works.
Anchor to RequirementsRequirements
- Have a storefront that uses
@shopify/hydrogenversion 2026.4.x or earlier, on Remix or React Router. - Install Node.js version 22.12 or later, or version 24.
- Commit your work before you start, so that you can compare each step with your original code.
Anchor to How the migration worksHow the migration works
In earlier versions, Hydrogen owned the request. It created the Storefront API client, the cart, and the customer account client in server.ts, and passed them to your loaders through the load context. Its CLI ran your development server and built your app.
In version 2026-10, you build each request yourself, in a root middleware:
- Create a request context from the request, the buyer's IP address, and your market settings.
- Create a Storefront API client from the request context.
- Run
handleShopifyRoutes(), which answers the routes that Hydrogen owns, like/api/cartand/checkout, before your router runs. - Pass the Storefront API client to your loaders through a typed React Router context.
- If your router returns a 404, then run
handleShopifyRedirects(), which checks for a Shopify redirect.
Development and builds use plain Vite and React Router commands. The shopify hydrogen dev and shopify hydrogen build commands, and the hydrogen() Vite plugin, aren't part of version 2026-10.
The following table shows where the main parts of a classic storefront move:
| Classic Hydrogen | Hydrogen 2026-10 |
|---|---|
createHydrogenContext() in server.ts | A root middleware that creates the client and runs the request handlers |
context.storefront.query() | storefrontClient.graphql() from a typed React Router context |
context.cart and a /cart action route | createCartServerHandlers() at /api/cart, and a client-side cart store |
createCustomerAccountClient() | createCustomerAccountServerHandlers() and createCustomerSession() |
Analytics.Provider and useAnalytics() | ShopifyScripts and the window.Shopify.analytics event bus |
<Image>, <Money>, and <Pagination> | Your own components, with helpers like formatMoney() and the collection store |
getSitemap() and getSitemapIndex() | Your own sitemap queries |
Anchor to Step 1: Check where your storefront startsStep 1: Check where your storefront starts
Moving from Remix to React Router and moving to Hydrogen 2026-10 are separate migrations. Earlier versions of Hydrogen run on either Remix or React Router, so a storefront that already uses React Router can still need the Hydrogen migration.
Check your package.json file:
- If it lists
@shopify/hydrogenat a version earlier than2026.10, then your storefront needs the Hydrogen migration. Follow every step in this guide. - If it lists
@remix-run/*packages, then your storefront also needs the React Router migration. Follow the Remix tasks in Step 2 and Step 7. - If it lists
react-routerand@react-router/dev, has no@remix-run/*packages, and has areact-router.config.tsfile, then your storefront is already on React Router. Skip the Remix tasks.
Anchor to Step 2: Update dependenciesStep 2: Update dependencies
Install Hydrogen 2026-10:
Terminal
Then update the rest of your dependencies:
- If your storefront uses Remix, then remove the
@remix-run/*packages and@shopify/remix-oxygen, and addreact-routerand@react-router/dev. You don't needreact-router-dom. - Remove
@shopify/cli-hydrogen. - Add
@shopify/mini-oxygenversion 4.2.0 or later, whichvite previewneeds. - Keep
@shopify/cliversion 4.4.0 or later as a development dependency. You use it only to deploy.
Don't update each package to its latest version on its own. Pin every React Router package to the same version, and keep vite at version 8 and react and react-dom at version 19. Newer majors of some of these packages exist, like React Router 8, but Hydrogen 2026-10 is tested with the following versions:
package.json
To find the versions that the current Hydrogen release uses, check the package.json file in the React Router starter template.
Anchor to Step 3: Update scriptsStep 3: Update scripts
Replace the Hydrogen CLI commands in your package.json scripts with Vite and React Router commands:
package.json
The hydrogen command comes with the @shopify/hydrogen package. In the dev script, it warns you when your project's agent instructions don't match the installed Hydrogen version, without stopping the server. The dev and dev:https scripts run the same command. Your Vite config turns on local HTTPS when the script name is dev:https.
Add the files that these scripts generate to your .gitignore file:
.gitignore
Anchor to Step 4: Update your Vite configStep 4: Update your Vite config
Remove the hydrogen() plugin. Add localHttps() from @shopify/hydrogen/vite and oxygen() from @shopify/mini-oxygen/vite, before reactRouter():
vite.config.ts
Keep the following details in mind:
oxygen()returns an array of plugins, so spread it into the list. You don't pass it an entry file, because it uses the React Router server build.localHttps()doesn't detect the script name. You passenabledyourself.- The
ssr.optimizeDeps.includeentries prevent module errors in the development server. Keep any entries that your storefront already lists.
The React Router starter template also pins the Oxygen compatibility date, so that MiniOxygen doesn't run a newer runtime locally than Oxygen runs in production. To check whether you need this, refer to the template's vite.config.ts file.
Anchor to Step 5: Add a React Router configStep 5: Add a React Router config
Add a react-router.config.ts file to your project's root directory, or update the one you have:
react-router.config.ts
Set buildDirectory to dist. React Router builds to build by default, but the deploy script uploads from dist/client and dist/server. Without this setting, the build succeeds but the deploy uploads nothing.
The v8_middleware flag turns on the root middleware that you add in the next step.
You can keep your route files where they are. If your storefront uses flat route filenames, like ($locale).products.$handle.tsx, then use @react-router/fs-routes in your app/routes.ts file to keep them. Renaming routes during the migration can drop them without an error.
Anchor to Step 6: Wire Hydrogen into your appStep 6: Wire Hydrogen into your app
Most of the migration is one context file and one middleware. The following code follows the shape of the React Router starter template. Check each signature against the types in your installed version.
Anchor to Create the environment contextCreate the environment context
The server entry passes the Oxygen environment to your app through a typed React Router context. Create the context and the environment's type in a library file. List the variables that your storefront reads, like the ones in Step 9:
app/lib/env.ts
The global Env type that server.ts uses comes from your env.d.ts file. Make it extend this type, and add cache and waitUntil to the router context's type, because server.ts sets them. For an example, refer to the template's env.d.ts file.
Anchor to Simplify the server entrySimplify the server entry
Your server.ts file no longer creates Hydrogen clients. It opens the cache, passes the Oxygen environment and waitUntil to the router, and runs the request handler:
server.ts
Anchor to Create the Storefront API clientCreate the Storefront API client
Create the client for each request in a library file. Pass the buyer's IP address to the request context, and the cache, waitUntil, and storefront ID to the client config:
app/lib/storefront.ts
getBuyerIp() is your app's code, not a Hydrogen export. If your storefront doesn't have a private Storefront API token, then use a public client instead. To compare the two, refer to Query the Storefront API.
Anchor to Add route templatesAdd route templates
Route templates tell Hydrogen how your app's URLs map to Shopify resources. Create them in their own file:
app/lib/route-templates.ts
If your storefront uses paths that differ from Shopify's defaults, like a locale prefix or a renamed cart page, then add them here. To learn more, refer to Configure route templates.
Anchor to Add the cart handlersAdd the cart handlers
Create the cart's server handlers and its React components:
app/lib/cart-handlers.ts
app/lib/cart.ts
Anchor to Add a session managerAdd a session manager
The request handlers need a session manager with four methods. A per-request map works until you add customer accounts, which provide a cookie-backed session manager:
app/lib/session.ts
Anchor to Add the root middlewareAdd the root middleware
In app/root.tsx, export a middleware that builds the request, runs the request handlers, and passes the client to your loaders:
app/root.tsx
handleShopifyRoutes() decides synchronously whether it owns a request. It returns a promise of a response, or null, so check the result without awaiting it. If you call it inside a try block, then use return await shopifyRoute so that your catch block handles its errors.
Reuse the request context from the Storefront API client. Creating a second one makes handleShopifyRoutes() throw an error.
The following lines are easy to leave out, and nothing reports an error when they're missing:
| Line | What goes wrong without it |
|---|---|
requestContext.applyResponseHeaders() | Shopify's response headers are missing from your pages. |
buyerIp in createShopifyRequestContext() | Prices and inventory that depend on the buyer's location are wrong. |
routeTemplates in both handler calls | URLs from the request handlers and redirect targets point to the wrong paths. |
To learn more about the request flow, refer to Wire Hydrogen request handlers.
Anchor to Read the client in loadersRead the client in loaders
Replace context.storefront in your loaders with the client from the React Router context. The client's graphql() method replaces query():
app/routes/products.$handle.tsx
Anchor to Step 7: Update routes and importsStep 7: Update routes and imports
Update your route modules for React Router 7 and Hydrogen 2026-10:
- If your storefront uses Remix, then change imports from
@remix-run/react,@remix-run/node, and@remix-run/server-runtimetoreact-router. - Replace
json()with a plain object, and replacedefer()with an object that contains promises. A deferred loader that you don't migrate correctly can leave a section of the page loading forever, with no error. - Cast
useMatches()[i].handleto your app's handle type before you read its properties. In React Router 7,handleis typed asunknown. - Delete route files for paths that the request handlers own, like
/api/cart,/checkout, cart permalinks, and/account/login. A route file for one of these paths can shadow the handler. For the full list, refer to Routes Hydrogen handles.
To find the imports that you missed, run npm run typecheck.
Anchor to Step 8: Update types and GraphQLStep 8: Update types and Graph QL
Hydrogen 2026-10 includes the Storefront API and Customer Account API schemas, and a TypeScript plugin that types your queries. Add the plugin to your tsconfig.json file:
tsconfig.json
Then update your types:
- Remove any gql.tada
schemaorschemassettings, and any generatedstorefrontapi.generated.d.tsfiles from earlier versions. - Type query results with
ResultOfand theStorefrontApinamespace. Thegql()function returns a typed query string, soFragmentOfdoesn't apply. - Replace types that Hydrogen no longer exports. For example,
I18nBaseis nowI18nConfig. Import Storefront API object types from@shopify/hydrogen/storefront-api-types, and Customer Account API types from@shopify/hydrogen/customer-account-api-types. - Some helpers are still exported, like
flattenConnection(),getSelectedProductOptions(),formatMoney(), and theMoneyV2type. Check the installed package's exports before you write your own.
Query results are typed from the fields that you select, so they're narrower than the full API types. If a component prop uses a full type, like ProductVariant, then change the prop to the fields that the component reads. For a query on a union field, like search, narrow the results with Extract on __typename.
hydrogen gql check in the typecheck script catches a broken setup. Without it, your queries stop being typed with no error. To learn more, refer to Type and validate Storefront API queries.
Anchor to Step 9: Connect your storeStep 9: Connect your store
Add your store's environment variables to .env. If you linked your project to a Hydrogen storefront, then run npx shopify hydrogen env pull to get them. On Oxygen, a linked storefront provides them for you.
.env
Leave PUBLIC_STOREFRONT_API_TOKEN commented out unless you use a public token. An empty value is an empty string, which the client rejects.
To check the migration before you connect your store, you can point the client at mock.shop instead. mock.shop accepts only a public client with no token. It rejects private tokens.
Anchor to Step 10: Migrate your storefront's featuresStep 10: Migrate your storefront's features
Steps 1 to 9 apply to every storefront. The rest of the migration depends on the features your storefront has. Search your original code for each feature, and migrate the ones you find.
Migrate the first three features in order, because each one uses the one before it:
- Markets: Look for
@inContext, alocalizationquery, apathPrefix, or a country selector. Markets set the currency and language that analytics use. Follow Support multiple markets. - Customer accounts: Look for
createCustomerAccountClient,/accountroutes, or login links. Follow Set up customer accounts. - Analytics and consent: Look for
Analytics.Provider,useAnalytics,getShopAnalytics, a GTM ordataLayersnippet, or a cookie banner. Migrate analytics and consent together. Follow Add analytics and consent.
Then migrate the others in any order:
| If your storefront has | Then follow |
|---|---|
| A cart page, a cart drawer, or add to cart buttons | Build cart UI and Build a cart drawer |
| Extra cart line fields or cart metafields | Use cart metafields |
| Product pages with variant options | Build product pages with variant selection |
| Collection pages, filters, pagination, or search | Build collection and search pages |
| Shop Pay buttons | Add Shop Pay buttons |
| Sitemap routes or SEO metadata | Migrate sitemaps |
Subscriptions, B2B, content pages, gift cards, product recommendations, and integrations that you built yourself are your app's code. Migrate them like any other route or component.
Keep a list of the features you didn't find in your original code. When you finish, that list shows that you skipped those features on purpose, and didn't miss them.
Anchor to Migrate the cartMigrate the cart
The cart moves from loader data to a client-side store. The following changes are easy to miss, because adding to the cart still works when they're wrong:
- Render every cart surface from
useCart((state) => state.data). If your layout passes a deferredcartfrom the root loader to<Await>, then the drawer and cart page stay empty, even though the header count updates. Delete the deferred cart from the loader. - Open the cart drawer from the cart store, not from a fetcher.
useCartForm()submits through the cart store, souseFetchers()never sees the request. Open the drawer whenuseCart((state) => state.pending.lines.size > 0)becomestrue. Don't open it based ontotalQuantity, which changes when an existing cart loads. - Load the cart in the root loader with
await cartHandlers.get({storefrontClient, request}), and pass the result'sdatatoCartProviderasinitialData. That's the{cart, errors?}envelope, not the cart object alone. Without this, the cart shows its empty state before it loads. - Disable line item controls only for new lines whose ID starts with
optimistic:. If you disable them for every pending line, then the buttons lock on every click. - If you wrapped the classic cart handler to add fields, then pass a
fragmenttocreateCartServerHandlers()instead.
Anchor to Migrate customer accountsMigrate customer accounts
Register the account handlers next to the cart handlers, in the handlers array of handleShopifyRoutes(). The handlers own /account/login, /account/logout, /account/authorize, and /account/refresh, so delete any route files for those paths. Link to them with <a> and <form> elements, not React Router's <Link> and <Form>, so that the request reaches the server.
Pass the same customer session to createCartServerHandlers(), so that the cart knows who's logged in. Use the library's login and callback functions instead of building OAuth URLs yourself.
Customer login needs HTTPS, even locally. Run npm run dev:https and open https://local.tryhydrogen.dev. To set up certificates, refer to Test sign-in locally with HTTPS.
Anchor to Migrate analyticsMigrate analytics
Hydrogen 2026-10 removes Analytics.Provider, the Analytics.ProductView and other view components, useAnalytics(), and getShopAnalytics(). The event bus still exists. Render ShopifyScripts from @shopify/hydrogen/react in your root layout, and publish events to window.Shopify.analytics.
Port every event that your storefront tracks. It's easy to rewrite your tracking component to publish only page views, which drops product views, cart views, and cart changes with no error. Publish route events from tracker components when the location changes, and track cart changes with useCartAnalytics().
Build the shop payload in your root loader. If your storefront sends events to its own destination, like GTM, then register it with addDestination(). Consent controls which events reach destinations, so migrate your consent settings at the same time.
Anchor to Migrate UI componentsMigrate UI components
Hydrogen 2026-10 doesn't export <Image>, <MediaFile>, <Money>, or <Pagination>. Replace them with your own components:
- For images, use an
<img>element with Shopify CDN URLs, or your framework's image component. - For prices, use
formatMoney(). Let your price component accept any object withamountandcurrencyCode, so that it works with both Storefront API and Customer Account API prices. - For pagination, use the collection store. "Load more" is a link that JavaScript upgrades to load in place.
Variant options are links that the product form store upgrades. Render each option value as an <a href> element, and spread the store's register() props on it, so that selecting an option updates the page without a full navigation.
After you replace your root layout, check that the skip link appears when it gets focus, that focus moves to the main content after navigation, and that icon buttons have accessible names.
Delete hooks and helpers that wrapped the old cart fetchers and routes. They still compile, but nothing uses them.
Anchor to Migrate sitemapsMigrate sitemaps
Hydrogen 2026-10 doesn't include getSitemap() or getSitemapIndex(). Write your own sitemap queries in a library file, like app/lib/sitemap.ts, and run them with storefront.graphql(). Keep any URL changes that your sitemap made, like locale prefixes or renamed routes.
Anchor to Step 11: Check your migrationStep 11: Check your migration
Run your type check and build. Both must finish without errors:
Terminal
Most missed imports, return helpers, and query problems show up here. Fix the cause instead of suppressing the error.
Then start your development server and test your storefront:
Terminal
Pay attention to the following areas, which can stop working while your storefront still builds:
- Analytics: Check that product views, cart views, and cart changes still publish, not only page views.
- Cart: Add, remove, and change the quantity of a product, and check that the cart drawer opens when you add a product.
- Customer login: Run
npm run dev:httpsand complete the full login flow. - Variant selection: Check that selecting an option updates the page without a full navigation.
Before you deploy, refer to Check your storefront before launch.