Skip to main content

React Components

The SDK provides React components and hooks for seamless integration with your Hydrogen application.

GloodProvider

A React context provider that automatically subscribes to Shopify Analytics events and manages pixel tracking across all registered Glood apps.

Signature

function GloodProvider({
  client,
  children
}: GloodProviderProps): React.ReactElement

Props

PropTypeRequiredDescription
clientGloodClientYesGlood client instance created with createGlood()
childrenReact.ReactNodeYesChild components to wrap

Basic Usage

import { Analytics } from '@shopify/hydrogen';
import { GloodProvider, createGlood } from '@glood/hydrogen';
import { recommendations, search, wishlist } from '@glood/hydrogen';

const glood = createGlood({
  apiKey: process.env.GLOOD_API_KEY!,
  myShopifyDomain: 'your-store.myshopify.com',
})
  .use(recommendations())
  .use(search())
  .use(wishlist());

export default function App() {
  return (
    <Analytics>
      <GloodProvider client={glood} loaderData={data}>
        <Outlet />
      </GloodProvider>
    </Analytics>
  );
}

How It Works

The GloodProvider component:
  1. Creates React Context - Provides the Glood client to all child components
  2. Subscribes to Analytics - Uses Hydrogen’s useAnalytics() hook to receive events
  3. Distributes Events - Routes events to interested apps based on their subscriptions
  4. Checks Consent - Verifies customer privacy permissions before sending pixels
  5. Handles Errors - Provides comprehensive error handling and debug logging

Event Subscription Flow

Error Handling

The provider includes comprehensive error handling:
// Invalid client handling
<GloodProvider client={null}>
  <App />
</GloodProvider>
// Logs: "[Glood] GloodProvider: client is required"
// Renders children without Glood functionality

// Debug mode error logging
const glood = createGlood({
  apiKey: process.env.GLOOD_API_KEY!,
  myShopifyDomain: 'your-store.myshopify.com',
  debug: true, // Enable detailed error logging
});

<GloodProvider client={glood} loaderData={data}>
  <App />
</GloodProvider>

Server-Side Rendering

The provider automatically handles SSR:
// Only runs on client-side
useEffect(() => {
  if (typeof window === 'undefined' || !analytics?.customerPrivacy) {
    if (debug) {
      console.log('[Glood Debug] Skipping analytics setup: not in browser');
    }
    return;
  }
  // Setup analytics subscriptions
}, [clientConfig, analytics]);

Context Integration

The provider creates a React Context to share the client:
const GloodContext = createContext<GloodClient | null>(null);

export function GloodProvider({ client, children }) {
  // ... analytics setup

  return (
    <GloodContext.Provider value={client}>
      {children}
    </GloodContext.Provider>
  );
}

useGloodAnalytics

A React hook that provides access to the Glood client from context.

Signature

function useGloodAnalytics(): GloodClient | null

Returns

TypeDescription
GloodClient | nullThe Glood client instance or null if used outside provider

Usage

import { useGloodAnalytics } from '@glood/hydrogen';

function MyComponent() {
  const glood = useGloodAnalytics();

  if (!glood) {
    // Component used outside GloodProvider
    return <div>Glood not available</div>;
  }

  // Access client properties
  const isDebugMode = glood.debug;
  const enabledApps = glood.getEnabledApps();

  return (
    <div>
      <p>Debug mode: {isDebugMode ? 'On' : 'Off'}</p>
      <p>Enabled apps: {enabledApps.map(app => app.name).join(', ')}</p>
    </div>
  );
}

Custom Integrations

Use the hook for custom integrations with Glood apps:
import { useGloodAnalytics } from '@glood/hydrogen';

function CustomRecommendations() {
  const glood = useGloodAnalytics();
  const [recommendations, setRecommendations] = useState([]);

  useEffect(() => {
    if (!glood) return;

    const recommendationsApp = glood.getApp('recommendations');
    if (!recommendationsApp) return;

    // Custom API call to recommendations endpoint
    fetch(`${recommendationsApp.endpoint}/api/recommendations`, {
      method: 'POST',
      headers: {
        'Authorization': `Bearer ${glood.config.apiKey}`,
        'Content-Type': 'application/json',
      },
      body: JSON.stringify({
        shopDomain: glood.config.myShopifyDomain,
        // ... other parameters
      }),
    })
      .then(response => response.json())
      .then(data => setRecommendations(data.recommendations))
      .catch(error => console.error('Recommendations error:', error));
  }, [glood]);

  return (
    <div>
      {recommendations.map(product => (
        <ProductCard key={product.id} product={product} />
      ))}
    </div>
  );
}

Conditional Rendering

Use the hook for conditional rendering based on Glood availability:
import { useGloodAnalytics } from '@glood/hydrogen';

function EnhancedSearchBox() {
  const glood = useGloodAnalytics();
  const hasSearchApp = glood?.getApp('search');

  if (hasSearchApp) {
    return <GloodSearchBox />;
  }

  return <StandardSearchBox />;
}

Provider Placement

Correct Placement

The GloodProvider must be placed correctly in your component tree:
// ✅ Correct - Inside Analytics, wrapping application routes
import { Analytics } from '@shopify/hydrogen';

export default function App() {
  return (
    <html>
      <head>
        <Meta />
        <Links />
      </head>
      <body>
        <Analytics>
          <GloodProvider client={glood} loaderData={data}>
            <Layout>
              <Outlet />
            </Layout>
          </GloodProvider>
        </Analytics>
        <Scripts />
      </body>
    </html>
  );
}

Incorrect Placement

// ❌ Incorrect - Outside Analytics component
export default function App() {
  return (
    <GloodProvider client={glood} loaderData={data}>
      <Analytics>
        <Outlet />
      </Analytics>
    </GloodProvider>
  );
}

// ❌ Incorrect - Inside individual routes
export default function ProductPage() {
  return (
    <GloodProvider client={glood} loaderData={data}>
      <ProductDetails />
    </GloodProvider>
  );
}

Debug Logging

Enable debug mode to see detailed component behavior:
const glood = createGlood({
  apiKey: process.env.GLOOD_API_KEY!,
  myShopifyDomain: 'your-store.myshopify.com',
  debug: process.env.NODE_ENV === 'development',
});

<GloodProvider client={glood} loaderData={data}>
  <App />
</GloodProvider>
Debug logs include:
[Glood Debug] Setting up event subscriptions for apps: ['recommendations', 'search', 'wishlist']
[Glood Debug] Set analytics instance on recommendations app
[Glood Debug] App recommendations subscribes to events: ['page_viewed', 'product_viewed', ...]
[Glood Debug] All unique event types: ['page_viewed', 'product_viewed', 'search_submitted', ...]
[Glood Debug] Setting up centralized subscription for page_viewed
[Glood Debug] Received page_viewed event, distributing to interested apps
[Glood Debug] Apps interested in page_viewed: ['recommendations', 'search', 'wishlist']
[Glood Debug] Processing page_viewed event for recommendations: { url: '/products/shirt', ... }
[Glood Debug] Consent granted for recommendations, processing event

Performance Considerations

Memoization

The provider uses React’s useMemo to prevent unnecessary re-renders:
const clientConfig = useMemo(() => ({
  debug: client.debug,
  enabledApps: client.getEnabledApps(),
  appsKeys: Array.from(client.apps.keys())
}), [client.debug, client.apps]);

Event Deduplication

Events are subscribed to only once per event type, regardless of how many apps are interested:
// Subscribe once per event type
allEventTypes.forEach((eventType: EventType) => {
  subscribe(eventType, (eventData: any) => {
    // Distribute to all interested apps
    const interestedApps = enabledAppsWithPixel.filter(app =>
      app.subscribedEvents.includes(eventType)
    );

    interestedApps.forEach(app => {
      app.handleEvent(eventType, eventData, client);
    });
  });
});

Lazy Loading

Analytics setup is deferred to prevent blocking:
useEffect(() => {
  const timer = setTimeout(() => {
    // Setup analytics subscriptions
  }, 0);

  return () => clearTimeout(timer);
}, [clientConfig, analytics]);

Error Scenarios

Missing Analytics

// Hydrogen analytics not available
if (!analytics?.customerPrivacy) {
  if (debug) {
    console.log('[Glood Debug] Skipping analytics setup: analytics not available');
  }
  return;
}

Network Errors

// Pixel transmission errors are handled gracefully
try {
  app.handleEvent(eventType, eventData, client);
} catch (error) {
  console.error('[Glood] Error processing event:', error);
  if (debug) {
    console.error('[Glood Debug] Event data:', eventData);
  }
}
// Events are not processed if consent is not granted
if (checkConsent(app.pixel.consent, canTrack, analytics)) {
  app.handleEvent(eventType, eventData, client);
} else {
  if (debug) {
    console.log(`[Glood Debug] Consent not granted for ${app.name}, skipping event`);
  }
}

Best Practices

1. Single Provider Instance

Use only one GloodProvider at the root of your application:
// ✅ Good - Single provider at root
<GloodProvider client={glood} loaderData={data}>
  <App />
</GloodProvider>

// ❌ Bad - Multiple providers
<GloodProvider client={glood1}>
  <Header />
</GloodProvider>
<GloodProvider client={glood2}>
  <Main />
</GloodProvider>

2. Client Stability

Create the client outside of the component to prevent recreating:
// ✅ Good - Client created once
const glood = createGlood(config).use(recommendations());

export default function App() {
  return (
    <GloodProvider client={glood} loaderData={data}>
      <Outlet />
    </GloodProvider>
  );
}

// ❌ Bad - Client recreated on every render
export default function App() {
  const glood = createGlood(config).use(recommendations());

  return (
    <GloodProvider client={glood} loaderData={data}>
      <Outlet />
    </GloodProvider>
  );
}

3. Conditional Hook Usage

Always check for null when using the hook:
function MyComponent() {
  const glood = useGloodAnalytics();

  // ✅ Good - Check for null
  if (!glood) {
    return <FallbackComponent />;
  }

  // Use glood safely
  const apps = glood.getEnabledApps();
}

4. Error Boundaries

Wrap the provider in error boundaries for production:
import { ErrorBoundary } from 'react-error-boundary';

function ErrorFallback({ error }) {
  return (
    <div>
      <h2>Glood Error</h2>
      <pre>{error.message}</pre>
    </div>
  );
}

export default function App() {
  return (
    <ErrorBoundary FallbackComponent={ErrorFallback}>
      <Analytics>
        <GloodProvider client={glood} loaderData={data}>
          <Outlet />
        </GloodProvider>
      </Analytics>
    </ErrorBoundary>
  );
}

See Also