@techsio/storefront-data
migrate-custom-hooks-to-storefront-data
lifecycleLoad this skill when replacing app-local Medusa hooks, query utilities, or service wrappers with the preset-first @techsio/storefront-data surface. Use it for migration cutovers, preserving storefront-specific callbacks, and removing dual source-of-truth behavior instead of running legacy and shared data layers in parallel.
This skill builds on setup-storefront-platform-in-next-app. Use it once the new preset seam exists in the app.
Migrate custom hooks to storefront-data
Setup
Start by centralizing the new preset seam before replacing feature code.
// src/lib/storefront.ts
import { createMedusaSdk } from "@techsio/storefront-data/shared/medusa-client"
import { createLocalStorageValueStore } from "@techsio/storefront-data/shared/storage-value-store"
import { createMedusaStorefrontPreset } from "@techsio/storefront-data/medusa/preset"
const sdk = createMedusaSdk({
baseUrl: process.env.NEXT_PUBLIC_MEDUSA_BACKEND_URL ?? "http://localhost:9000",
publishableKey: process.env.NEXT_PUBLIC_MEDUSA_PUBLISHABLE_KEY ?? "",
})
export const storefront = createMedusaStorefrontPreset({
sdk,
queryKeyNamespace: "n1",
cart: {
hooks: {
cartStorage: createLocalStorageValueStore({ key: "n1-cart-id" }),
},
},
})Core Patterns
Replace one vertical feature block at a time
"use client"
import { storefront } from "@/src/lib/storefront"
export function ProductList({ regionId }: { regionId: string }) {
const query = storefront.hooks.products.useProducts({
region_id: regionId,
limit: 24,
})
if (query.isLoading) return <div>Loading...</div>
return <div>{query.products.length}</div>
}The goal is a clean cutover for one screen or feature block, followed by deletion of the old implementation for that block.
Keep storefront-specific UX by wrapping the shared hook, not forking it
// src/lib/auth-hooks.ts
import { storefront } from "@/src/lib/storefront"
const track = (event: string) => {
console.log(event)
}
export function useLoginWithAnalytics() {
return storefront.hooks.auth.useLogin({
onSuccess: () => {
track("login_success")
},
})
}Delete the legacy query helpers after the cutover
// before
export const productQueryKey = (params: ProductParams) => ["n1-products", params]
// after
export const productQueryKey = storefront.queryKeys.products.listIf the screen already reads and writes through the preset, keeping the old query helper around only invites drift back into the app.
Common Mistakes
CRITICAL Old and new data layers on the same screen
Wrong:
const legacy = useLegacyProducts(params)
const shared = storefront.hooks.products.useProducts(params)Correct:
const products = storefront.hooks.products.useProducts(params)Parallel legacy and shared reads create duplicate requests, conflicting query keys, and no single source of truth.
Source: maintainer interview, libs/storefront-data/AGENTS.md, libs/storefront-data/README.md
HIGH Feature migration before preset centralization
Wrong:
export const useN1Products = () => storefront.hooks.products.useProducts({})
export const useN1Cart = () => storefront.hooks.cart.useCart({})Correct:
export const storefront = createMedusaStorefrontPreset({
sdk,
queryKeyNamespace: "n1",
})If the preset seam is not centralized first, query keys, storage, field defaults, and adapters stay scattered across the app.
Source: libs/storefront-data/README.md, maintainer interview
HIGH Losing storefront-specific callbacks during replacement
Wrong:
export const useLogin = storefront.hooks.auth.useLoginCorrect:
export function useLogin() {
return storefront.hooks.auth.useLogin({
onSuccess: () => {
toast.success("Logged in")
track("login_success")
},
})
}Legacy wrappers often exist only because they carry storefront-specific analytics, debug hooks, or UX side effects.
Source: maintainer interview
MEDIUM Keeping proven common logic app-local
Wrong:
export const useCustomOrders = () =>
useQuery({ queryKey: ["n1-orders"], queryFn: loadOrders })Correct:
const orders = storefront.hooks.orders.useOrders({ limit: 20 })Once a second storefront needs the same backend behavior, leaving it local recreates the duplication problem the migration is trying to remove.
Source: maintainer interview
See also: decide-app-specific-overrides-vs-shared-platform for promotion rules.