@hydrogen-ui/hooks
v0.1.0
Published
React hooks for Shopify Hydrogen storefronts
Readme
@hydrogen-ui/hooks
React hooks for Shopify Hydrogen storefronts.
Installation
npm install @hydrogen-ui/hooksHydrogen 2025 Migration
This package has been updated to support Hydrogen 2025's server-side architecture. The old client-side hooks are deprecated but still available for backward compatibility.
New Server-Compatible Hooks
Import from @hydrogen-ui/hooks/server:
import {
useServerCart,
useServerProduct,
CartProvider
} from '@hydrogen-ui/hooks/server';Legacy Hooks (Deprecated)
The following hooks from @hydrogen-ui/hooks/shopify are deprecated and will throw errors:
useCart→ UseuseServerCartuseProduct→ UseuseServerProductuseCustomer→ Use server-side customer handlinguseAnalytics→ Use Hydrogen 2025's Analytics component
Utility Hooks (Still Supported)
Import from @hydrogen-ui/hooks:
import {
useMediaQuery,
useDebounce,
useLocalStorage,
useIntersectionObserver
} from '@hydrogen-ui/hooks';Migration Guide
See MIGRATION_GUIDE.md for detailed migration instructions.
Entry Points
@hydrogen-ui/hooks- Utility hooks (no Shopify dependencies)@hydrogen-ui/hooks/shopify- Legacy Shopify hooks (deprecated)@hydrogen-ui/hooks/server- New server-compatible hooks for Hydrogen 2025
Requirements
- React 18.0 or higher
- @shopify/hydrogen 2025.5.0 or higher (for server hooks)
- @remix-run/react 2.8.0 or higher (for server hooks)
Features
- 🛒 Cart Management - Complete cart operations with optimistic UI
- 👤 Customer Accounts - Authentication and profile management
- 📦 Product Data - Fetching and managing product information
- 📊 Analytics - E-commerce event tracking
- 🔧 Utilities - Common patterns like debouncing and local storage
- 🚀 Performance Optimized - Memoization and smart caching
- 💪 TypeScript - Full type safety and autocompletion
- 🎯 SSR Safe - Works with server-side rendering
Quick Start
import { useCart, useProduct, useCustomer } from '@hydrogen-ui/hooks';
function ProductPage({ handle }) {
const { product, selectedVariant, setSelectedOptions } = useProduct(handle);
const { addItem } = useCart();
const { customer } = useCustomer();
const handleAddToCart = () => {
if (selectedVariant) {
addItem(selectedVariant.id, 1);
}
};
return (
<div>
<h1>{product?.title}</h1>
{customer && <p>Welcome back, {customer.firstName}!</p>}
<button onClick={handleAddToCart}>Add to Cart</button>
</div>
);
}Hook Categories
Cart Hooks
- useCart - Complete cart management
- useCartDrawer - Cart drawer state management
- useOptimisticCart - Optimistic UI for cart operations
Product Hooks
- useProduct - Single product data and variant selection
- useProductRecommendations - Related product suggestions
- useProductSearch - Real-time product search
Customer Hooks
- useCustomer - Customer authentication and profile
- useAddresses - Address book management
Analytics Hooks
- useAnalytics - E-commerce event tracking
Utility Hooks
- useDebouncedValue - Debounce rapidly changing values
- useLocalStorage - Persistent state with localStorage
- useIntersectionObserver - Viewport visibility detection
- useMediaQuery - Responsive design utilities
Cart Hooks
useCart
Complete cart management with all CRUD operations.
import { useCart } from '@hydrogen-ui/hooks';
function CartManager() {
const {
// Cart state
cart, // Full cart object
isLoading, // Loading state
error, // Error state
totalQuantity, // Total items in cart
subtotal, // Cart subtotal
// Cart operations
addItem, // Add product to cart
updateItem, // Update item quantity
removeItem, // Remove item from cart
clearCart, // Remove all items
applyDiscount, // Apply discount code
removeDiscount, // Remove discount code
updateBuyerIdentity, // Update customer info
// Cart UI
cartUrl // Checkout URL
} = useCart();
// Add item to cart
const handleAddItem = async () => {
try {
await addItem('variant-id', 2);
console.log('Item added!');
} catch (error) {
console.error('Failed to add item:', error);
}
};
// Update quantity
const handleUpdateQuantity = (lineId, newQuantity) => {
updateItem(lineId, newQuantity);
};
// Apply discount
const handleApplyDiscount = async (code) => {
const result = await applyDiscount(code);
if (result.success) {
console.log('Discount applied!');
}
};
return (
<div>
<p>Items in cart: {totalQuantity}</p>
<p>Subtotal: ${subtotal}</p>
{cart?.lines.nodes.map((line) => (
<div key={line.id}>
<span>{line.merchandise.product.title}</span>
<input
type="number"
value={line.quantity}
onChange={(e) => handleUpdateQuantity(line.id, parseInt(e.target.value))}
/>
<button onClick={() => removeItem(line.id)}>Remove</button>
</div>
))}
</div>
);
}Return Value:
interface UseCartReturn {
cart: Cart | null;
isLoading: boolean;
error: Error | null;
totalQuantity: number;
subtotal: number;
cartUrl: string;
addItem: (variantId: string, quantity: number) => Promise<void>;
updateItem: (lineId: string, quantity: number) => Promise<void>;
removeItem: (lineId: string) => Promise<void>;
clearCart: () => Promise<void>;
applyDiscount: (code: string) => Promise<{ success: boolean }>;
removeDiscount: (code: string) => Promise<void>;
updateBuyerIdentity: (buyerIdentity: BuyerIdentity) => Promise<void>;
}useCartDrawer
Simple state management for cart drawer UI.
import { useCartDrawer } from '@hydrogen-ui/hooks';
function Header() {
const { isOpen, open, close, toggle } = useCartDrawer();
// Automatically closes on Escape key
return (
<>
<button onClick={toggle}>
Cart ({isOpen ? 'Close' : 'Open'})
</button>
{isOpen && (
<div className="cart-drawer">
<button onClick={close}>×</button>
<CartContents />
</div>
)}
</>
);
}useOptimisticCart
Provides optimistic UI updates for cart operations.
import { useOptimisticCart } from '@hydrogen-ui/hooks';
function OptimisticCartExample() {
const {
optimisticCart, // Optimistically updated cart
addOptimisticItem, // Add with immediate UI update
isPending // Server update pending
} = useOptimisticCart();
const handleQuickAdd = (variantId) => {
// UI updates immediately
addOptimisticItem(variantId, 1);
// Server update happens in background
};
return (
<div>
{isPending && <span>Updating...</span>}
<div>Items: {optimisticCart.totalQuantity}</div>
</div>
);
}Product Hooks
useProduct
Fetches complete product data with variant selection management.
import { useProduct } from '@hydrogen-ui/hooks';
function ProductDetail({ handle }) {
const {
product, // Complete product data
loading, // Loading state
error, // Error state
selectedVariant, // Currently selected variant
selectedOptions, // Selected option values
setSelectedOptions // Update selected options
} = useProduct(handle);
if (loading) return <div>Loading...</div>;
if (error) return <div>Error: {error.message}</div>;
if (!product) return <div>Product not found</div>;
return (
<div>
<h1>{product.title}</h1>
<p>{product.description}</p>
{/* Option selectors */}
{product.options.map((option) => (
<div key={option.name}>
<label>{option.name}</label>
<select
value={selectedOptions[option.name] || ''}
onChange={(e) =>
setSelectedOptions({
...selectedOptions,
[option.name]: e.target.value
})
}
>
{option.values.map((value) => (
<option key={value} value={value}>
{value}
</option>
))}
</select>
</div>
))}
{/* Selected variant info */}
{selectedVariant && (
<div>
<p>Price: ${selectedVariant.price.amount}</p>
<p>Available: {selectedVariant.availableForSale ? 'Yes' : 'No'}</p>
</div>
)}
</div>
);
}GraphQL Fragment:
fragment ProductFragment on Product {
id
title
handle
description
vendor
seo {
title
description
}
images(first: 10) {
nodes {
id
url
altText
width
height
}
}
variants(first: 100) {
nodes {
id
title
availableForSale
price {
amount
currencyCode
}
selectedOptions {
name
value
}
}
}
options {
name
values
}
}useProductRecommendations
Fetches product recommendations based on a product ID.
import { useProductRecommendations } from '@hydrogen-ui/hooks';
function ProductRecommendations({ productId }) {
const {
recommendations, // Array of recommended products
loading,
error
} = useProductRecommendations(productId, { limit: 6 });
if (loading) return <div>Loading recommendations...</div>;
if (error) return null;
return (
<div className="recommendations">
<h3>You May Also Like</h3>
<div className="grid">
{recommendations.map((product) => (
<div key={product.id}>
<img src={product.images.nodes[0]?.url} alt={product.title} />
<h4>{product.title}</h4>
<p>${product.priceRange.minVariantPrice.amount}</p>
</div>
))}
</div>
</div>
);
}useProductSearch
Real-time product search with debouncing.
import { useProductSearch } from '@hydrogen-ui/hooks';
function SearchBar() {
const {
query, // Current search query
setQuery, // Update search query
results, // Search results
isSearching, // Loading state
error, // Error state
clearSearch // Clear search
} = useProductSearch({ debounceMs: 300 });
return (
<div className="search">
<input
type="search"
value={query}
onChange={(e) => setQuery(e.target.value)}
placeholder="Search products..."
/>
{isSearching && <div>Searching...</div>}
{results.length > 0 && (
<div className="search-results">
{results.map((product) => (
<a key={product.id} href={`/products/${product.handle}`}>
<img src={product.images.nodes[0]?.url} alt="" />
<span>{product.title}</span>
<span>${product.priceRange.minVariantPrice.amount}</span>
</a>
))}
</div>
)}
{query && results.length === 0 && !isSearching && (
<div>No products found</div>
)}
</div>
);
}Customer Hooks
useCustomer
Complete customer account management including authentication.
import { useCustomer } from '@hydrogen-ui/hooks';
function Account() {
const {
customer, // Customer data
isLoggedIn, // Authentication state
loading, // Loading state
error, // Error state
// Authentication methods
login, // Login with email/password
logout, // Logout customer
register, // Create new account
recover, // Send password recovery email
reset, // Reset password with token
updateProfile // Update customer info
} = useCustomer();
// Login form handler
const handleLogin = async (email, password) => {
try {
await login(email, password);
console.log('Logged in successfully!');
} catch (error) {
console.error('Login failed:', error);
}
};
// Registration handler
const handleRegister = async (data) => {
try {
await register({
email: data.email,
password: data.password,
firstName: data.firstName,
lastName: data.lastName,
acceptsMarketing: data.marketing
});
console.log('Account created!');
} catch (error) {
console.error('Registration failed:', error);
}
};
// Profile update
const handleUpdateProfile = async (updates) => {
try {
await updateProfile(updates);
console.log('Profile updated!');
} catch (error) {
console.error('Update failed:', error);
}
};
if (loading) return <div>Loading...</div>;
if (!isLoggedIn) {
return <LoginForm onSubmit={handleLogin} />;
}
return (
<div>
<h2>Welcome, {customer.firstName}!</h2>
<p>Email: {customer.email}</p>
<button onClick={logout}>Logout</button>
</div>
);
}Customer Methods:
interface UseCustomerReturn {
customer: Customer | null;
isLoggedIn: boolean;
loading: boolean;
error: Error | null;
login: (email: string, password: string) => Promise<void>;
logout: () => Promise<void>;
register: (data: RegisterData) => Promise<void>;
recover: (email: string) => Promise<void>;
reset: (password: string, resetToken: string) => Promise<void>;
updateProfile: (data: UpdateProfileData) => Promise<void>;
}useAddresses
Customer address book management with full CRUD operations.
import { useAddresses } from '@hydrogen-ui/hooks';
function AddressBook() {
const {
addresses, // Array of addresses
defaultAddress, // Default address
loading,
error,
// Address operations
createAddress, // Add new address
updateAddress, // Update existing address
deleteAddress, // Delete address
setDefaultAddress // Set as default
} = useAddresses();
// Create new address
const handleCreateAddress = async (addressData) => {
try {
await createAddress({
address1: addressData.address1,
address2: addressData.address2,
city: addressData.city,
province: addressData.province,
country: addressData.country,
zip: addressData.zip,
phone: addressData.phone
});
console.log('Address added!');
} catch (error) {
console.error('Failed to add address:', error);
}
};
// Update address
const handleUpdateAddress = async (id, updates) => {
try {
await updateAddress(id, updates);
console.log('Address updated!');
} catch (error) {
console.error('Failed to update address:', error);
}
};
// Set default
const handleSetDefault = async (id) => {
try {
await setDefaultAddress(id);
console.log('Default address updated!');
} catch (error) {
console.error('Failed to set default:', error);
}
};
if (loading) return <div>Loading addresses...</div>;
if (error) return <div>Error: {error.message}</div>;
return (
<div>
<h3>Your Addresses</h3>
{addresses.map((address) => (
<div key={address.id}>
<p>{address.address1}</p>
<p>{address.city}, {address.province} {address.zip}</p>
<p>{address.country}</p>
{address.isDefault && <span>Default</span>}
<button onClick={() => handleSetDefault(address.id)}>
Set as Default
</button>
<button onClick={() => deleteAddress(address.id)}>
Delete
</button>
</div>
))}
<button onClick={() => setShowAddForm(true)}>
Add New Address
</button>
</div>
);
}Analytics Hooks
useAnalytics
Unified e-commerce analytics tracking.
import { useAnalytics } from '@hydrogen-ui/hooks';
function AnalyticsExample() {
const {
trackPageView,
trackProductView,
trackAddToCart,
trackCheckoutStart,
trackEvent
} = useAnalytics();
// Track page view
useEffect(() => {
trackPageView({
pathname: location.pathname,
search: location.search,
title: document.title
});
}, [location]);
// Track product view
const handleProductView = (product) => {
trackProductView({
product: {
id: product.id,
title: product.title,
vendor: product.vendor,
price: product.variants[0].price.amount,
currency: product.variants[0].price.currencyCode
}
});
};
// Track add to cart
const handleAddToCart = (variant, quantity) => {
trackAddToCart({
productId: variant.product.id,
productTitle: variant.product.title,
variantId: variant.id,
variantTitle: variant.title,
quantity,
price: variant.price.amount,
currency: variant.price.currencyCode
});
};
// Track custom events
const handleCustomAction = () => {
trackEvent('newsletter_signup', {
location: 'footer',
incentive: '10% off'
});
};
return <div>Analytics integrated!</div>;
}Event Types:
page_viewed- Page navigationproduct_viewed- Product detail viewsproduct_added_to_cart- Add to cart actionscheckout_started- Checkout initiation- Custom events via
trackEvent(name, data)
Utility Hooks
useDebouncedValue
Debounces rapidly changing values to reduce updates.
import { useDebouncedValue } from '@hydrogen-ui/hooks';
function SearchInput() {
const [input, setInput] = useState('');
const debouncedSearch = useDebouncedValue(input, 500);
// API call only fires after 500ms of no typing
useEffect(() => {
if (debouncedSearch) {
searchProducts(debouncedSearch);
}
}, [debouncedSearch]);
return (
<input
value={input}
onChange={(e) => setInput(e.target.value)}
placeholder="Type to search..."
/>
);
}useLocalStorage
Persistent state management with localStorage.
import { useLocalStorage } from '@hydrogen-ui/hooks';
function Preferences() {
const [theme, setTheme] = useLocalStorage('theme', 'light');
const [currency, setCurrency] = useLocalStorage('currency', 'USD');
const [recentlyViewed, setRecentlyViewed] = useLocalStorage('recent', []);
// Values persist across page reloads
const addToRecentlyViewed = (productId) => {
setRecentlyViewed((prev) => {
const updated = [productId, ...prev.filter(id => id !== productId)];
return updated.slice(0, 10); // Keep last 10
});
};
return (
<div>
<select value={theme} onChange={(e) => setTheme(e.target.value)}>
<option value="light">Light</option>
<option value="dark">Dark</option>
</select>
<select value={currency} onChange={(e) => setCurrency(e.target.value)}>
<option value="USD">USD</option>
<option value="CAD">CAD</option>
<option value="EUR">EUR</option>
</select>
</div>
);
}useIntersectionObserver
Detects when elements enter the viewport.
import { useIntersectionObserver } from '@hydrogen-ui/hooks';
function LazyImage({ src, alt }) {
const ref = useRef(null);
const isVisible = useIntersectionObserver(ref, {
threshold: 0.1,
rootMargin: '50px'
});
return (
<div ref={ref}>
{isVisible ? (
<img src={src} alt={alt} />
) : (
<div className="placeholder" />
)}
</div>
);
}
function InfiniteScroll({ onLoadMore }) {
const triggerRef = useRef(null);
const shouldLoadMore = useIntersectionObserver(triggerRef);
useEffect(() => {
if (shouldLoadMore) {
onLoadMore();
}
}, [shouldLoadMore]);
return <div ref={triggerRef} />;
}useMediaQuery
Responsive design utilities with media query matching.
import { useMediaQuery } from '@hydrogen-ui/hooks';
function ResponsiveLayout() {
const isMobile = useMediaQuery('(max-width: 768px)');
const isTablet = useMediaQuery('(min-width: 769px) and (max-width: 1024px)');
const isDesktop = useMediaQuery('(min-width: 1025px)');
const prefersReducedMotion = useMediaQuery('(prefers-reduced-motion: reduce)');
const isDarkMode = useMediaQuery('(prefers-color-scheme: dark)');
return (
<div>
{isMobile && <MobileNav />}
{isTablet && <TabletNav />}
{isDesktop && <DesktopNav />}
<motion.div
animate={prefersReducedMotion ? {} : { opacity: 1 }}
>
Content with motion
</motion.div>
<div className={isDarkMode ? 'dark-theme' : 'light-theme'}>
Theme-aware content
</div>
</div>
);
}TypeScript Support
All hooks are fully typed with TypeScript. Import types as needed:
import type {
UseCartReturn,
UseProductReturn,
UseCustomerReturn,
CartItem,
Product,
Customer,
Address
} from '@hydrogen-ui/hooks';
// Use types in your components
const cartManager: UseCartReturn = useCart();
const productData: UseProductReturn = useProduct('product-handle');Performance Best Practices
- Memoize expensive operations - Hooks use
useMemointernally - Debounce user input - Use
useDebouncedValuefor search - Lazy load with IntersectionObserver - Load content as needed
- Cache product data - Products are cached during the session
- Batch cart operations - Multiple operations are batched automatically
Error Handling
All hooks include consistent error handling:
function ErrorExample() {
const { error, loading, retry } = useProduct('handle');
if (error) {
return (
<div>
<p>Error: {error.message}</p>
<button onClick={retry}>Try Again</button>
</div>
);
}
// ... rest of component
}SSR Compatibility
All utility hooks are SSR-safe:
// Safe to use in SSR
const stored = useLocalStorage('key', 'default');
const matches = useMediaQuery('(min-width: 768px)');
const isVisible = useIntersectionObserver(ref);Contributing
License
MIT
