LIGHTNING FAST
E-COMMERCE
REMIX YOUR SHOP WITH SHOPIFY HYDROGEN
OVERVIEW
01
02
03
HEADLESS COMMERCE
Definition, importance, comparison
SHOPIFY HYDROGEN
Introduction, features
HYDROGEN CUSTOM STOREFRONT
Let's build a storefront!
HEADLESS COMMERCE DEFINITION
-
Separation of frontend and backend
-
API driven architecture
-
Flexible, customizable front-end
BENEFITS OF HEADLESS COMMERCE
-
Omnichannel selling
-
Faster time to market
-
Better performance
-
Better SEO
-
Personalization and customization
-
Improved security
Well, some of them...
HEADLESS VS MONOLITHIC
HEADLESS
- Front-end presentation layer separated from back-end
- Enables businesses to create and deliver custom shopping experiences
-
More flexibility, scalability, and agility
MONOLITHIC
- Front-end and back-end tightly integrated
- Limited flexibility, scalability, and agility
-
Not easily adaptable to changing consumer preferences and market trends
INTRODUCING SHOPIFY HYDROGEN
- New framework for building headless Shopify stores
- Simplified development experience
- Built-in best practices
- Built on Remix!
KEY FEATURES OF HYDROGEN
- Components-based architecture
- Improved performance with REMIX
- Seamless integration with Shopify APIs
CREATING A CUSTOM STOREFRONT
- Creating the project
- Fetching and mutating data in Hydrogen
- Building the collections and product
- Building a cart
CREATE THE PROJECT
npm create @shopify/hydrogen@latest
FETCHING AND MUTATING DATA IN HYDROGEN
Hydrogen provides a storefront client to send queries and mutations from Remix loaders and actions
- Create and inject the Storefront client in server.js
- Call the storefront client in loaders and actions
- Use storefront.query() to send GraphQL queries to your storefront and load data
- Use storefront.mutate() to perform GraphQL mutations in Remix actions
- Use storefront.query() to send GraphQL queries to your storefront and load data
CREATING COLLECTIONS
In routes create a collections folder
- index.jsx will display all the collections entries
-
$handle.jsx will display the title, description and paginated products form a collection
CREATING PRODUCTS
In routes create a products folder
- $handle.jsx will display the product details
- Use hydrogen-react components: Money, Image, ShopPayButton
ADD A SHOP PAY BUTTON
export const loader = async ({params, context, request}) => {
const {handle} = params;
const searchParams = new URL(request.url).searchParams;
const selectedOptions = [];
searchParams.forEach((value, name) => {
selectedOptions.push({name, value});
});
const {product} = await context.storefront.query(PRODUCT_QUERY, {
variables: {
handle,
selectedOptions,
},
});
if (!product?.id) {
throw new Response(null, {status: 404});
}
const selectedVariant =
product.selectedVariant ?? product?.variants?.nodes[0];
const storeDomain = context.storefront.getShopifyDomain();
return json({
product,
selectedVariant,
storeDomain,
});
};
BUILDING A CART
- Client side cart operations degrade performance and UX
- Remix mutations API leverage web standards like form and HTTP
- Let’s build a performant server-side cart!
CREATE A CART ROUTE
- app/routes/cart.jsx
- Create an action that handles a form submission to the cart route
export async function action({request, context}) {
const {session, storefront} = context;
const headers = new Headers();
const [formData, storedCartId] = await Promise.all([
request.formData(),
session.get('cartId'),
]);
let cartId = storedCartId;
let status = 200;
let result;
// TODO form action
}
CREATE THE API FUNCTIONS
Create functions to create a Cart, add line items to the cart, and remove items from the cart
Each function will use the context.storefront
CREATE CART
-
cartCreate
creates a cart with line item data using theCREATE_CART_MUTATION
query
export async function cartCreate({input, storefront}) {
const {cartCreate} = await storefront.mutate(CREATE_CART_MUTATION, {
variables: {input},
});
return cartCreate;
}
ADD TO CART
-
cartAdd
adds line items to an existing cart using theADD_LINES_MUTATION
query
export async function cartAdd({cartId, lines, storefront}) {
const {cartLinesAdd} = await storefront.mutate(ADD_LINES_MUTATION, {
variables: {cartId, lines},
});
return cartLinesAdd;
}
REMOVE FROM CART
-
cartRemove
removes line items from a cart using theREMOVE_LINE_ITEMS_MUTATION
query
export async function cartRemove({cartId, lineIds, storefront}) {
const {cartLinesRemove} = await storefront.mutate(
REMOVE_LINE_ITEMS_MUTATION,
{
variables: {
cartId,
lineIds,
},
},
);
if (!cartLinesRemove) {
throw new Error('No data returned from remove lines mutation');
}
return cartLinesRemove;
}
CALL THE FUNCTIONS FROM THE ACTION
A route can only have 1 action, so we create a switch to run the function based on the submitted formAction input
CREATE AN ADD TO CART BUTTON
import {useFetcher} from '@remix-run/react';
export const AddToCartButton = ({variantId}) => {
const fetcher = useFetcher();
const lines = [{merchandiseId: variantId, quantity: 1}];
return (
<fetcher.Form action="/cart" method="post">
<input type="hidden" name="cartAction" value={'ADD_TO_CART'} />
<input type="hidden" name="lines" value={JSON.stringify(lines)} />
<button className="bg-black text-white px-6 py-3 w-full rounded-md text-center font-medium max-w-[400px]">
Add to Cart
</button>
</fetcher.Form>
);
};
CREATE AN ADD TO CART BUTTON
switch (cartAction) {
case 'ADD_TO_CART':
const lines = formData.get('lines')
? JSON.parse(String(formData.get('lines')))
: [];
if (!cartId) {
result = await cartCreate({
input: countryCode ? {lines, buyerIdentity: {countryCode}} : {lines},
storefront,
});
} else {
result = await cartAdd({
cartId,
lines,
storefront,
});
}
cartId = result.cart.id;
break;
}
CREATE A REMOVE FROM CART BUTTON
import {useFetcher} from '@remix-run/react';
import {BsTrash3 as IconRemove} from 'react-icons/bs';
const RemoveFromCartButton = ({lineIds}) => {
const fetcher = useFetcher();
return (
<fetcher.Form action="/cart" method="post">
<input type="hidden" name="cartAction" value="REMOVE_FROM_CART" />
<input type="hidden" name="linesIds" value={JSON.stringify(lineIds)} />
<button
className="flex items-center justify-center w-10 h-10 max-w-xl my-2 leading-none text-center text-black bg-white border border-black rounded-md hover:text-white hover:bg-black font-small"
type="submit"
>
<IconRemove />
</button>
</fetcher.Form>
);
};
export default RemoveFromCartButton;
CREATE A REMOVE FROM CART BUTTON
case 'REMOVE_FROM_CART':
const lineIds = formData.get('linesIds')
? JSON.parse(String(formData.get('linesIds')))
: [];
if (!lineIds.length) {
throw new Error('No lines to remove');
}
result = await cartRemove({
cartId,
lineIds,
storefront,
});
FETCH AND READ THE CART
- Create a cart query in app/queries/cart.js
- Create a cart fetcher function
- Load the cart
FETCH AND READ THE CART
- Create a cart fetcher function: getCart()
- Disable the cache to avoid stale data
export async function getCart({storefront}, cartId) {
if (!storefront) {
throw new Error('missing storefront client in cart query');
}
const {cart} = await storefront.query(CART_QUERY, {
variables: {
cartId,
country: storefront.i18n.country,
language: storefront.i18n.language,
},
cache: storefront.CacheNone(),
});
return cart;
}
FETCH AND READ THE CART
Load the Cart
import {getCart} from '~/utils/getCart';
export const loader = async ({context}) => {
const cartId = await context.session.get('cartId');
return json({cart: cartId ? await getCart(context, cartId) : undefined});
};
CREATE THE CART COMPONENTS
- LineItem.jsx: app/components/cart/LineItem.jsx
- CartLineItems.jsx: app/components/cart/CartLineItems.jsx
- CartSummary.jsx: app/components/cart/CartSummary.jsx
- CheckoutButton.jsx: app/components/cart/CheckoutButton.jsx
- CartContent.jsx: app/components/cart/CartContent.jsx
CREATE THE CART COMPONENTS
Finally render our cart data on the cart route
const Cart = () => {
const {cart} = useLoaderData();
return cart?.totalQuantity > 0 ? (
<CartContent cart={cart} location="page" />
) : (
<CartEmpty />
);
};
CREATE A CART DRAWER
- create Drawer.jsx: app/components/Drawer.jsx using Headless UI
- Fetch the cart in the root loader to make it globally available
- Create the CartDrawer
CREATE A CART DRAWER
Fetch the cart inside the loader
import {defer} from '@shopify/remix-oxygen';
export async function loader({context}) {
const cartId = await context.session.get('cartId');
const layout = await context.storefront.query(LAYOUT_QUERY);
return defer({
layout,
cart: cartId ? getCart(context, cartId) : undefined,
});
}
CREATE A CART DRAWER
Build the CartDrawer component:
- useLoaderData to get the cart data from the root
- Suspense and Await to resolve the cart deffered value
const {cart} = useLoaderData();
<Suspense>
<Await resolve={cart}>
{(data) => (
<>
<button
className="relative flex items-center justify-center w-8 h-8 ml-auto"
onClick={openDrawer}
>
<IconCart />
{data?.totalQuantity > 0 && (
<div className="text-contrast bg-red-500 text-white absolute bottom-1 right-1 text-[0.625rem] font-medium subpixel-antialiased h-3 min-w-[0.75rem] flex items-center justify-center leading-none text-center rounded-full w-auto px-[0.125rem] pb-px">
<span>{data?.totalQuantity}</span>
</div>
)}
</button>
<Drawer open={isOpen} onClose={closeDrawer}>
<>
{data?.totalQuantity > 0 ? (
<CartContent cart={data} />
) : (
<CartEmpty />
)}
<button
onClick={closeDrawer}
className="inline-block w-full max-w-xl px-6 py-3 font-medium leading-none text-center text-white bg-black rounded-sm"
></button>
</>
</Drawer>
</>
)}
</Await>
</Suspense>
deck
By Alexandra Spalato
deck
- 169