Skip to main content

Documentation Index

Fetch the complete documentation index at: https://docs.joinrefine.io/llms.txt

Use this file to discover all available pages before exploring further.

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

Server-Side Usage

SSR and API patterns

Event Tracking

Complete event tracking guide