Skip to main content

Overview

This guide covers patterns for integrating Refine into React applications, including custom hooks, context providers, and component patterns.

Setup

Create SDK Instance

// lib/refine.ts
import { Refine } from '@refine-ai/sdk';

export const refine = new Refine({
  apiKey: process.env.NEXT_PUBLIC_REFINE_API_KEY!,
  organizationId: process.env.NEXT_PUBLIC_REFINE_ORG_ID!,
  catalogId: process.env.NEXT_PUBLIC_REFINE_CATALOG_ID!
});

Context Provider

// contexts/RefineContext.tsx
import { createContext, useContext, ReactNode } from 'react';
import { Refine } from '@refine-ai/sdk';
import { refine } from '@/lib/refine';

const RefineContext = createContext<Refine | null>(null);

export function RefineProvider({ children }: { children: ReactNode }) {
  return (
    <RefineContext.Provider value={refine}>
      {children}
    </RefineContext.Provider>
  );
}

export function useRefine(): Refine {
  const context = useContext(RefineContext);
  if (!context) {
    throw new Error('useRefine must be used within RefineProvider');
  }
  return context;
}

Custom Hooks

useSearch

// hooks/useSearch.ts
import { useState, useCallback, useRef } from 'react';
import { refine } from '@/lib/refine';
import type { SearchResponse, Filter, SortBy } from '@refine-ai/sdk';

interface UseSearchOptions {
  topK?: number;
  visualWeight?: number;
  filters?: Filter[];
  sortBy?: SortBy;
}

interface UseSearchResult {
  results: SearchResponse['results'];
  totalResults: number;
  isLoading: boolean;
  error: Error | null;
  search: (query: string) => Promise<void>;
  trackClick: (productId: string, position: number) => void;
  trackView: (productId: string, position: number) => void;
  trackAddToCart: (productId: string) => void;
}

export function useSearch(options: UseSearchOptions = {}): UseSearchResult {
  const [results, setResults] = useState<SearchResponse['results']>([]);
  const [totalResults, setTotalResults] = useState(0);
  const [isLoading, setIsLoading] = useState(false);
  const [error, setError] = useState<Error | null>(null);
  const contextRef = useRef<any>(null);

  const search = useCallback(async (query: string) => {
    if (!query.trim()) {
      setResults([]);
      setTotalResults(0);
      return;
    }

    setIsLoading(true);
    setError(null);

    try {
      const response = await refine.search.text({
        query,
        topK: options.topK || 24,
        visualWeight: options.visualWeight,
        filters: options.filters,
        sortBy: options.sortBy
      });

      setResults(response.results);
      setTotalResults(response.totalResults);

      // Create tracking context
      contextRef.current = refine.events.trackSearch(query, response.results, {
        surface: 'search_results',
        totalResults: response.totalResults
      });
    } catch (err) {
      setError(err as Error);
      setResults([]);
      setTotalResults(0);
    } finally {
      setIsLoading(false);
    }
  }, [options.topK, options.visualWeight, options.filters, options.sortBy]);

  const trackClick = useCallback((productId: string, position: number) => {
    contextRef.current?.trackClick(productId, position);
  }, []);

  const trackView = useCallback((productId: string, position: number) => {
    contextRef.current?.trackView(productId, position);
  }, []);

  const trackAddToCart = useCallback((productId: string) => {
    contextRef.current?.trackAddToCart(productId);
  }, []);

  return {
    results,
    totalResults,
    isLoading,
    error,
    search,
    trackClick,
    trackView,
    trackAddToCart
  };
}

useRecommendations

// hooks/useRecommendations.ts
import { useState, useCallback, useEffect, useRef } from 'react';
import { refine } from '@/lib/refine';
import type { RecommendationResponse } from '@refine-ai/sdk';

interface UseRecommendationsResult {
  recommendations: RecommendationResponse['results'];
  isLoading: boolean;
  error: Error | null;
  trackClick: (productId: string, position: number) => void;
  trackAddToCart: (productId: string) => void;
}

export function useSimilarItems(
  anchorId: string,
  topK: number = 8
): UseRecommendationsResult {
  const [recommendations, setRecommendations] = useState<any[]>([]);
  const [isLoading, setIsLoading] = useState(true);
  const [error, setError] = useState<Error | null>(null);
  const contextRef = useRef<any>(null);

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

    setIsLoading(true);
    setError(null);

    refine.recs.similarItems({ anchorId, topK })
      .then(response => {
        setRecommendations(response.results);
        contextRef.current = refine.events.trackRecommendations(
          response.serveId,
          response.results,
          'product_page',
          'similar-items',
          { anchorId }
        );
      })
      .catch(err => {
        setError(err);
        setRecommendations([]);
      })
      .finally(() => setIsLoading(false));
  }, [anchorId, topK]);

  const trackClick = useCallback((productId: string, position: number) => {
    contextRef.current?.trackClick(productId, position);
  }, []);

  const trackAddToCart = useCallback((productId: string) => {
    contextRef.current?.trackAddToCart(productId);
  }, []);

  return { recommendations, isLoading, error, trackClick, trackAddToCart };
}

export function useVisitorRecommendations(
  configId: string,
  topK: number = 12
): UseRecommendationsResult {
  const [recommendations, setRecommendations] = useState<any[]>([]);
  const [isLoading, setIsLoading] = useState(true);
  const [error, setError] = useState<Error | null>(null);
  const contextRef = useRef<any>(null);

  useEffect(() => {
    refine.recs.forVisitor({ configId, topK })
      .then(response => {
        setRecommendations(response.results);
        contextRef.current = refine.events.trackRecommendations(
          response.serveId,
          response.results,
          'home_page',
          'visitor-recs'
        );
      })
      .catch(err => {
        setError(err);
        setRecommendations([]);
      })
      .finally(() => setIsLoading(false));
  }, [configId, topK]);

  const trackClick = useCallback((productId: string, position: number) => {
    contextRef.current?.trackClick(productId, position);
  }, []);

  const trackAddToCart = useCallback((productId: string) => {
    contextRef.current?.trackAddToCart(productId);
  }, []);

  return { recommendations, isLoading, error, trackClick, trackAddToCart };
}

useViewability

// hooks/useViewability.ts
import { useEffect, useRef } from 'react';

export function useViewability(
  onView: (productId: string, position: number) => void,
  threshold: number = 0.5,
  duration: number = 1000
) {
  const observerRef = useRef<IntersectionObserver | null>(null);
  const viewedRef = useRef<Set<string>>(new Set());
  const timersRef = useRef<Map<string, NodeJS.Timeout>>(new Map());

  useEffect(() => {
    observerRef.current = new IntersectionObserver(
      (entries) => {
        entries.forEach(entry => {
          const el = entry.target as HTMLElement;
          const productId = el.dataset.productId;
          const position = parseInt(el.dataset.position || '0');

          if (!productId || viewedRef.current.has(productId)) return;

          if (entry.isIntersecting) {
            const timer = setTimeout(() => {
              if (viewedRef.current.has(productId)) return;
              viewedRef.current.add(productId);
              onView(productId, position);
            }, duration);
            timersRef.current.set(productId, timer);
          } else {
            const timer = timersRef.current.get(productId);
            if (timer) {
              clearTimeout(timer);
              timersRef.current.delete(productId);
            }
          }
        });
      },
      { threshold }
    );

    return () => {
      observerRef.current?.disconnect();
      timersRef.current.forEach(clearTimeout);
    };
  }, [onView, threshold, duration]);

  const observe = useCallback((element: HTMLElement | null) => {
    if (element) {
      observerRef.current?.observe(element);
    }
  }, []);

  return { observe };
}

Component Patterns

Search Results Page

// components/SearchPage.tsx
import { useState, FormEvent } from 'react';
import { useSearch } from '@/hooks/useSearch';
import { ProductCard } from './ProductCard';

export function SearchPage() {
  const [query, setQuery] = useState('');
  const { 
    results, 
    totalResults, 
    isLoading, 
    error, 
    search,
    trackClick,
    trackView,
    trackAddToCart
  } = useSearch({ topK: 24 });

  const handleSubmit = (e: FormEvent) => {
    e.preventDefault();
    search(query);
  };

  return (
    <div className="search-page">
      <form onSubmit={handleSubmit} className="search-form">
        <input
          type="search"
          value={query}
          onChange={(e) => setQuery(e.target.value)}
          placeholder="Search products..."
        />
        <button type="submit">Search</button>
      </form>

      {isLoading && <div className="loading">Searching...</div>}
      
      {error && <div className="error">Search failed. Please try again.</div>}

      {results.length > 0 && (
        <>
          <p className="results-count">
            Showing {results.length} of {totalResults} results
          </p>
          <div className="product-grid">
            {results.map((product, index) => (
              <ProductCard
                key={product.productId}
                product={product}
                position={index}
                onView={() => trackView(product.productId, index)}
                onClick={() => trackClick(product.productId, index)}
                onAddToCart={() => trackAddToCart(product.productId)}
              />
            ))}
          </div>
        </>
      )}
    </div>
  );
}

Product Card with Viewability

// components/ProductCard.tsx
import { useRef, useEffect } from 'react';

interface ProductCardProps {
  product: {
    productId: string;
    title: string;
    price: number;
    imageUrl: string;
  };
  position: number;
  onView: () => void;
  onClick: () => void;
  onAddToCart: () => void;
}

export function ProductCard({ 
  product, 
  position, 
  onView, 
  onClick, 
  onAddToCart 
}: ProductCardProps) {
  const cardRef = useRef<HTMLDivElement>(null);
  const hasViewed = useRef(false);

  useEffect(() => {
    const card = cardRef.current;
    if (!card) return;

    const observer = new IntersectionObserver(
      ([entry]) => {
        if (entry.isIntersecting && !hasViewed.current) {
          const timer = setTimeout(() => {
            if (!hasViewed.current) {
              hasViewed.current = true;
              onView();
            }
          }, 1000);

          return () => clearTimeout(timer);
        }
      },
      { threshold: 0.5 }
    );

    observer.observe(card);
    return () => observer.disconnect();
  }, [onView]);

  const handleClick = () => {
    onClick();
    window.location.href = `/products/${product.productId}`;
  };

  const handleAddToCart = (e: React.MouseEvent) => {
    e.stopPropagation();
    onAddToCart();
    // Add to cart logic
  };

  return (
    <div 
      ref={cardRef}
      className="product-card"
      data-product-id={product.productId}
      data-position={position}
      onClick={handleClick}
    >
      <img src={product.imageUrl} alt={product.title} />
      <h3>{product.title}</h3>
      <p className="price">${product.price.toFixed(2)}</p>
      <button onClick={handleAddToCart}>Add to Cart</button>
    </div>
  );
}
// components/SimilarItems.tsx
import { useSimilarItems } from '@/hooks/useRecommendations';
import { ProductCard } from './ProductCard';

interface SimilarItemsProps {
  productId: string;
}

export function SimilarItems({ productId }: SimilarItemsProps) {
  const { 
    recommendations, 
    isLoading, 
    error,
    trackClick,
    trackAddToCart
  } = useSimilarItems(productId, 8);

  if (isLoading) {
    return <div className="loading">Loading recommendations...</div>;
  }

  if (error || recommendations.length === 0) {
    return null;
  }

  return (
    <section className="similar-items">
      <h2>You May Also Like</h2>
      <div className="carousel">
        {recommendations.map((product, index) => (
          <ProductCard
            key={product.productId}
            product={product}
            position={index}
            onView={() => {}}
            onClick={() => trackClick(product.productId, index)}
            onAddToCart={() => trackAddToCart(product.productId)}
          />
        ))}
      </div>
    </section>
  );
}

Identity Management

Auth Integration

// hooks/useRefineAuth.ts
import { useEffect } from 'react';
import { refine } from '@/lib/refine';
import { useAuth } from '@/contexts/AuthContext';

export function useRefineAuth() {
  const { user, isAuthenticated } = useAuth();

  useEffect(() => {
    if (isAuthenticated && user?.id) {
      refine.identify(user.id);
    }
  }, [isAuthenticated, user?.id]);

  const logout = () => {
    refine.reset();
  };

  return { logout };
}

Provider with Auth

// app/layout.tsx
'use client';

import { RefineProvider } from '@/contexts/RefineContext';
import { AuthProvider } from '@/contexts/AuthContext';
import { useRefineAuth } from '@/hooks/useRefineAuth';

function RefineAuthSync({ children }) {
  useRefineAuth();
  return children;
}

export default function RootLayout({ children }) {
  return (
    <html>
      <body>
        <AuthProvider>
          <RefineProvider>
            <RefineAuthSync>
              {children}
            </RefineAuthSync>
          </RefineProvider>
        </AuthProvider>
      </body>
    </html>
  );
}

Purchase Tracking

// hooks/usePurchaseTracking.ts
import { useCallback } from 'react';
import { refine } from '@/lib/refine';

interface OrderItem {
  productId: string;
  quantity: number;
  price: number;
}

interface Order {
  id: string;
  total: number;
  currency: string;
  items: OrderItem[];
}

export function usePurchaseTracking() {
  const trackPurchase = useCallback(async (order: Order) => {
    refine.events.trackPurchase({
      orderId: order.id,
      value: order.total,
      currency: order.currency,
      items: order.items.map(item => ({
        itemId: item.productId,
        quantity: item.quantity,
        unitPrice: item.price
      }))
    });

    await refine.events.flush();
  }, []);

  return { trackPurchase };
}

Next Steps