Overview
This guide covers patterns for integrating Refine into React applications, including custom hooks, context providers, and component patterns.Setup
Create SDK Instance
Copy
// 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
Copy
// 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
Copy
// 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
Copy
// 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
Copy
// 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
Copy
// 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
Copy
// 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>
);
}
Similar Items Carousel
Copy
// 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
Copy
// 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
Copy
// 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
Copy
// 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 };
}