Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Support 2k variant, combined listing usage with getProductOptions #2659

Merged
merged 24 commits into from
Dec 10, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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
Loading