npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2025 – Pkg Stats / Ryan Hefner

@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/hooks

Hydrogen 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 → Use useServerCart
  • useProduct → Use useServerProduct
  • useCustomer → Use server-side customer handling
  • useAnalytics → 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

Product Hooks

Customer Hooks

Analytics Hooks

Utility Hooks

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 navigation
  • product_viewed - Product detail views
  • product_added_to_cart - Add to cart actions
  • checkout_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

  1. Memoize expensive operations - Hooks use useMemo internally
  2. Debounce user input - Use useDebouncedValue for search
  3. Lazy load with IntersectionObserver - Load content as needed
  4. Cache product data - Products are cached during the session
  5. 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

See Contributing Guide

License

MIT