Skip to content

Commit d1bc804

Browse files
authored
Refactor ProductItem and fix fragment collision in Skeleton (#2872)
* Fix ProductItem fragment name collision * Use CollectionItemFragment * Refactor ProductItem for reusability * Cleanup * Cleanup * Add changeset * Implement ProductItem in RecommendedProducts
1 parent 61ddf92 commit d1bc804

File tree

6 files changed

+102
-123
lines changed

6 files changed

+102
-123
lines changed

.changeset/twelve-goats-fetch.md

+7
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
---
2+
"skeleton": patch
3+
"@shopify/cli-hydrogen": patch
4+
"@shopify/create-hydrogen": patch
5+
---
6+
7+
Refactor ProductItem into a separate component
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,44 @@
1+
import {Link} from '@remix-run/react';
2+
import {Image, Money} from '@shopify/hydrogen';
3+
import type {
4+
ProductItemFragment,
5+
CollectionItemFragment,
6+
RecommendedProductFragment,
7+
} from 'storefrontapi.generated';
8+
import {useVariantUrl} from '~/lib/variants';
9+
10+
export function ProductItem({
11+
product,
12+
loading,
13+
}: {
14+
product:
15+
| CollectionItemFragment
16+
| ProductItemFragment
17+
| RecommendedProductFragment;
18+
loading?: 'eager' | 'lazy';
19+
}) {
20+
const variantUrl = useVariantUrl(product.handle);
21+
const image = product.featuredImage;
22+
return (
23+
<Link
24+
className="product-item"
25+
key={product.id}
26+
prefetch="intent"
27+
to={variantUrl}
28+
>
29+
{image && (
30+
<Image
31+
alt={image.altText || product.title}
32+
aspectRatio="1/1"
33+
data={image}
34+
loading={loading}
35+
sizes="(min-width: 45em) 400px, 100vw"
36+
/>
37+
)}
38+
<h4>{product.title}</h4>
39+
<small>
40+
<Money data={product.priceRange.minVariantPrice} />
41+
</small>
42+
</Link>
43+
);
44+
}

templates/skeleton/app/routes/_index.tsx

+8-23
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ import type {
66
FeaturedCollectionFragment,
77
RecommendedProductsQuery,
88
} from 'storefrontapi.generated';
9+
import {ProductItem} from '~/components/ProductItem';
910

1011
export const meta: MetaFunction = () => {
1112
return [{title: 'Hydrogen | Home'}];
@@ -101,21 +102,7 @@ function RecommendedProducts({
101102
<div className="recommended-products-grid">
102103
{response
103104
? response.products.nodes.map((product) => (
104-
<Link
105-
key={product.id}
106-
className="recommended-product"
107-
to={`/products/${product.handle}`}
108-
>
109-
<Image
110-
data={product.images.nodes[0]}
111-
aspectRatio="1/1"
112-
sizes="(min-width: 45em) 20vw, 50vw"
113-
/>
114-
<h4>{product.title}</h4>
115-
<small>
116-
<Money data={product.priceRange.minVariantPrice} />
117-
</small>
118-
</Link>
105+
<ProductItem key={product.id} product={product} />
119106
))
120107
: null}
121108
</div>
@@ -161,14 +148,12 @@ const RECOMMENDED_PRODUCTS_QUERY = `#graphql
161148
currencyCode
162149
}
163150
}
164-
images(first: 1) {
165-
nodes {
166-
id
167-
url
168-
altText
169-
width
170-
height
171-
}
151+
featuredImage {
152+
id
153+
url
154+
altText
155+
width
156+
height
172157
}
173158
}
174159
query RecommendedProducts ($country: CountryCode, $language: LanguageCode)

templates/skeleton/app/routes/collections.$handle.tsx

+3-41
Original file line numberDiff line numberDiff line change
@@ -1,15 +1,9 @@
11
import {redirect, type LoaderFunctionArgs} from '@shopify/remix-oxygen';
2-
import {useLoaderData, Link, type MetaFunction} from '@remix-run/react';
3-
import {
4-
getPaginationVariables,
5-
Image,
6-
Money,
7-
Analytics,
8-
} from '@shopify/hydrogen';
9-
import type {ProductItemFragment} from 'storefrontapi.generated';
10-
import {useVariantUrl} from '~/lib/variants';
2+
import {useLoaderData, type MetaFunction} from '@remix-run/react';
3+
import {getPaginationVariables, Analytics} from '@shopify/hydrogen';
114
import {PaginatedResourceSection} from '~/components/PaginatedResourceSection';
125
import {redirectIfHandleIsLocalized} from '~/lib/redirect';
6+
import {ProductItem} from '~/components/ProductItem';
137

148
export const meta: MetaFunction<typeof loader> = ({data}) => {
159
return [{title: `Hydrogen | ${data?.collection.title ?? ''} Collection`}];
@@ -105,38 +99,6 @@ export default function Collection() {
10599
);
106100
}
107101

108-
function ProductItem({
109-
product,
110-
loading,
111-
}: {
112-
product: ProductItemFragment;
113-
loading?: 'eager' | 'lazy';
114-
}) {
115-
const variantUrl = useVariantUrl(product.handle);
116-
return (
117-
<Link
118-
className="product-item"
119-
key={product.id}
120-
prefetch="intent"
121-
to={variantUrl}
122-
>
123-
{product.featuredImage && (
124-
<Image
125-
alt={product.featuredImage.altText || product.title}
126-
aspectRatio="1/1"
127-
data={product.featuredImage}
128-
loading={loading}
129-
sizes="(min-width: 45em) 400px, 100vw"
130-
/>
131-
)}
132-
<h4>{product.title}</h4>
133-
<small>
134-
<Money data={product.priceRange.minVariantPrice} />
135-
</small>
136-
</Link>
137-
);
138-
}
139-
140102
const PRODUCT_ITEM_FRAGMENT = `#graphql
141103
fragment MoneyProductItem on MoneyV2 {
142104
amount

templates/skeleton/app/routes/collections.all.tsx

+11-44
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,8 @@
11
import {type LoaderFunctionArgs} from '@shopify/remix-oxygen';
2-
import {useLoaderData, Link, type MetaFunction} from '@remix-run/react';
3-
import {getPaginationVariables, Image, Money} from '@shopify/hydrogen';
4-
import type {ProductItemFragment} from 'storefrontapi.generated';
5-
import {useVariantUrl} from '~/lib/variants';
2+
import {useLoaderData, type MetaFunction} from '@remix-run/react';
3+
import {getPaginationVariables} from '@shopify/hydrogen';
64
import {PaginatedResourceSection} from '~/components/PaginatedResourceSection';
5+
import {ProductItem} from '~/components/ProductItem';
76

87
export const meta: MetaFunction<typeof loader> = () => {
98
return [{title: `Hydrogen | Products`}];
@@ -69,44 +68,12 @@ export default function Collection() {
6968
);
7069
}
7170

72-
function ProductItem({
73-
product,
74-
loading,
75-
}: {
76-
product: ProductItemFragment;
77-
loading?: 'eager' | 'lazy';
78-
}) {
79-
const variantUrl = useVariantUrl(product.handle);
80-
return (
81-
<Link
82-
className="product-item"
83-
key={product.id}
84-
prefetch="intent"
85-
to={variantUrl}
86-
>
87-
{product.featuredImage && (
88-
<Image
89-
alt={product.featuredImage.altText || product.title}
90-
aspectRatio="1/1"
91-
data={product.featuredImage}
92-
loading={loading}
93-
sizes="(min-width: 45em) 400px, 100vw"
94-
/>
95-
)}
96-
<h4>{product.title}</h4>
97-
<small>
98-
<Money data={product.priceRange.minVariantPrice} />
99-
</small>
100-
</Link>
101-
);
102-
}
103-
104-
const PRODUCT_ITEM_FRAGMENT = `#graphql
105-
fragment MoneyProductItem on MoneyV2 {
71+
const COLLECTION_ITEM_FRAGMENT = `#graphql
72+
fragment MoneyCollectionItem on MoneyV2 {
10673
amount
10774
currencyCode
10875
}
109-
fragment ProductItem on Product {
76+
fragment CollectionItem on Product {
11077
id
11178
handle
11279
title
@@ -119,16 +86,16 @@ const PRODUCT_ITEM_FRAGMENT = `#graphql
11986
}
12087
priceRange {
12188
minVariantPrice {
122-
...MoneyProductItem
89+
...MoneyCollectionItem
12390
}
12491
maxVariantPrice {
125-
...MoneyProductItem
92+
...MoneyCollectionItem
12693
}
12794
}
12895
}
12996
` as const;
13097

131-
// NOTE: https://shopify.dev/docs/api/storefront/2024-01/objects/product
98+
// NOTE: https://shopify.dev/docs/api/storefront/latest/objects/product
13299
const CATALOG_QUERY = `#graphql
133100
query Catalog(
134101
$country: CountryCode
@@ -140,7 +107,7 @@ const CATALOG_QUERY = `#graphql
140107
) @inContext(country: $country, language: $language) {
141108
products(first: $first, last: $last, before: $startCursor, after: $endCursor) {
142109
nodes {
143-
...ProductItem
110+
...CollectionItem
144111
}
145112
pageInfo {
146113
hasPreviousPage
@@ -150,5 +117,5 @@ const CATALOG_QUERY = `#graphql
150117
}
151118
}
152119
}
153-
${PRODUCT_ITEM_FRAGMENT}
120+
${COLLECTION_ITEM_FRAGMENT}
154121
` as const;

templates/skeleton/storefrontapi.generated.d.ts

+29-15
Original file line numberDiff line numberDiff line change
@@ -331,11 +331,9 @@ export type RecommendedProductFragment = Pick<
331331
priceRange: {
332332
minVariantPrice: Pick<StorefrontAPI.MoneyV2, 'amount' | 'currencyCode'>;
333333
};
334-
images: {
335-
nodes: Array<
336-
Pick<StorefrontAPI.Image, 'id' | 'url' | 'altText' | 'width' | 'height'>
337-
>;
338-
};
334+
featuredImage?: StorefrontAPI.Maybe<
335+
Pick<StorefrontAPI.Image, 'id' | 'url' | 'altText' | 'width' | 'height'>
336+
>;
339337
};
340338

341339
export type RecommendedProductsQueryVariables = StorefrontAPI.Exact<{
@@ -353,14 +351,12 @@ export type RecommendedProductsQuery = {
353351
'amount' | 'currencyCode'
354352
>;
355353
};
356-
images: {
357-
nodes: Array<
358-
Pick<
359-
StorefrontAPI.Image,
360-
'id' | 'url' | 'altText' | 'width' | 'height'
361-
>
362-
>;
363-
};
354+
featuredImage?: StorefrontAPI.Maybe<
355+
Pick<
356+
StorefrontAPI.Image,
357+
'id' | 'url' | 'altText' | 'width' | 'height'
358+
>
359+
>;
364360
}
365361
>;
366362
};
@@ -594,6 +590,24 @@ export type StoreCollectionsQuery = {
594590
};
595591
};
596592

593+
export type MoneyCollectionItemFragment = Pick<
594+
StorefrontAPI.MoneyV2,
595+
'amount' | 'currencyCode'
596+
>;
597+
598+
export type CollectionItemFragment = Pick<
599+
StorefrontAPI.Product,
600+
'id' | 'handle' | 'title'
601+
> & {
602+
featuredImage?: StorefrontAPI.Maybe<
603+
Pick<StorefrontAPI.Image, 'id' | 'altText' | 'url' | 'width' | 'height'>
604+
>;
605+
priceRange: {
606+
minVariantPrice: Pick<StorefrontAPI.MoneyV2, 'amount' | 'currencyCode'>;
607+
maxVariantPrice: Pick<StorefrontAPI.MoneyV2, 'amount' | 'currencyCode'>;
608+
};
609+
};
610+
597611
export type CatalogQueryVariables = StorefrontAPI.Exact<{
598612
country?: StorefrontAPI.InputMaybe<StorefrontAPI.CountryCode>;
599613
language?: StorefrontAPI.InputMaybe<StorefrontAPI.LanguageCode>;
@@ -1188,7 +1202,7 @@ interface GeneratedQueryTypes {
11881202
return: FeaturedCollectionQuery;
11891203
variables: FeaturedCollectionQueryVariables;
11901204
};
1191-
'#graphql\n fragment RecommendedProduct on Product {\n id\n title\n handle\n priceRange {\n minVariantPrice {\n amount\n currencyCode\n }\n }\n images(first: 1) {\n nodes {\n id\n url\n altText\n width\n height\n }\n }\n }\n query RecommendedProducts ($country: CountryCode, $language: LanguageCode)\n @inContext(country: $country, language: $language) {\n products(first: 4, sortKey: UPDATED_AT, reverse: true) {\n nodes {\n ...RecommendedProduct\n }\n }\n }\n': {
1205+
'#graphql\n fragment RecommendedProduct on Product {\n id\n title\n handle\n priceRange {\n minVariantPrice {\n amount\n currencyCode\n }\n }\n featuredImage {\n id\n url\n altText\n width\n height\n }\n }\n query RecommendedProducts ($country: CountryCode, $language: LanguageCode)\n @inContext(country: $country, language: $language) {\n products(first: 4, sortKey: UPDATED_AT, reverse: true) {\n nodes {\n ...RecommendedProduct\n }\n }\n }\n': {
11921206
return: RecommendedProductsQuery;
11931207
variables: RecommendedProductsQueryVariables;
11941208
};
@@ -1212,7 +1226,7 @@ interface GeneratedQueryTypes {
12121226
return: StoreCollectionsQuery;
12131227
variables: StoreCollectionsQueryVariables;
12141228
};
1215-
'#graphql\n query Catalog(\n $country: CountryCode\n $language: LanguageCode\n $first: Int\n $last: Int\n $startCursor: String\n $endCursor: String\n ) @inContext(country: $country, language: $language) {\n products(first: $first, last: $last, before: $startCursor, after: $endCursor) {\n nodes {\n ...ProductItem\n }\n pageInfo {\n hasPreviousPage\n hasNextPage\n startCursor\n endCursor\n }\n }\n }\n #graphql\n fragment MoneyProductItem on MoneyV2 {\n amount\n currencyCode\n }\n fragment ProductItem on Product {\n id\n handle\n title\n featuredImage {\n id\n altText\n url\n width\n height\n }\n priceRange {\n minVariantPrice {\n ...MoneyProductItem\n }\n maxVariantPrice {\n ...MoneyProductItem\n }\n }\n }\n\n': {
1229+
'#graphql\n query Catalog(\n $country: CountryCode\n $language: LanguageCode\n $first: Int\n $last: Int\n $startCursor: String\n $endCursor: String\n ) @inContext(country: $country, language: $language) {\n products(first: $first, last: $last, before: $startCursor, after: $endCursor) {\n nodes {\n ...CollectionItem\n }\n pageInfo {\n hasPreviousPage\n hasNextPage\n startCursor\n endCursor\n }\n }\n }\n #graphql\n fragment MoneyCollectionItem on MoneyV2 {\n amount\n currencyCode\n }\n fragment CollectionItem on Product {\n id\n handle\n title\n featuredImage {\n id\n altText\n url\n width\n height\n }\n priceRange {\n minVariantPrice {\n ...MoneyCollectionItem\n }\n maxVariantPrice {\n ...MoneyCollectionItem\n }\n }\n }\n\n': {
12161230
return: CatalogQuery;
12171231
variables: CatalogQueryVariables;
12181232
};

0 commit comments

Comments
 (0)