Build a product details page with Hydrogen
In this tutorial, you'll create a new page in your Remix app to display product data from Shopify.
You can display detailed information about products on your custom storefront. Your product details affect the way that the product is displayed to customers, help you to organize your products, and help customers to find the product.
By creating a product page, you can share information about a product with customers, including the product’s variants, description, and price. You’ll also be able to offer customers a way to purchase the product.
What you'll learnAnchor link to section titled "What you'll learn"
In this tutorial, you'll learn how to do the following tasks:
- Set up a new Remix route
- Query products by their handle
- Render product data on the page
- Create a product variant selector
- Make the product variant selector interactive with Remix Links and Optimistic UI
- Add a Shop Pay button
RequirementsAnchor link to section titled "Requirements"
Step 1: Create a product routeAnchor link to section titled "Step 1: Create a product route"
To begin building your product page, create a file called
app/routes/products.$handle.jsx, and add the following code.
You should test your route and ensure the page renders. Open your browser and navigate to http://localhost:3000/products/snowboard
Step 2: Setup the Remix loader to get product dataAnchor link to section titled "Step 2: Setup the Remix loader to get product data"
Loaders are functions that retrieve the data that's needed to render this page.
Create the loaderAnchor link to section titled "Create the loader"
Create a loader function by adding the following code to the top of your
$handle.jsx route. This code example retrieves the URL handle variable from the
params that are passed to the loader function, and returns sample JSON that you can use in the JSX component.
Render the loader dataAnchor link to section titled "Render the loader data"
Now that you have a loader, you can get the loader's data in your JSX component with the
useLoaderData() hook. Now import that function and render the handle. Your complete
$handle.jsx file should look like this.
Reload your page and check if your URL handle shows in your
Step 3: Query the Storefront APIAnchor link to section titled "Step 3: Query the Storefront API"
Your loader function uses the
params object to get URL data. Remix loaders also include the
context objects. Shopify's storefront query function exists in the
context object. You can use the following steps to set up a GraphQL query and connect it to your storefront
Create a GraphQL stringAnchor link to section titled "Create a GraphQL string"
Add the following query to the bottom of your product route. This simple query uses the $handle` variables to retrieve the data from a single product, using the handle in your URL.
Execute the query in the Remix loaderAnchor link to section titled "Execute the query in the Remix loader"
Now you can combine your handle and GraphQL query string with a storefront
query() function. This function returns a promise object, so you need to update your loader function to be
async. You also need to update your loader parameters to destructure the
Update your loader so it includes this code.
Here are the changes made:
asyncto allow the loader function to handle Promises
contextto your loader function arguments
- Added a function
context.storefront.query()that uses the
handlecreated in previous steps
- Destructured the
productfrom the API query function response
- Added the
productto the JSON object returned from the loader
Create a debug component to see product dataAnchor link to section titled "Create a debug component to see product data"
It might be difficult to test the API query that you created. You can
console.log() your product data to do a quick check, but it might be easier to see the data formatted in your browser. You can add the following code to your product route to create a small utility component that renders the JSON.
You can update your
ProductHandle() component to show product data. Add the
product object to your
userLoaderData() destructured object. Then add the product data to your
PrintJson() component as a
Step 4: Display title, description, and product imagesAnchor link to section titled "Step 4: Display title, description, and product images"
Now that you're familiar with querying product data, you can start building the page for your customers. First, you need to update your GraphQL query to get the full set of data for the rest of this tutorial.
Update the GraphQL queryAnchor link to section titled "Update the GraphQL query"
Replace your existing
PRODUCT_QUERY with the following query. This query fetches data for the product display page, including descriptive details, images, and options data.
Now that you have the data, you can render it on the page. You can use HTML, CSS, and Tailwind to render the product data on the page, as shown in the following example:
Create a product image galleryAnchor link to section titled "Create a product image gallery"
In your previous query, you retrieved the image data for the product. In this step, you'll create a
ProductGallery component that uses Hydrogen's
MediaFile component to build a product image gallery.
MediaFile component at the top of your product route:
ProductHandle component, create a new component:
This code transforms the GraphQL
Model3d nodes into a format that the
MediaFile component can use to render images and 3D models.
Render this component by replacing the
<h2>TODO Product Gallery</h2> in your
You should have the image, title, vendor, and description HTML rendering in a two-column layout.
Step 5: Render the product variant optionsAnchor link to section titled "Step 5: Render the product variant options"
Remix loads a full page when you navigate to it. For each additional URL change, React Router will handle page updates. The end result is a website that feels like an app, without all of the hassle.
Remix uses the URL as a state manager. This means that any interaction that you build for the product options selector should update the URL. You'll use Remix
<Link> components to help you build these interactive pieces.
The option selector works as follows:
- User clicks on option
- Remix updates the URL
- Remix runs the
loader()function with the new URL
- The loader function executes a
- Remix returns the new data as JSON
- React receives an update from the
useLoaderData()hook and makes the necessary updates to the page.
You will need to make two updates for the options selector to work.
- Render the options as
- Update the
storefront.query()to accept the new options URL parameters
Render the product options linksAnchor link to section titled "Render the product options links"
Create a new component to render these links. Create the file
app/components/ProductOptions.jsx. Add in the following code.
This is the important code to build your links:
Now you can add this component to your
ProductHandle() function in
At the top of the file:
Right before the description block:
Reload your page and click on some links. You should see the URL change, but the page doesn't reload. This is React Router taking control and loading your updated state.
You still need to update the loader function to recognize our URL parameters and update the data, which is covered in the next section.
Update the LoaderAnchor link to section titled "Update the Loader"
context, the loader has access to the
request object. You'll need the
searchParams from the request URL.
Use the following code to update the
loader function in your product route to pull in the
request and format the options data for the Storefront API.
This updated code takes the search params and creates a
selectedOptions array of objects that's passed to the
Now, you can update your
PRODUCT_QUERY to get a selected variant from the
selectedOptions data. Replace your current query with the following query:
variantBySelectedOptions returns the selected variant based on the option values that you retrieved from the URL. You can test the return by adding a line below
<ProductOptions/> to print out the selected variant ID:
Click on some options to see the selected variant ID change. After you have verified, you can remove the selected variant line of code.
Right now you can't visually see what options are selected. You will fix this in the next section.
Update the product options UIAnchor link to section titled "Update the product options UI"
The URL query parameters hold your "state", so you can refer to these parameters to determine the selected option. You can read these parameters with Remix's
useSearchParams() hook. Update your logic so that it loops over each option to add a dark border to selected
Now you have an option selector that responds to URL changes, but the link underline doesn't update immediately.
This delay happens because Remix is responding to the URL change, running the
loader() function, and passing that data down to the React component. Even when all servers and APIs are fast, this process isn't instant. Because you know that the selection will be successful, you can update the UI without waiting for a server response. Remix's Optimistic UI pattern is perfect for this use case. Next you'll add the
useNavigation() hook and update the logic that reads the search parameters.
useNavigationat the top of your
Insert the following code at the top of your
Reload the page and make some option selections. The underline selections are now instant, even when the loader is still working to update the data.
Here is your complete
Step 6: Set a default variantAnchor link to section titled "Step 6: Set a default variant"
You might notice that a fresh product page has no options selected. You can update your loader to use a default variant so there is always an orderable product available on the page.
PRODUCT_QUERY already gets the first variant for you with the
variants(first: 1) query filter. You can use that data to update the logic in your
loader() function. You'll need to make updates in 4 places:
- Create a new
selectedVariantvalue in the loader
- Destructure it from
- Pass the
- Update the logic in
ProductOptionsto set the selected default value
Update the loaderAnchor link to section titled "Update the loader"
Add the following code to the bottom of the
loader function, replacing the
Update the Anchor link to section titled "Update the useLoaderData() hook"
selectedVariant to the
useLoaderData() destructured object:
Pass the variant to Anchor link to section titled "Pass the variant to ProductOptions"
<ProductOptions> component to accept a
Update Anchor link to section titled "Update ProductOptions"
Replace the top portion of
ProductOptions with the following code:
The new logic does the following:
- Get the
selectedVariantfrom the component props
- On render, create a
- Clone the existing parameters
- If the parameters don't already include a selected value for the selected variant, then add it to the cloned parameters
paramsWithDefaultsas the fallback when you create the
Test the new logic by visiting http://localhost:3000/products/snowboard. Default option values should be selected on page load.
This logic doesn't override a user selection, but it pre-populates selected options based on the first variant returned from the Storefront API.
Step 7: Add a Shop Pay ButtonAnchor link to section titled "Step 7: Add a Shop Pay Button"
You've already created a page where customers can view and select your product. To add a Shop Pay button, you can refer to the Hydrogen React components for pre-built commerce primitives.
ShopPay components to simplify your development experience.
Get the Shop DomainAnchor link to section titled "Get the Shop Domain"
To use the Shop Pay button, you'll need to get the shop domain from the
context object in the loader.
loader()function, add the following code:
storeDomainvalue in the
ProductHandlefunction, add the
storeDomainvalue to the
Create a Shop Pay ButtonAnchor link to section titled "Create a Shop Pay Button"
products.$handle.jsxfile, import the Hydrogen React components to render pricing and the Shop Pay button:
useLoaderData(), set a new value that's based on whether the user can purchase the selected product:
<ProductOptions>, render the price and the Shop Pay button:
The pricing should now show for the selected variant, and you can jump directly to a Shop Pay checkout.
The product page now renders all of the details for a product and its variants. It also includes a button to purchase the product.
Learn how to build a cart by defining the context for interacting with a cart and adding an Add to Cart button, enabling customers to choose products to purchase before entering the payment process.