Skip to content

Commit

Permalink
Support 2k variant, combined listing usage with getProductOptions (#2659
Browse files Browse the repository at this point in the history
)
  • Loading branch information
wizardlyhel authored Dec 10, 2024
1 parent 8f64915 commit a57d526
Show file tree
Hide file tree
Showing 50 changed files with 5,318 additions and 2,355 deletions.
754 changes: 754 additions & 0 deletions .changeset/lemon-beans-drum.md

Large diffs are not rendered by default.

6 changes: 6 additions & 0 deletions .changeset/three-cows-shave.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
---
'@shopify/hydrogen-react': patch
'@shopify/hydrogen': patch
---

Introduce `getProductOptions`, `getAdjacentAndFirstAvailableVariants`, `useSelectedOptionInUrlParam`, and `mapSelectedProductOptionToObject` to support combined listing products and products with 2000 variants limit.
Original file line number Diff line number Diff line change
Expand Up @@ -114,8 +114,7 @@ export default function Collection() {
* }}
*/
function ProductItem({product, loading}) {
const variant = product.variants.nodes[0];
const variantUrl = useVariantUrl(product.handle, variant.selectedOptions);
const variantUrl = useVariantUrl(product.handle);
return (
<Link
className="product-item"
Expand Down Expand Up @@ -164,14 +163,6 @@ const PRODUCT_ITEM_FRAGMENT = `#graphql
...MoneyProductItem
}
}
variants(first: 1) {
nodes {
selectedOptions {
name
value
}
}
}
}
`;

Expand Down
157 changes: 43 additions & 114 deletions docs/shopify-dev/analytics-setup/js/app/routes/products.$handle.jsx
Original file line number Diff line number Diff line change
@@ -1,12 +1,14 @@
import {Suspense} from 'react';
import {defer, redirect} from '@shopify/remix-oxygen';
import {Await, useLoaderData} from '@remix-run/react';
import {defer} from '@shopify/remix-oxygen';
import {useLoaderData} from '@remix-run/react';
import {
getSelectedProductOptions,
// [START import]
Analytics,
// [END import]
useOptimisticVariant,
getProductOptions,
getAdjacentAndFirstAvailableVariants,
useSelectedOptionInUrlParam,
} from '@shopify/hydrogen';
import {getVariantUrl} from '~/lib/variants';
import {ProductPrice} from '~/components/ProductPrice';
Expand Down Expand Up @@ -57,23 +59,6 @@ async function loadCriticalData({context, params, request}) {
throw new Response(null, {status: 404});
}

const firstVariant = product.variants.nodes[0];
const firstVariantIsDefault = Boolean(
firstVariant.selectedOptions.find(
(option) => option.name === 'Title' && option.value === 'Default Title',
),
);

if (firstVariantIsDefault) {
product.selectedVariant = firstVariant;
} else {
// if no selected variant was returned from the selected options,
// we redirect to the first variant's url with it's selected options applied
if (!product.selectedVariant) {
throw redirectToFirstVariant({product, request});
}
}

return {
product,
};
Expand All @@ -86,57 +71,32 @@ async function loadCriticalData({context, params, request}) {
* @param {LoaderFunctionArgs}
*/
function loadDeferredData({context, params}) {
// In order to show which variants are available in the UI, we need to query
// all of them. But there might be a *lot*, so instead separate the variants
// into it's own separate query that is deferred. So there's a brief moment
// where variant options might show as available when they're not, but after
// this deffered query resolves, the UI will update.
const variants = context.storefront
.query(VARIANTS_QUERY, {
variables: {handle: params.handle},
})
.catch((error) => {
// Log query errors, but don't throw them so the page can still render
console.error(error);
return null;
});
// Put any API calls that is not critical to be available on first page render
// For example: product reviews, product recommendations, social feeds.

return {
variants,
};
}

/**
* @param {{
* product: ProductFragment;
* request: Request;
* }}
*/
function redirectToFirstVariant({product, request}) {
const url = new URL(request.url);
const firstVariant = product.variants.nodes[0];

return redirect(
getVariantUrl({
pathname: url.pathname,
handle: product.handle,
selectedOptions: firstVariant.selectedOptions,
searchParams: new URLSearchParams(url.search),
}),
{
status: 302,
},
);
return {};
}

export default function Product() {
/** @type {LoaderReturnData} */
const {product, variants} = useLoaderData();
const {product} = useLoaderData();

// Optimistically selects a variant with given available variant information
const selectedVariant = useOptimisticVariant(
product.selectedVariant,
variants,
product.selectedOrFirstAvailableVariant,
getAdjacentAndFirstAvailableVariants(product),
);

// Sets the search param to the selected variant without navigation
// only when no search params are set in the url
useSelectedOptionInUrlParam(selectedVariant.selectedOptions);

// Get the product options array
const productOptions = getProductOptions({
...product,
selectedOrFirstAvailableVariant: selectedVariant,
});

const {title, descriptionHtml} = product;

return (
Expand All @@ -149,28 +109,10 @@ export default function Product() {
compareAtPrice={selectedVariant?.compareAtPrice}
/>
<br />
<Suspense
fallback={
<ProductForm
product={product}
selectedVariant={selectedVariant}
variants={[]}
/>
}
>
<Await
errorElement="There was a problem loading product variants"
resolve={variants}
>
{(data) => (
<ProductForm
product={product}
selectedVariant={selectedVariant}
variants={data?.product?.variants.nodes || []}
/>
)}
</Await>
</Suspense>
<ProductForm
productOptions={productOptions}
selectedVariant={selectedVariant}
/>
<br />
<br />
<p>
Expand Down Expand Up @@ -246,19 +188,30 @@ const PRODUCT_FRAGMENT = `#graphql
handle
descriptionHtml
description
encodedVariantExistence
encodedVariantAvailability
options {
name
optionValues {
name
firstSelectableVariant {
...ProductVariant
}
swatch {
color
image {
previewImage {
url
}
}
}
}
}
selectedVariant: variantBySelectedOptions(selectedOptions: $selectedOptions, ignoreUnknownOptions: true, caseInsensitiveMatch: true) {
selectedOrFirstAvailableVariant(selectedOptions: $selectedOptions, ignoreUnknownOptions: true, caseInsensitiveMatch: true) {
...ProductVariant
}
variants(first: 1) {
nodes {
...ProductVariant
}
adjacentVariants (selectedOptions: $selectedOptions) {
...ProductVariant
}
seo {
description
Expand All @@ -282,30 +235,6 @@ const PRODUCT_QUERY = `#graphql
${PRODUCT_FRAGMENT}
`;

const PRODUCT_VARIANTS_FRAGMENT = `#graphql
fragment ProductVariants on Product {
variants(first: 250) {
nodes {
...ProductVariant
}
}
}
${PRODUCT_VARIANT_FRAGMENT}
`;

const VARIANTS_QUERY = `#graphql
${PRODUCT_VARIANTS_FRAGMENT}
query ProductVariants(
$country: CountryCode
$language: LanguageCode
$handle: String!
) @inContext(country: $country, language: $language) {
product(handle: $handle) {
...ProductVariants
}
}
`;

/** @typedef {import('@shopify/remix-oxygen').LoaderFunctionArgs} LoaderFunctionArgs */
/** @template T @typedef {import('@remix-run/react').MetaFunction<T>} MetaFunction */
/** @typedef {import('storefrontapi.generated').ProductFragment} ProductFragment */
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -110,8 +110,7 @@ function ProductItem({
product: ProductItemFragment;
loading?: 'eager' | 'lazy';
}) {
const variant = product.variants.nodes[0];
const variantUrl = useVariantUrl(product.handle, variant.selectedOptions);
const variantUrl = useVariantUrl(product.handle);
return (
<Link
className="product-item"
Expand Down Expand Up @@ -160,14 +159,6 @@ const PRODUCT_ITEM_FRAGMENT = `#graphql
...MoneyProductItem
}
}
variants(first: 1) {
nodes {
selectedOptions {
name
value
}
}
}
}
` as const;

Expand Down
Loading

0 comments on commit a57d526

Please sign in to comment.