@shopify/hydrogen 2026.10.0-preview.4 → 2026.10.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/cli/index.mjs +23 -12
- package/dist/client/client.d.mts +18 -15
- package/dist/client/client.mjs +8 -3
- package/dist/client/client.mjs.map +1 -1
- package/dist/client/errors.d.mts +35 -13
- package/dist/client/errors.mjs +23 -13
- package/dist/client/errors.mjs.map +1 -1
- package/dist/client/types.d.mts +140 -90
- package/dist/core/analytics/cart-tracker.d.mts +19 -10
- package/dist/core/analytics/cart-tracker.mjs +18 -10
- package/dist/core/analytics/cart-tracker.mjs.map +1 -1
- package/dist/core/analytics/events.d.mts +17 -11
- package/dist/core/analytics/events.mjs +13 -4
- package/dist/core/analytics/events.mjs.map +1 -1
- package/dist/core/analytics/types.d.mts +207 -118
- package/dist/core/analytics/utils/flatten-connection.d.mts +11 -9
- package/dist/core/analytics/utils/flatten-connection.mjs +7 -6
- package/dist/core/analytics/utils/flatten-connection.mjs.map +1 -1
- package/dist/core/cache/fetch-with-cache.d.mts +24 -4
- package/dist/core/cache/fetch-with-cache.mjs +17 -2
- package/dist/core/cache/fetch-with-cache.mjs.map +1 -1
- package/dist/core/cache/key.d.mts +2 -0
- package/dist/core/cache/key.mjs.map +1 -1
- package/dist/core/cache/run-with-cache.d.mts +56 -8
- package/dist/core/cache/run-with-cache.mjs +17 -6
- package/dist/core/cache/run-with-cache.mjs.map +1 -1
- package/dist/core/cache/store.d.mts +8 -6
- package/dist/core/cache/store.mjs.map +1 -1
- package/dist/core/cache/strategies.d.mts +27 -2
- package/dist/core/cache/strategies.mjs +6 -0
- package/dist/core/cache/strategies.mjs.map +1 -1
- package/dist/core/cart/actions.d.mts +37 -30
- package/dist/core/cart/actions.mjs +21 -9
- package/dist/core/cart/actions.mjs.map +1 -1
- package/dist/core/cart/attach-quantity-input.d.mts +8 -7
- package/dist/core/cart/attach-quantity-input.mjs +8 -7
- package/dist/core/cart/attach-quantity-input.mjs.map +1 -1
- package/dist/core/cart/buyer-identity-sync.d.mts +7 -6
- package/dist/core/cart/buyer-identity-sync.mjs.map +1 -1
- package/dist/core/cart/cart.d.mts +59 -46
- package/dist/core/cart/cart.mjs +46 -29
- package/dist/core/cart/cart.mjs.map +1 -1
- package/dist/core/cart/cookie.d.mts +8 -4
- package/dist/core/cart/cookie.mjs +8 -4
- package/dist/core/cart/cookie.mjs.map +1 -1
- package/dist/core/cart/form.d.mts +34 -20
- package/dist/core/cart/form.mjs +6 -4
- package/dist/core/cart/form.mjs.map +1 -1
- package/dist/core/cart/get-cart.d.mts +27 -15
- package/dist/core/cart/get-cart.mjs +13 -8
- package/dist/core/cart/get-cart.mjs.map +1 -1
- package/dist/core/cart/quantity.d.mts +16 -5
- package/dist/core/cart/quantity.mjs +16 -5
- package/dist/core/cart/quantity.mjs.map +1 -1
- package/dist/core/cart/queries.d.mts +25 -19
- package/dist/core/cart/queries.mjs +15 -1
- package/dist/core/cart/queries.mjs.map +1 -1
- package/dist/core/cart/server-handlers.d.mts +82 -45
- package/dist/core/cart/server-handlers.mjs +4 -0
- package/dist/core/cart/server-handlers.mjs.map +1 -1
- package/dist/core/cart/state.d.mts +110 -64
- package/dist/core/cart/state.mjs +24 -6
- package/dist/core/cart/state.mjs.map +1 -1
- package/dist/core/collection/collection.d.mts +52 -53
- package/dist/core/collection/collection.mjs +5 -3
- package/dist/core/collection/collection.mjs.map +1 -1
- package/dist/core/collection/reconciler.d.mts +23 -16
- package/dist/core/collection/reconciler.mjs +8 -4
- package/dist/core/collection/reconciler.mjs.map +1 -1
- package/dist/core/collection/state.d.mts +31 -26
- package/dist/core/collection/state.mjs +4 -3
- package/dist/core/collection/state.mjs.map +1 -1
- package/dist/core/collection/url.d.mts +70 -29
- package/dist/core/collection/url.mjs +63 -25
- package/dist/core/collection/url.mjs.map +1 -1
- package/dist/core/constants.d.mts +1 -0
- package/dist/core/constants.mjs +1 -0
- package/dist/core/constants.mjs.map +1 -1
- package/dist/core/logging/logging.d.mts +14 -8
- package/dist/core/logging/logging.mjs +15 -10
- package/dist/core/logging/logging.mjs.map +1 -1
- package/dist/core/logging/types.d.mts +28 -11
- package/dist/core/logging/types.mjs.map +1 -1
- package/dist/core/money/format.d.mts +6 -4
- package/dist/core/money/format.mjs +19 -9
- package/dist/core/money/format.mjs.map +1 -1
- package/dist/core/money/types.d.mts +41 -25
- package/dist/core/predictive-search/form.d.mts +37 -7
- package/dist/core/predictive-search/form.mjs +21 -3
- package/dist/core/predictive-search/form.mjs.map +1 -1
- package/dist/core/predictive-search/queries.d.mts +18 -30
- package/dist/core/predictive-search/queries.mjs +7 -3
- package/dist/core/predictive-search/queries.mjs.map +1 -1
- package/dist/core/predictive-search/search.d.mts +25 -27
- package/dist/core/predictive-search/search.mjs +20 -19
- package/dist/core/predictive-search/search.mjs.map +1 -1
- package/dist/core/predictive-search/server-handlers.d.mts +25 -26
- package/dist/core/predictive-search/server-handlers.mjs +4 -0
- package/dist/core/predictive-search/server-handlers.mjs.map +1 -1
- package/dist/core/predictive-search/store.d.mts +65 -24
- package/dist/core/predictive-search/store.mjs +12 -1
- package/dist/core/predictive-search/store.mjs.map +1 -1
- package/dist/core/predictive-search/url.d.mts +36 -37
- package/dist/core/predictive-search/url.mjs +14 -6
- package/dist/core/predictive-search/url.mjs.map +1 -1
- package/dist/core/product/form.d.mts +63 -16
- package/dist/core/product/form.mjs +8 -2
- package/dist/core/product/form.mjs.map +1 -1
- package/dist/core/product/options.d.mts +8 -10
- package/dist/core/product/options.mjs +8 -10
- package/dist/core/product/options.mjs.map +1 -1
- package/dist/core/product/product-form.d.mts +77 -29
- package/dist/core/product/product-form.mjs +32 -10
- package/dist/core/product/product-form.mjs.map +1 -1
- package/dist/core/product/state.d.mts +55 -32
- package/dist/core/product/url.d.mts +19 -13
- package/dist/core/product/url.mjs +10 -10
- package/dist/core/product/url.mjs.map +1 -1
- package/dist/core/request-context.d.mts +77 -19
- package/dist/core/request-context.mjs +5 -1
- package/dist/core/request-context.mjs.map +1 -1
- package/dist/core/request-routing/handle-shopify-redirects.d.mts +12 -4
- package/dist/core/request-routing/handle-shopify-redirects.mjs +9 -2
- package/dist/core/request-routing/handle-shopify-redirects.mjs.map +1 -1
- package/dist/core/request-routing/handle-shopify-routes.d.mts +5 -5
- package/dist/core/request-routing/handle-shopify-routes.mjs +7 -5
- package/dist/core/request-routing/handle-shopify-routes.mjs.map +1 -1
- package/dist/core/request-routing/interceptors/proxy.mjs +79 -43
- package/dist/core/request-routing/interceptors/proxy.mjs.map +1 -1
- package/dist/core/request-routing/interceptors/ucp-profile-proxy.mjs +69 -0
- package/dist/core/request-routing/interceptors/ucp-profile-proxy.mjs.map +1 -0
- package/dist/core/request-routing/interceptors/url-redirects.mjs +6 -1
- package/dist/core/request-routing/interceptors/url-redirects.mjs.map +1 -1
- package/dist/core/request-routing/interceptors/well-known.mjs +1 -1
- package/dist/core/request-routing/interceptors/well-known.mjs.map +1 -1
- package/dist/core/request-routing/registered-routes.d.mts +12 -0
- package/dist/core/request-routing/registered-routes.mjs +12 -0
- package/dist/core/request-routing/registered-routes.mjs.map +1 -1
- package/dist/core/request-routing/route-types.d.mts +43 -18
- package/dist/core/shop-pay/shop-pay.d.mts +71 -35
- package/dist/core/shop-pay/shop-pay.mjs +36 -10
- package/dist/core/shop-pay/shop-pay.mjs.map +1 -1
- package/dist/core/shopify-scripts/index.d.mts +15 -5
- package/dist/core/shopify-scripts/index.mjs +15 -5
- package/dist/core/shopify-scripts/index.mjs.map +1 -1
- package/dist/core/shopify-scripts/initialize.d.mts +12 -4
- package/dist/core/shopify-scripts/initialize.mjs +12 -4
- package/dist/core/shopify-scripts/initialize.mjs.map +1 -1
- package/dist/core/shopify-scripts/types.d.mts +58 -25
- package/dist/core/standard-routes/build.d.mts +11 -12
- package/dist/core/standard-routes/build.mjs +11 -12
- package/dist/core/standard-routes/build.mjs.map +1 -1
- package/dist/core/standard-routes/types.d.mts +30 -24
- package/dist/core/url.mjs +2 -1
- package/dist/core/url.mjs.map +1 -1
- package/dist/customer-account/client.d.mts +36 -69
- package/dist/customer-account/client.mjs +7 -8
- package/dist/customer-account/client.mjs.map +1 -1
- package/dist/customer-account/errors.d.mts +45 -36
- package/dist/customer-account/errors.mjs +21 -36
- package/dist/customer-account/errors.mjs.map +1 -1
- package/dist/customer-account/graphql.d.mts +11 -23
- package/dist/customer-account/graphql.mjs +3 -9
- package/dist/customer-account/graphql.mjs.map +1 -1
- package/dist/customer-account/session.d.mts +130 -100
- package/dist/customer-account/session.mjs +32 -10
- package/dist/customer-account/session.mjs.map +1 -1
- package/dist/customer-account/type-resolver.d.mts +7 -8
- package/dist/development/client/client.d.mts +18 -15
- package/dist/development/client/client.mjs +8 -3
- package/dist/development/client/client.mjs.map +1 -1
- package/dist/development/client/errors.d.mts +35 -13
- package/dist/development/client/errors.mjs +23 -13
- package/dist/development/client/errors.mjs.map +1 -1
- package/dist/development/client/types.d.mts +140 -90
- package/dist/development/core/analytics/cart-tracker.d.mts +19 -10
- package/dist/development/core/analytics/cart-tracker.mjs +18 -10
- package/dist/development/core/analytics/cart-tracker.mjs.map +1 -1
- package/dist/development/core/analytics/events.d.mts +17 -11
- package/dist/development/core/analytics/events.mjs +13 -4
- package/dist/development/core/analytics/events.mjs.map +1 -1
- package/dist/development/core/analytics/types.d.mts +207 -118
- package/dist/development/core/analytics/utils/flatten-connection.d.mts +11 -9
- package/dist/development/core/analytics/utils/flatten-connection.mjs +7 -6
- package/dist/development/core/analytics/utils/flatten-connection.mjs.map +1 -1
- package/dist/development/core/cache/fetch-with-cache.d.mts +24 -4
- package/dist/development/core/cache/fetch-with-cache.mjs +17 -2
- package/dist/development/core/cache/fetch-with-cache.mjs.map +1 -1
- package/dist/development/core/cache/key.d.mts +2 -0
- package/dist/development/core/cache/key.mjs.map +1 -1
- package/dist/development/core/cache/run-with-cache.d.mts +56 -8
- package/dist/development/core/cache/run-with-cache.mjs +17 -6
- package/dist/development/core/cache/run-with-cache.mjs.map +1 -1
- package/dist/development/core/cache/store.d.mts +8 -6
- package/dist/development/core/cache/store.mjs.map +1 -1
- package/dist/development/core/cache/strategies.d.mts +27 -2
- package/dist/development/core/cache/strategies.mjs +6 -0
- package/dist/development/core/cache/strategies.mjs.map +1 -1
- package/dist/development/core/cart/actions.d.mts +37 -30
- package/dist/development/core/cart/actions.mjs +21 -9
- package/dist/development/core/cart/actions.mjs.map +1 -1
- package/dist/development/core/cart/attach-quantity-input.d.mts +8 -7
- package/dist/development/core/cart/attach-quantity-input.mjs +8 -7
- package/dist/development/core/cart/attach-quantity-input.mjs.map +1 -1
- package/dist/development/core/cart/buyer-identity-sync.d.mts +6 -5
- package/dist/development/core/cart/buyer-identity-sync.mjs.map +1 -1
- package/dist/development/core/cart/cart.d.mts +59 -46
- package/dist/development/core/cart/cart.mjs +46 -29
- package/dist/development/core/cart/cart.mjs.map +1 -1
- package/dist/development/core/cart/cookie.d.mts +8 -4
- package/dist/development/core/cart/cookie.mjs +8 -4
- package/dist/development/core/cart/cookie.mjs.map +1 -1
- package/dist/development/core/cart/form.d.mts +34 -20
- package/dist/development/core/cart/form.mjs +6 -4
- package/dist/development/core/cart/form.mjs.map +1 -1
- package/dist/development/core/cart/get-cart.d.mts +27 -15
- package/dist/development/core/cart/get-cart.mjs +13 -8
- package/dist/development/core/cart/get-cart.mjs.map +1 -1
- package/dist/development/core/cart/quantity.d.mts +16 -5
- package/dist/development/core/cart/quantity.mjs +16 -5
- package/dist/development/core/cart/quantity.mjs.map +1 -1
- package/dist/development/core/cart/queries.d.mts +25 -19
- package/dist/development/core/cart/queries.mjs +15 -1
- package/dist/development/core/cart/queries.mjs.map +1 -1
- package/dist/development/core/cart/server-handlers.d.mts +82 -45
- package/dist/development/core/cart/server-handlers.mjs +4 -0
- package/dist/development/core/cart/server-handlers.mjs.map +1 -1
- package/dist/development/core/cart/state.d.mts +110 -64
- package/dist/development/core/cart/state.mjs +24 -6
- package/dist/development/core/cart/state.mjs.map +1 -1
- package/dist/development/core/collection/collection.d.mts +52 -53
- package/dist/development/core/collection/collection.mjs +5 -3
- package/dist/development/core/collection/collection.mjs.map +1 -1
- package/dist/development/core/collection/reconciler.d.mts +23 -16
- package/dist/development/core/collection/reconciler.mjs +8 -4
- package/dist/development/core/collection/reconciler.mjs.map +1 -1
- package/dist/development/core/collection/state.d.mts +31 -26
- package/dist/development/core/collection/state.mjs +4 -3
- package/dist/development/core/collection/state.mjs.map +1 -1
- package/dist/development/core/collection/url.d.mts +70 -29
- package/dist/development/core/collection/url.mjs +63 -25
- package/dist/development/core/collection/url.mjs.map +1 -1
- package/dist/development/core/constants.mjs.map +1 -1
- package/dist/development/core/logging/logging.d.mts +14 -8
- package/dist/development/core/logging/logging.mjs +15 -10
- package/dist/development/core/logging/logging.mjs.map +1 -1
- package/dist/development/core/logging/types.d.mts +28 -11
- package/dist/development/core/logging/types.mjs.map +1 -1
- package/dist/development/core/money/format.d.mts +6 -4
- package/dist/development/core/money/format.mjs +19 -9
- package/dist/development/core/money/format.mjs.map +1 -1
- package/dist/development/core/money/types.d.mts +41 -25
- package/dist/development/core/predictive-search/form.d.mts +37 -7
- package/dist/development/core/predictive-search/form.mjs +21 -3
- package/dist/development/core/predictive-search/form.mjs.map +1 -1
- package/dist/development/core/predictive-search/queries.d.mts +18 -30
- package/dist/development/core/predictive-search/queries.mjs +7 -3
- package/dist/development/core/predictive-search/queries.mjs.map +1 -1
- package/dist/development/core/predictive-search/search.d.mts +25 -27
- package/dist/development/core/predictive-search/search.mjs +20 -19
- package/dist/development/core/predictive-search/search.mjs.map +1 -1
- package/dist/development/core/predictive-search/server-handlers.d.mts +25 -26
- package/dist/development/core/predictive-search/server-handlers.mjs +4 -0
- package/dist/development/core/predictive-search/server-handlers.mjs.map +1 -1
- package/dist/development/core/predictive-search/store.d.mts +65 -24
- package/dist/development/core/predictive-search/store.mjs +12 -1
- package/dist/development/core/predictive-search/store.mjs.map +1 -1
- package/dist/development/core/predictive-search/url.d.mts +36 -37
- package/dist/development/core/predictive-search/url.mjs +14 -6
- package/dist/development/core/predictive-search/url.mjs.map +1 -1
- package/dist/development/core/product/form.d.mts +63 -16
- package/dist/development/core/product/form.mjs +8 -2
- package/dist/development/core/product/form.mjs.map +1 -1
- package/dist/development/core/product/options.d.mts +8 -10
- package/dist/development/core/product/options.mjs +8 -10
- package/dist/development/core/product/options.mjs.map +1 -1
- package/dist/development/core/product/product-form.d.mts +77 -29
- package/dist/development/core/product/product-form.mjs +32 -10
- package/dist/development/core/product/product-form.mjs.map +1 -1
- package/dist/development/core/product/state.d.mts +55 -32
- package/dist/development/core/product/url.d.mts +19 -13
- package/dist/development/core/product/url.mjs +10 -10
- package/dist/development/core/product/url.mjs.map +1 -1
- package/dist/development/core/request-context.d.mts +77 -19
- package/dist/development/core/request-context.mjs +5 -1
- package/dist/development/core/request-context.mjs.map +1 -1
- package/dist/development/core/request-routing/handle-shopify-redirects.d.mts +12 -4
- package/dist/development/core/request-routing/handle-shopify-redirects.mjs +9 -2
- package/dist/development/core/request-routing/handle-shopify-redirects.mjs.map +1 -1
- package/dist/development/core/request-routing/handle-shopify-routes.development.d.mts +9 -1
- package/dist/development/core/request-routing/handle-shopify-routes.development.mjs +7 -0
- package/dist/development/core/request-routing/handle-shopify-routes.development.mjs.map +1 -1
- package/dist/development/core/request-routing/handle-shopify-routes.mjs +7 -5
- package/dist/development/core/request-routing/handle-shopify-routes.mjs.map +1 -1
- package/dist/development/core/request-routing/interceptors/proxy.mjs +79 -43
- package/dist/development/core/request-routing/interceptors/proxy.mjs.map +1 -1
- package/dist/development/core/request-routing/interceptors/ucp-profile-proxy.mjs +69 -0
- package/dist/development/core/request-routing/interceptors/ucp-profile-proxy.mjs.map +1 -0
- package/dist/development/core/request-routing/interceptors/url-redirects.mjs +6 -1
- package/dist/development/core/request-routing/interceptors/url-redirects.mjs.map +1 -1
- package/dist/development/core/request-routing/interceptors/well-known.mjs +1 -1
- package/dist/development/core/request-routing/interceptors/well-known.mjs.map +1 -1
- package/dist/development/core/request-routing/registered-routes.d.mts +12 -0
- package/dist/development/core/request-routing/registered-routes.mjs +12 -0
- package/dist/development/core/request-routing/registered-routes.mjs.map +1 -1
- package/dist/development/core/request-routing/route-types.d.mts +43 -18
- package/dist/development/core/shop-pay/shop-pay.d.mts +71 -35
- package/dist/development/core/shop-pay/shop-pay.mjs +36 -10
- package/dist/development/core/shop-pay/shop-pay.mjs.map +1 -1
- package/dist/development/core/shopify-scripts/index.d.mts +15 -5
- package/dist/development/core/shopify-scripts/index.mjs +15 -5
- package/dist/development/core/shopify-scripts/index.mjs.map +1 -1
- package/dist/development/core/shopify-scripts/initialize.d.mts +12 -4
- package/dist/development/core/shopify-scripts/initialize.mjs +12 -4
- package/dist/development/core/shopify-scripts/initialize.mjs.map +1 -1
- package/dist/development/core/shopify-scripts/types.d.mts +58 -25
- package/dist/development/core/standard-routes/build.d.mts +11 -12
- package/dist/development/core/standard-routes/build.mjs +11 -12
- package/dist/development/core/standard-routes/build.mjs.map +1 -1
- package/dist/development/core/standard-routes/types.d.mts +30 -24
- package/dist/development/core/url.mjs +2 -1
- package/dist/development/core/url.mjs.map +1 -1
- package/dist/development/customer-account/session.d.mts +45 -54
- package/dist/development/customer-account/session.mjs +20 -4
- package/dist/development/customer-account/session.mjs.map +1 -1
- package/dist/development/globals.d.mts +20 -11
- package/dist/development/graphql/generated/storefront-api-types.d.mts +127 -1
- package/dist/development/graphql/graphql.d.mts +17 -16
- package/dist/development/graphql/graphql.mjs +7 -8
- package/dist/development/graphql/graphql.mjs.map +1 -1
- package/dist/development/graphql/type-resolver.d.mts +15 -8
- package/dist/development/react/cart.d.mts +78 -32
- package/dist/development/react/cart.mjs +56 -29
- package/dist/development/react/cart.mjs.map +1 -1
- package/dist/development/react/collection.d.mts +40 -22
- package/dist/development/react/collection.mjs +27 -7
- package/dist/development/react/collection.mjs.map +1 -1
- package/dist/development/react/predictive-search.d.mts +65 -50
- package/dist/development/react/predictive-search.mjs +23 -21
- package/dist/development/react/predictive-search.mjs.map +1 -1
- package/dist/development/react/product.d.mts +45 -16
- package/dist/development/react/product.mjs +13 -11
- package/dist/development/react/product.mjs.map +1 -1
- package/dist/development/react/shop-pay.d.mts +15 -0
- package/dist/development/react/shop-pay.mjs +9 -0
- package/dist/development/react/shop-pay.mjs.map +1 -1
- package/dist/development/react/shopify-scripts.d.mts +23 -2
- package/dist/development/react/shopify-scripts.mjs +9 -0
- package/dist/development/react/shopify-scripts.mjs.map +1 -1
- package/dist/development/vue/cart.d.mts +28 -23
- package/dist/development/vue/cart.mjs +42 -25
- package/dist/development/vue/cart.mjs.map +1 -1
- package/dist/development/vue/collection.d.mts +20 -12
- package/dist/development/vue/collection.mjs +15 -7
- package/dist/development/vue/collection.mjs.map +1 -1
- package/dist/development/vue/index.d.mts +2 -2
- package/dist/development/vue/index.mjs +2 -2
- package/dist/development/vue/predictive-search.d.mts +31 -36
- package/dist/development/vue/predictive-search.mjs +16 -22
- package/dist/development/vue/predictive-search.mjs.map +1 -1
- package/dist/development/vue/product.d.mts +16 -15
- package/dist/development/vue/product.mjs +11 -11
- package/dist/development/vue/product.mjs.map +1 -1
- package/dist/development/vue/shop-pay.d.mts +14 -17
- package/dist/development/vue/shop-pay.mjs +8 -0
- package/dist/development/vue/shop-pay.mjs.map +1 -1
- package/dist/development/vue/shopify-scripts.d.mts +11 -0
- package/dist/development/vue/shopify-scripts.mjs +6 -0
- package/dist/development/vue/shopify-scripts.mjs.map +1 -1
- package/dist/globals.d.mts +20 -11
- package/dist/graphql/generated/storefront-api-types.d.mts +127 -1
- package/dist/graphql/graphql.d.mts +17 -16
- package/dist/graphql/graphql.mjs +7 -8
- package/dist/graphql/graphql.mjs.map +1 -1
- package/dist/graphql/type-resolver.d.mts +15 -8
- package/dist/react/cart.d.mts +78 -32
- package/dist/react/cart.mjs +56 -29
- package/dist/react/cart.mjs.map +1 -1
- package/dist/react/collection.d.mts +40 -22
- package/dist/react/collection.mjs +27 -7
- package/dist/react/collection.mjs.map +1 -1
- package/dist/react/predictive-search.d.mts +65 -50
- package/dist/react/predictive-search.mjs +23 -21
- package/dist/react/predictive-search.mjs.map +1 -1
- package/dist/react/product.d.mts +45 -16
- package/dist/react/product.mjs +13 -11
- package/dist/react/product.mjs.map +1 -1
- package/dist/react/shop-pay.d.mts +15 -0
- package/dist/react/shop-pay.mjs +9 -0
- package/dist/react/shop-pay.mjs.map +1 -1
- package/dist/react/shopify-scripts.d.mts +23 -2
- package/dist/react/shopify-scripts.mjs +9 -0
- package/dist/react/shopify-scripts.mjs.map +1 -1
- package/dist/ts-plugin/index.cjs +6 -0
- package/dist/ts-plugin/index.cjs.map +1 -1
- package/dist/ts-plugin/index.d.cts +6 -0
- package/dist/vite/local-https.d.mts +51 -17
- package/dist/vite/local-https.mjs +26 -5
- package/dist/vite/local-https.mjs.map +1 -1
- package/dist/vue/cart.d.mts +28 -23
- package/dist/vue/cart.mjs +42 -25
- package/dist/vue/cart.mjs.map +1 -1
- package/dist/vue/collection.d.mts +20 -12
- package/dist/vue/collection.mjs +15 -7
- package/dist/vue/collection.mjs.map +1 -1
- package/dist/vue/index.d.mts +2 -2
- package/dist/vue/index.mjs +2 -2
- package/dist/vue/predictive-search.d.mts +31 -36
- package/dist/vue/predictive-search.mjs +16 -22
- package/dist/vue/predictive-search.mjs.map +1 -1
- package/dist/vue/product.d.mts +16 -15
- package/dist/vue/product.mjs +11 -11
- package/dist/vue/product.mjs.map +1 -1
- package/dist/vue/shop-pay.d.mts +14 -17
- package/dist/vue/shop-pay.mjs +8 -0
- package/dist/vue/shop-pay.mjs.map +1 -1
- package/dist/vue/shopify-scripts.d.mts +11 -0
- package/dist/vue/shopify-scripts.mjs +6 -0
- package/dist/vue/shopify-scripts.mjs.map +1 -1
- package/package.json +3 -1
- package/skills/hydrogen-analytics/SKILL.md +1 -1
- package/skills/hydrogen-cart-ui/SKILL.md +28 -2
- package/skills/hydrogen-collection-browser/SKILL.md +9 -1
- package/skills/hydrogen-collection-browser/references/nextjs.md +1 -1
- package/skills/hydrogen-collection-browser/references/nuxt.md +1 -1
- package/skills/hydrogen-collection-browser/references/react.md +151 -32
- package/skills/hydrogen-customer-account/SKILL.md +2 -0
- package/skills/hydrogen-request-handlers/SKILL.md +21 -11
- package/skills/hydrogen-setup/references/analytics.md +14 -8
- package/skills/hydrogen-setup/steps/1-inspect-app.md +2 -2
- package/skills/hydrogen-setup/steps/6-product-detail-page.md +21 -11
- package/skills/hydrogen-storefront-client/SKILL.md +1 -1
- package/skills/hydrogen-storefront-client/references/caching.md +2 -0
- package/skills/hydrogen-variant-form/SKILL.md +25 -7
- package/skills/hydrogen-variant-form/references/nextjs.md +39 -4
- package/skills/hydrogen-variant-form/references/nuxt.md +83 -42
- package/skills/hydrogen-variant-form/references/react.md +45 -9
|
@@ -106,8 +106,14 @@ When creating a product detail page, use the app's existing route convention whe
|
|
|
106
106
|
|
|
107
107
|
Before building product UI, check whether this skill has a reference file for the app's framework in `references/`. If one exists, read it and use that framework binding or route pattern first.
|
|
108
108
|
|
|
109
|
+
- `references/react.md` — React Router (`Link`, `useNavigate`, `useLocation`).
|
|
110
|
+
- `references/nextjs.md` — Next.js App Router (`next/link`, `next/navigation`).
|
|
111
|
+
- `references/nuxt.md` — Nuxt (`NuxtLink`, Vue Router).
|
|
112
|
+
|
|
109
113
|
If there is no matching reference, use `createProductFormStore` from `@shopify/hydrogen` directly, subscribe with the framework's reactivity primitive, and own hydration, URL sync, and `destroy()` yourself. Packaged bindings are thin wrappers over this same store — apply every rule and anti-pattern below; do not invent a different contract.
|
|
110
114
|
|
|
115
|
+
For example, in SolidStart or SvelteKit, write one select function that calls `store.selectOption(name, value)` and, when the result is not `invalid`, navigates once with the router (replace the history entry, keep scroll). This function takes the place of the provider `onSelect`. Render existing same-product values as native `<a href>` links. In the link click handler, apply the guard from the rules below, call `event.preventDefault()`, and then call your select function. The router's own link handling skips a click that is already prevented, so your select function is the only client navigation.
|
|
116
|
+
|
|
111
117
|
## Hydration
|
|
112
118
|
|
|
113
119
|
When the product data changes (e.g. after a URL navigation triggers a data refetch), the store must be hydrated with the new product — not recreated. `hydrate(product, opts?)` replaces the product data, clears the decoded variant cache, and recomputes state. The new product's `selectedOrFirstAvailableVariant` takes priority; if absent, falls back to explicitly provided `opts.selectedOptions`, then to the selection that was active before hydration.
|
|
@@ -156,9 +162,12 @@ Pass `style: "variant"` with a resolved `variant` to emit a shareable `?variant=
|
|
|
156
162
|
|
|
157
163
|
### Selection and navigation
|
|
158
164
|
|
|
159
|
-
- **With provider bindings, put URL navigation in the provider `onSelect` callback.** Do not navigate inside each same-product option
|
|
165
|
+
- **With provider bindings, put URL navigation in the provider `onSelect` callback.** Do not navigate inside each same-product option control. Let `register("optionValue", ...)` call `selectOption`; the provider receives the valid selection result and owns URL sync from there.
|
|
160
166
|
- **ALWAYS use URL-based variant selection in URL-routing apps.** When the buyer selects an option, navigate to the URL representing that selection — replacing the history entry and without resetting scroll position. The URL is the durable source of truth — the data loader reads the selection from the URL, queries the Storefront API, and the response hydrates the store when a reload or revalidation happens.
|
|
161
|
-
- **Same-product option values must degrade to GET links (no JS).** In URL-routing apps, render each same-product option value as a real link (the framework's link component or `<a href>`) whose `href` is the option URL for that value, built from `value.selectedOptions` — not a bare `<button onClick>`. The `href` is the no-JS path: with scripting off, activating it issues a GET to the option URL, the loader reads the params, queries the Storefront API, and the server renders the newly selected variant.
|
|
167
|
+
- **Same-product option values must degrade to GET links (no JS).** In URL-routing apps, render each existing same-product option value (`exists: true`) as a real link (the framework's link component or `<a href>`) whose `href` is the option URL for that value, built from `value.selectedOptions` — not a bare `<button onClick>`. The `href` is the no-JS path: with scripting off, activating it issues a GET to the option URL, the loader reads the params, queries the Storefront API, and the server renders the newly selected variant. A button-/`onClick`-only control gives a no-JS shopper nothing to act on, so they stay on the default variant. Non-existent combinations (`exists: false`) have no valid option URL; render those as a disabled `<button>`, not a link. Cross-product values are links too (see Combined listings).
|
|
168
|
+
- **After hydration, the provider `onSelect` is the only client navigation.** Handle the same-product link click yourself. For a plain primary click — not already prevented, `event.button === 0`, no Meta, Ctrl, Shift, or Alt key, and the link `target` is empty or `_self` — call `event.preventDefault()`, then call `onClick()` from `register("optionValue", ...)` (or your select function when you use the store directly). The registered handler calls `selectOption`, and the provider `onSelect` replaces the URL one time. `preventDefault()` stops the link and the framework router from navigating a second time. Do not call `navigate()` yourself in addition to the registered handler. Keep the selection callback safe to repeat, including clicks on already-selected values. Each framework reference shows the click hook to use.
|
|
169
|
+
- **Leave other link clicks native.** Modifier-key clicks, middle clicks, and links with a `target` other than `_self` must keep the browser's link behavior (for example, open the option URL in a new tab). They must not change the selection on the current page. Do not put `download` on option links; a guard on a native `<a>` also returns early when the link has `download`. Do not spread `register("optionValue", ...)` onto a link: the spread `onClick` runs for every click, including modifier-key clicks, and does not stop the link's own navigation.
|
|
170
|
+
- **Use the same query base for the `href` and for the selection callback** (provider `onSelect`, or your select function when you use the store directly). Pass the current URL search params as `base` to `buildProductSelectionSearchParams` in both places. Then the no-JS link and the hydrated navigation go to the same URL, and unrelated params (`?ref=campaign`) stay on both paths.
|
|
162
171
|
- **ALWAYS use the `selectOption` return value for navigation, not a reactive effect on state.** `selectOption` returns the result synchronously. Use the returned `selectedOptions` to construct the next URL immediately. Reacting to derived state (e.g. `selectedOptions` from the store) instead introduces an unnecessary update cycle and risks stale values. Framework bindings may wrap this as an `onSelect` callback — the principle is the same.
|
|
163
172
|
- **Skip data refetch only when the resolved local state is sufficient.** When `selectOption` returns `resolved`, the selected variant is already in the local cache. It is safe to skip revalidation only if the route does not need fresh loader data for other UI. When a complete selection returns `unresolved`, fetch or otherwise resolve the exact variant before treating the selection as complete.
|
|
164
173
|
- **NEVER call `selectOption` for combined-listing cross-product values.** When `value.handle !== product.handle`, the value belongs to a different product. Render it as a navigation element (anchor or link component) that navigates to the other product's URL — not a button that calls `selectOption`. The store only knows about the current product's variant matrix.
|
|
@@ -167,6 +176,7 @@ Pass `style: "variant"` with a resolved `variant` to emit a shareable `?variant=
|
|
|
167
176
|
|
|
168
177
|
- **ALWAYS check `value.handle` against the current `product.handle`.** If they differ, the option value points to a different product in a combined listing. The UI must navigate to that product (full page navigation or a link component), not call `selectOption`.
|
|
169
178
|
- **Use the framework's client-side link component for cross-product values when one exists.** Use the app's idiomatic navigation primitive. Use a raw `<a>` only when that is the app's established routing convention.
|
|
179
|
+
- **Keep focus on the activated value across a cross-product navigation.** After the switch, the activated value is a same-product value on the new product page. Render same-product and cross-product values with the same element type, so a framework that keeps the route component mounted across a param change (React Router) reconciles the node in place and focus stays on it. Where the framework remounts the page on a handle change (Next.js App Router keys the `[handle]` segment by its value), record the activated value before navigating and focus its same-product link as the new page mounts. Otherwise focus drops to `<body>`, and keyboard and screen-reader users lose their place and never hear the new selection.
|
|
170
180
|
- **Preserve non-option query params on combined-listing links.** When constructing the URL for a combined-listing link, carry forward existing search params (e.g. `?ref=campaign`) and replace only the option params. Use `buildProductSelectionSearchParams` with the current product's option names as `optionNames` — it deletes all option params (and any stale `variant` param) first, then sets the new ones, preventing stale params when combined-listing products have different option names.
|
|
171
181
|
- **Ignore selected options that are not in the current product option matrix.** Divergent combined-listing child products can have different option names. A stale `Color=Black` param must not constrain a child product whose options are `Size` and `Mount`.
|
|
172
182
|
|
|
@@ -204,11 +214,11 @@ Pass `style: "variant"` with a resolved `variant` to emit a shareable `?variant=
|
|
|
204
214
|
|
|
205
215
|
1. **Pre-selected variant** — When the product has a `selectedOrFirstAvailableVariant`, the corresponding option values are marked as selected on first render. The variant's price and details are displayed. `selectedVariant` on the state is non-null.
|
|
206
216
|
2. **No pre-selected variant** — When `selectedOrFirstAvailableVariant` is `null`, no option values are selected. `selectedVariant` is `null`. The add-to-cart button shows "Select options" (or equivalent) and is disabled.
|
|
207
|
-
3. **URL-driven selection** — When the URL encodes a variant selection (e.g. via option params like `?Color=Blue&Size=Small`), the data loader resolves the selection and passes it to the Storefront API query. The returned `selectedOrFirstAvailableVariant` reflects the URL selection, and the store initializes with those options selected.
|
|
217
|
+
3. **URL-driven selection** — When the URL encodes a variant selection (e.g. via option params like `?Color=Blue&Size=Small`), the data loader resolves the selection and passes it to the Storefront API query. The returned `selectedOrFirstAvailableVariant` reflects the URL selection, and the store initializes with those options selected. The page shows the shopper which option values are selected (visible styling or text), in addition to `aria-current` or `aria-pressed`.
|
|
208
218
|
|
|
209
219
|
### Option selection
|
|
210
220
|
|
|
211
|
-
4. **Select a value** — Click an option value
|
|
221
|
+
4. **Select a value** — Click an option value. The value becomes selected (visually indicated). If the selection resolves to a variant, the price and variant details update immediately. `selectedVariant` updates on the state. In URL-routing apps, the URL updates to reflect the new selection without a scroll reset.
|
|
212
222
|
5. **Multi-option selection** — On a product with Size and Color options, select Size=Large then Color=Blue. Both selections are reflected in the state. The resolved variant matches Large/Blue.
|
|
213
223
|
6. **Symmetric availability** — On a product where Medium/Olive is unavailable but Medium/Green is available, selecting Color=Olive marks Size=Medium as `available: false`; selecting Color=Green marks Size=Medium as `available: true`.
|
|
214
224
|
7. **Switch within an option** — With Color=Red selected, click Color=Blue. The selection switches; Red is deselected, Blue is selected. Only one value per option is selected at a time.
|
|
@@ -218,8 +228,8 @@ Pass `style: "variant"` with a resolved `variant` to emit a shareable `?variant=
|
|
|
218
228
|
|
|
219
229
|
### Combined listings
|
|
220
230
|
|
|
221
|
-
11. **Cross-product value** — An option value where `value.handle !== product.handle` renders as a navigation element (anchor or link component), not a button. Clicking it navigates to the other product's page with the appropriate option params.
|
|
222
|
-
12. **Same-product value** — An option value where `value.handle === product.handle` renders as a GET link to its option URL
|
|
231
|
+
11. **Cross-product value** — An option value where `value.handle !== product.handle` renders as a navigation element (anchor or link component), not a button. Clicking it navigates to the other product's page with the appropriate option params. After the navigation, focus is on the activated value's link on the new product page and the scroll position is unchanged.
|
|
232
|
+
12. **Same-product value** — An option value where `value.handle === product.handle` renders as a GET link to its option URL enhanced by the `register("optionValue", ...)` click handler. With JavaScript disabled the link navigates and the server resolves the variant. Hydrated, a plain click calls `selectOption`, the provider syncs the URL client-side, and the link's own navigation does not run, so a resolved selection issues no loader request. A Cmd- or Ctrl-click opens the option URL in a new tab and leaves the current page's selection unchanged.
|
|
223
233
|
13. **Preserved params** — When navigating via a combined-listing link, non-option query params from the current URL are preserved in the destination URL.
|
|
224
234
|
|
|
225
235
|
### Hydration
|
|
@@ -261,12 +271,20 @@ Pass `style: "variant"` with a resolved `variant` to emit a shareable `?variant=
|
|
|
261
271
|
|
|
262
272
|
34. **Reset to initial state** — Calling `reset()` restores the store to the product and selected options it was created with. All user selections are discarded.
|
|
263
273
|
|
|
274
|
+
### Progressive links
|
|
275
|
+
|
|
276
|
+
35. **No JavaScript** — With JavaScript disabled, activating an existing option link issues a GET to its option URL, and the server renders that selection. If the app cannot render the product page without JavaScript, record that path as not available. Do not record it as a pass.
|
|
277
|
+
36. **One navigation** — Hydrated, a plain click on an existing same-product option link causes exactly one client navigation. That navigation replaces the history entry, keeps scroll, and does not reload the page. The selection updates.
|
|
278
|
+
37. **Unrelated params kept** — With `?ref=campaign` in the URL, option link `href` values and the URL after a hydrated selection both keep `ref=campaign`.
|
|
279
|
+
38. **Modifier and new-tab clicks** — A Cmd/Ctrl click, a middle click, or a link with `target="_blank"` opens the option URL natively. The current page's selection and URL do not change.
|
|
280
|
+
|
|
264
281
|
---
|
|
265
282
|
|
|
266
283
|
## Anti-patterns
|
|
267
284
|
|
|
268
285
|
- **Using reactive effects on state to sync selection to URL.** The `selectOption` return value provides the selection result synchronously. Reacting to `state.selectedOptions` instead introduces an extra update cycle and can fire with stale values.
|
|
269
|
-
- **Navigating inside same-product option
|
|
286
|
+
- **Navigating inside same-product option controls.** Do not call `navigate()` in an option click handler and then also call the registered handler. This bypasses the provider `onSelect` contract and can navigate from stale or invalid data. A guarded link handler that calls `event.preventDefault()` and then only `registered.onClick()` is correct.
|
|
287
|
+
- **Spreading `register("optionValue", ...)` onto a link unguarded.** The link runs the registered handler and then its own navigation. The second navigation refetches the loader a resolved selection skipped, and a Cmd- or Ctrl-click both opens a new tab and changes the current page. Run the handler only for a plain primary click and cancel the link's navigation.
|
|
270
288
|
- **Button-/onClick-only same-product option values.** A same-product option value rendered as `<button onClick={selectOption}>` with no `href` is dead without JavaScript — a no-JS shopper cannot switch variants and is stuck on the default. Render it as a GET link to the option URL and let the registered handler enhance it; the `href` is the progressive-enhancement fallback.
|
|
271
289
|
- **Using raw anchors when the framework has a client-side link component.** Raw `<a>` tags lose client-router behavior such as scroll preservation, pending navigation state, prefetching, and route transitions. Use the app's established link component unless raw anchors are the framework convention.
|
|
272
290
|
- **Putting variant selection inside the add-to-cart form.** Variant selection is button/link interactions that update store state. The add-to-cart form submits `merchandiseId` and `quantity` to the cart. Mixing them creates ambiguous form semantics and breaks progressive enhancement.
|
|
@@ -5,6 +5,7 @@
|
|
|
5
5
|
- Server Page
|
|
6
6
|
- Client Details Component
|
|
7
7
|
- Same-Product And Cross-Product Values
|
|
8
|
+
- No-JavaScript Limits
|
|
8
9
|
- Add To Cart
|
|
9
10
|
|
|
10
11
|
Product data is fetched in the server page. Variant selection and add-to-cart live in a `"use client"` component because they use `ProductProvider`, browser routing, and cart forms.
|
|
@@ -146,23 +147,30 @@ Wrap this tree in the app's `CartProvider` from `hydrogen-cart-ui`; `ProductProv
|
|
|
146
147
|
|
|
147
148
|
## Same-Product And Cross-Product Values
|
|
148
149
|
|
|
149
|
-
Render same-product option values as GET links (`next/link`) so variant selection degrades without JavaScript (the skill's GET-links
|
|
150
|
+
Render existing same-product option values as GET links (`next/link`) so variant selection degrades without JavaScript (see the skill's GET-links and Accessibility rules for the no-JS fallback and `aria-current`, and its "provider `onSelect` is the only client navigation" rule for `onSelect`). The `href` is the option URL built from `value.selectedOptions`, with the same `searchParams` base as the provider `onSelect`. Do not spread `register("optionValue", ...)` onto `Link`. Use `onNavigate` instead: call `event.preventDefault()` to cancel the `Link` navigation, then call `registered.onClick()` so the provider `onSelect` is the only navigation. Next.js does not call `onNavigate` for modifier-key clicks, a `target` other than `_self`, or links with `download`, so those clicks stay native. Cancel navigation and call the registered handler only on same-product values; cross-product links can use `onNavigate` to record focus as shown below. Keep sold-out-but-existing values interactive and derive their visual treatment from `value.available`. Render non-existent combinations (`exists: false`) as a disabled `<button>` instead of a link.
|
|
150
151
|
|
|
151
152
|
```tsx
|
|
153
|
+
const searchParams = useSearchParams();
|
|
154
|
+
|
|
152
155
|
<Link
|
|
153
156
|
href={variantUrl(product, value.selectedOptions, value.handle, searchParams)}
|
|
154
|
-
replace
|
|
155
157
|
scroll={false}
|
|
158
|
+
ref={(node) => focusIfPending(node, product.handle, option.name, value.name)}
|
|
156
159
|
aria-current={value.selected ? "true" : undefined}
|
|
157
160
|
data-available={value.available ? "true" : "false"}
|
|
158
|
-
{
|
|
161
|
+
onNavigate={(event) => {
|
|
162
|
+
// The provider owns URL sync; stop Link from navigating a second time.
|
|
163
|
+
event.preventDefault();
|
|
164
|
+
const registered = register("optionValue", { optionName: option.name, value: value.name });
|
|
165
|
+
registered.onClick();
|
|
166
|
+
}}
|
|
159
167
|
>
|
|
160
168
|
{value.name}
|
|
161
169
|
{!value.available ? <span className="sr-only"> (Sold out)</span> : null}
|
|
162
170
|
</Link>
|
|
163
171
|
```
|
|
164
172
|
|
|
165
|
-
Cross-product combined-listing values point at a different `value.handle` and navigate to that product.
|
|
173
|
+
Cross-product combined-listing values point at a different `value.handle` and navigate to that product. Render them as a normal `next/link` with `scroll={false}`. Use `onNavigate` only to record focus for the new page, not to select an option or cancel navigation. Both link types use the same URL helper:
|
|
166
174
|
|
|
167
175
|
```tsx
|
|
168
176
|
import { buildProductSelectionSearchParams, type SelectedOption } from "@shopify/hydrogen";
|
|
@@ -183,6 +191,33 @@ function variantUrl(
|
|
|
183
191
|
}
|
|
184
192
|
```
|
|
185
193
|
|
|
194
|
+
Next.js keys the `[handle]` segment by its value, so a cross-product navigation unmounts the product component and the focused link with it, and `scroll={false}` also turns off the router's own focus handling. Keep focus on the activated value by recording it in module state from the cross-product link's `onNavigate`, which does not fire for a Cmd-click, and focusing its same-product link through a callback `ref` as the new page mounts. Module state outlives the remount. A ref or `useState` inside the component does not.
|
|
195
|
+
|
|
196
|
+
```tsx
|
|
197
|
+
let pendingFocus: { handle: string; optionName: string; value: string } | null = null;
|
|
198
|
+
|
|
199
|
+
function focusIfPending(node: HTMLAnchorElement | null, handle: string, optionName: string, value: string) {
|
|
200
|
+
if (!node || pendingFocus?.handle !== handle || pendingFocus.optionName !== optionName || pendingFocus.value !== value) return;
|
|
201
|
+
pendingFocus = null;
|
|
202
|
+
node.focus();
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
<Link
|
|
206
|
+
href={variantUrl(product, value.selectedOptions, value.handle, searchParams)}
|
|
207
|
+
scroll={false}
|
|
208
|
+
onNavigate={() => {
|
|
209
|
+
pendingFocus = { handle: value.handle, optionName: option.name, value: value.name };
|
|
210
|
+
}}
|
|
211
|
+
data-available={value.available ? "true" : "false"}
|
|
212
|
+
>
|
|
213
|
+
{value.name}
|
|
214
|
+
</Link>
|
|
215
|
+
```
|
|
216
|
+
|
|
217
|
+
## No-JavaScript Limits
|
|
218
|
+
|
|
219
|
+
A valid option `href` does not by itself make the product page render without JavaScript. When product content is inside a `<Suspense>` boundary that streams after the first HTML (for example, a root layout that wraps a dynamic app shell in `<Suspense>`), React reveals the streamed content with an inline script. With JavaScript disabled, the shopper can see only the fallback, even though the server resolved the selected variant. Test the no-JS path in your app. If the product page does not render without JavaScript, record that limit; do not report the no-JS option links as verified.
|
|
220
|
+
|
|
186
221
|
## Add To Cart
|
|
187
222
|
|
|
188
223
|
Use the local `hydrogen-shop-pay` skill when adding Shop Pay. Use the local `hydrogen-money` skill for prices.
|
|
@@ -20,19 +20,23 @@ Wrap this tree in the app's `CartProvider` from `hydrogen-cart-ui`; `ProductProv
|
|
|
20
20
|
|
|
21
21
|
In `pages/products/[handle].vue`, pass selected options from `getSelectedProductOptions({searchParams})` into the Storefront API query. Use one reusable variant fragment for `firstSelectableVariant`, `selectedOrFirstAvailableVariant`, and `adjacentVariants`; it must include variant `price`, `availableForSale`, `selectedOptions`, and `product { handle title }` so price display and combined-listing navigation work. Use the injected server/client Storefront client from the Nuxt storefront-client recipe.
|
|
22
22
|
|
|
23
|
-
Wrap the UI
|
|
23
|
+
Wrap the UI. The provider `onSelect` is the only client navigation for same-product selection; use `router.replace` from `useRouter()`:
|
|
24
24
|
|
|
25
25
|
```vue
|
|
26
|
+
<script setup lang="ts">
|
|
27
|
+
import type { ValidProductSelectionResult } from "@shopify/hydrogen/vue";
|
|
28
|
+
import { ProductProvider } from "~/storefront/product";
|
|
29
|
+
import type { ProductData } from "~/storefront/product-types";
|
|
30
|
+
|
|
31
|
+
const router = useRouter();
|
|
32
|
+
|
|
33
|
+
function handleSelect(result: ValidProductSelectionResult<ProductData>) {
|
|
34
|
+
router.replace(variantRoute(result.selectedOptions, result.selectedVariant?.product?.handle));
|
|
35
|
+
}
|
|
36
|
+
</script>
|
|
37
|
+
|
|
26
38
|
<template>
|
|
27
|
-
<ProductProvider
|
|
28
|
-
:product="product"
|
|
29
|
-
:on-select="
|
|
30
|
-
(result) =>
|
|
31
|
-
navigateTo(variantRoute(result.selectedOptions, result.selectedVariant?.product?.handle), {
|
|
32
|
-
replace: true,
|
|
33
|
-
})
|
|
34
|
-
"
|
|
35
|
-
>
|
|
39
|
+
<ProductProvider :product="product" :on-select="handleSelect">
|
|
36
40
|
<ProductVariantSelector :product="product" />
|
|
37
41
|
<ProductAddToCart :product="product" />
|
|
38
42
|
</ProductProvider>
|
|
@@ -41,42 +45,79 @@ Wrap the UI:
|
|
|
41
45
|
|
|
42
46
|
## Variant Selector
|
|
43
47
|
|
|
44
|
-
Same-product option values are GET links
|
|
48
|
+
Same-product option values are GET links so selection degrades without JavaScript; non-existent combinations render as a disabled `<button>`, and cross-product combined-listing values are normal `NuxtLink` links. See the skill's GET-links and Accessibility rules for the no-JS fallback and `aria-current`, and its "provider `onSelect` is the only client navigation" rule for `onSelect`.
|
|
49
|
+
|
|
50
|
+
For existing same-product values, use `NuxtLink` with `custom` and render the anchor yourself from the slot `href`. Put a guarded `@click` on that anchor: for a plain primary click it calls `event.preventDefault()` and then `onClick()` from `form.register("optionValue", ...)`, so the provider `onSelect` is the only navigation. Other clicks keep native link behavior. Do not call the slot `navigate`. Do not put `@click` on a regular `NuxtLink` to cancel its navigation, and do not `v-bind` the registration onto it; the order of your listener and the link's own navigation is not something to rely on.
|
|
51
|
+
|
|
52
|
+
This custom-link guard has not been verified in a running Nuxt storefront. Check plain-click, modified-click, and no-JavaScript behavior in your app before treating this path as verified.
|
|
45
53
|
|
|
46
54
|
```vue
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
>
|
|
74
|
-
|
|
75
|
-
<
|
|
76
|
-
|
|
55
|
+
<script setup lang="ts">
|
|
56
|
+
const form = useProductForm();
|
|
57
|
+
|
|
58
|
+
function handleOptionClick(event: MouseEvent, optionName: string, value: string) {
|
|
59
|
+
const anchor = event.currentTarget;
|
|
60
|
+
if (
|
|
61
|
+
event.defaultPrevented ||
|
|
62
|
+
event.button !== 0 ||
|
|
63
|
+
event.metaKey ||
|
|
64
|
+
event.ctrlKey ||
|
|
65
|
+
event.shiftKey ||
|
|
66
|
+
event.altKey ||
|
|
67
|
+
!(anchor instanceof HTMLAnchorElement) ||
|
|
68
|
+
(anchor.target !== "" && anchor.target.toLowerCase() !== "_self") ||
|
|
69
|
+
anchor.hasAttribute("download")
|
|
70
|
+
) {
|
|
71
|
+
return;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
// The provider owns URL sync; stop the anchor from navigating too.
|
|
75
|
+
event.preventDefault();
|
|
76
|
+
const registered = form.register("optionValue", { optionName, value });
|
|
77
|
+
registered.onClick();
|
|
78
|
+
}
|
|
79
|
+
</script>
|
|
80
|
+
|
|
81
|
+
<template>
|
|
82
|
+
<!-- cross-product: navigate to a different product -->
|
|
83
|
+
<NuxtLink
|
|
84
|
+
v-if="value.handle !== product.handle"
|
|
85
|
+
:to="variantRoute(value.selectedOptions, value.handle)"
|
|
86
|
+
replace
|
|
87
|
+
>
|
|
88
|
+
{{ value.name }}
|
|
89
|
+
</NuxtLink>
|
|
90
|
+
|
|
91
|
+
<!-- same-product, non-existent combination: no option URL to degrade to -->
|
|
92
|
+
<button
|
|
93
|
+
v-else-if="!value.exists"
|
|
94
|
+
type="button"
|
|
95
|
+
disabled
|
|
96
|
+
:aria-pressed="value.selected"
|
|
97
|
+
>
|
|
98
|
+
{{ value.name }}
|
|
99
|
+
</button>
|
|
100
|
+
|
|
101
|
+
<!-- same-product, existing value: GET link with a guarded click -->
|
|
102
|
+
<NuxtLink
|
|
103
|
+
v-else
|
|
104
|
+
v-slot="{ href }"
|
|
105
|
+
:to="variantRoute(value.selectedOptions)"
|
|
106
|
+
custom
|
|
107
|
+
>
|
|
108
|
+
<a
|
|
109
|
+
:href="href"
|
|
110
|
+
:aria-current="value.selected ? 'true' : undefined"
|
|
111
|
+
@click="handleOptionClick($event, option.name, value.name)"
|
|
112
|
+
>
|
|
113
|
+
{{ value.name }}
|
|
114
|
+
<template v-if="!value.available"> - Sold out</template>
|
|
115
|
+
</a>
|
|
116
|
+
</NuxtLink>
|
|
117
|
+
</template>
|
|
77
118
|
```
|
|
78
119
|
|
|
79
|
-
Build
|
|
120
|
+
`variantRoute(selectedOptions, handle)` returns `{ path, query }`. Build the query with `buildProductSelectionSearchParams`, passing the product option names as `optionNames`. Convert Nuxt's current route query to `URLSearchParams` for `base` by appending every scalar or array value, then convert the returned params back to a Nuxt query object while preserving repeated keys. Use the current route query as `base` both for link `href` values and for `onSelect`. This preserves non-option params while removing stale option params and the reserved `variant` param.
|
|
80
121
|
|
|
81
122
|
## Add To Cart
|
|
82
123
|
|
|
@@ -1,4 +1,6 @@
|
|
|
1
|
-
# React Product Bindings
|
|
1
|
+
# React Router Product Bindings
|
|
2
|
+
|
|
3
|
+
This reference uses React Router (`Link`, `useNavigate`, `useLocation`). For Next.js App Router, read `nextjs.md`.
|
|
2
4
|
|
|
3
5
|
Create typed React product bindings once from `@shopify/hydrogen/react`, usually in a shared product module:
|
|
4
6
|
|
|
@@ -10,19 +12,29 @@ export const { ProductProvider, useProductForm } =
|
|
|
10
12
|
createProductComponents<ProductData>();
|
|
11
13
|
```
|
|
12
14
|
|
|
13
|
-
Use the provider's `onSelect` callback for same-product URL sync
|
|
15
|
+
Use the provider's `onSelect` callback for same-product URL sync. Pass the current search params as the `variantUrl` base, so the URL keeps unrelated params and matches the option link `to`:
|
|
14
16
|
|
|
15
17
|
```tsx
|
|
18
|
+
const navigate = useNavigate();
|
|
19
|
+
const location = useLocation();
|
|
20
|
+
|
|
16
21
|
<ProductProvider
|
|
17
22
|
product={product}
|
|
18
23
|
onSelect={(result) => {
|
|
19
24
|
void navigate(
|
|
20
25
|
toRouterLocation(
|
|
21
|
-
variantUrl(
|
|
26
|
+
variantUrl(
|
|
27
|
+
product,
|
|
28
|
+
result.selectedOptions,
|
|
29
|
+
result.selectedVariant?.product?.handle,
|
|
30
|
+
new URLSearchParams(location.search),
|
|
31
|
+
),
|
|
22
32
|
),
|
|
23
33
|
{
|
|
24
34
|
replace: true,
|
|
25
35
|
preventScrollReset: true,
|
|
36
|
+
// Optional: skip loader revalidation when the variant resolved locally.
|
|
37
|
+
...(result.status === "resolved" ? { defaultShouldRevalidate: false } : {}),
|
|
26
38
|
},
|
|
27
39
|
);
|
|
28
40
|
}}
|
|
@@ -31,18 +43,42 @@ Use the provider's `onSelect` callback for same-product URL sync:
|
|
|
31
43
|
</ProductProvider>
|
|
32
44
|
```
|
|
33
45
|
|
|
34
|
-
|
|
46
|
+
`variantUrl(product, selectedOptions, handle, base)` calls `buildProductSelectionSearchParams` with the product option names and `base`, then returns `/products/{handle}?{params}`; `handle` defaults to `product.handle`.
|
|
47
|
+
|
|
48
|
+
`defaultShouldRevalidate: false` is optional. Use it only for `resolved` selections, only when no other UI on the route needs fresh loader data, and only if the installed React Router supports it. Leave it out for `unresolved` selections, so the loader resolves the exact variant. A route `shouldRevalidate` export still makes the final decision.
|
|
35
49
|
|
|
36
|
-
Same-product option values are GET links so selection works without JavaScript
|
|
50
|
+
Same-product option values are GET links so selection works without JavaScript. The `to` is the option URL built from `value.selectedOptions` with the same base as `onSelect`. Do not spread the registration onto `Link`. Pass a guarded `onClick` instead: for a plain primary click it calls `event.preventDefault()` and then `registered.onClick()`. React Router's `Link` does not navigate when the event is already prevented, so the provider `onSelect` is the only navigation. Other clicks keep native link behavior. Keep sold-out-but-existing values interactive and derive their visual treatment from `value.available`:
|
|
37
51
|
|
|
38
52
|
```tsx
|
|
53
|
+
const location = useLocation();
|
|
54
|
+
const baseParams = useMemo(() => new URLSearchParams(location.search), [location.search]);
|
|
55
|
+
|
|
56
|
+
const registered = register("optionValue", { optionName: option.name, value: value.name });
|
|
57
|
+
const onSelectLink = (event: MouseEvent<HTMLAnchorElement>) => {
|
|
58
|
+
if (
|
|
59
|
+
event.defaultPrevented ||
|
|
60
|
+
event.button !== 0 ||
|
|
61
|
+
event.metaKey ||
|
|
62
|
+
event.altKey ||
|
|
63
|
+
event.ctrlKey ||
|
|
64
|
+
event.shiftKey ||
|
|
65
|
+
(event.currentTarget.target && event.currentTarget.target !== "_self")
|
|
66
|
+
) {
|
|
67
|
+
return;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
// The provider owns URL sync; stop Link from navigating a second time.
|
|
71
|
+
event.preventDefault();
|
|
72
|
+
registered.onClick();
|
|
73
|
+
};
|
|
74
|
+
|
|
39
75
|
<Link
|
|
40
|
-
to={toRouterLocation(variantUrl(product, value.selectedOptions, value.handle))}
|
|
76
|
+
to={toRouterLocation(variantUrl(product, value.selectedOptions, value.handle, baseParams))}
|
|
41
77
|
replace
|
|
42
78
|
preventScrollReset
|
|
43
79
|
aria-current={value.selected ? "true" : undefined}
|
|
44
80
|
data-available={value.available ? "true" : "false"}
|
|
45
|
-
{
|
|
81
|
+
onClick={onSelectLink}
|
|
46
82
|
>
|
|
47
83
|
{value.name}
|
|
48
84
|
{!value.available ? <span className="sr-only"> (Sold out)</span> : null}
|
|
@@ -57,11 +93,11 @@ Non-existent combinations (`exists: false`) render as a disabled `<button>` inst
|
|
|
57
93
|
</button>
|
|
58
94
|
```
|
|
59
95
|
|
|
60
|
-
Cross-product option values are framework links that reuse the same URL helper
|
|
96
|
+
Cross-product option values are framework links that reuse the same URL helper and base. They do not call the registered handler. Rendering them as `<Link>` like the same-product values is what keeps focus across the switch. React Router keeps the route component mounted when only `:handle` changes, so React reconciles the activated link in place. A cross-product `<Link>` next to a same-product `<button>` would change element type on the activated control after the navigation, React would remount it, and focus would drop to `<body>`.
|
|
61
97
|
|
|
62
98
|
```tsx
|
|
63
99
|
<Link
|
|
64
|
-
to={toRouterLocation(variantUrl(product, value.selectedOptions, value.handle))}
|
|
100
|
+
to={toRouterLocation(variantUrl(product, value.selectedOptions, value.handle, baseParams))}
|
|
65
101
|
preventScrollReset
|
|
66
102
|
data-available={value.available ? "true" : "false"}
|
|
67
103
|
>
|