npm package: @hydrogen-ui/hooks
Description: React hooks for Shopify Hydrogen storefronts
Latest version: 0.1.0

# @hydrogen-ui/hooks

React hooks for Shopify Hydrogen storefronts.

## Installation

```bash
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`:

```typescript
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`:

```typescript
import { 
  useMediaQuery, 
  useDebounce, 
  useLocalStorage,
  useIntersectionObserver 
} from '@hydrogen-ui/hooks';
```

## Migration Guide

See [MIGRATION_GUIDE.md](./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

```tsx
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](#usecart) - Complete cart management
- [useCartDrawer](#usecartdrawer) - Cart drawer state management
- [useOptimisticCart](#useoptimisticcart) - Optimistic UI for cart operations

### Product Hooks

- [useProduct](#useproduct) - Single product data and variant selection
- [useProductRecommendations](#useproductrecommendations) - Related product suggestions
- [useProductSearch](#useproductsearch) - Real-time product search

### Customer Hooks

- [useCustomer](#usecustomer) - Customer authentication and profile
- [useAddresses](#useaddresses) - Address book management

### Analytics Hooks

- [useAnalytics](#useanalytics) - E-commerce event tracking

### Utility Hooks

- [useDebouncedValue](#usedebouncedvalue) - Debounce rapidly changing values
- [useLocalStorage](#uselocalstorage) - Persistent state with localStorage
- [useIntersectionObserver](#useintersectionobserver) - Viewport visibility detection
- [useMediaQuery](#usemediaquery) - Responsive design utilities

## Cart Hooks

### useCart

Complete cart management with all CRUD operations.

```tsx
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:**
```typescript
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.

```tsx
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.

```tsx
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.

```tsx
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:**
```graphql
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.

```tsx
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.

```tsx
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.

```tsx
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:**
```typescript
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.

```tsx
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.

```tsx
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.

```tsx
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.

```tsx
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.

```tsx
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.

```tsx
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:

```typescript
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:

```tsx
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:

```tsx
// Safe to use in SSR
const stored = useLocalStorage('key', 'default');
const matches = useMediaQuery('(min-width: 768px)');
const isVisible = useIntersectionObserver(ref);
```

## Contributing

See [Contributing Guide](../../CONTRIBUTING.md)

## License

MIT