React Integration Example#
Integrate InterviewRelay AI interviews into your React application with our SDK. This guide covers everything from basic setup to advanced patterns with TypeScript, React Router, and Next.js.
Installation#
Install the InterviewRelay React SDK:
npm install @interviewrelay/react-sdk
Or with yarn:
yarn add @interviewrelay/react-sdk
Or with pnpm:
pnpm add @interviewrelay/react-sdk
Complete Example Component#
Here's a fully-featured interview component with TypeScript, error handling, and loading states:
import React, { useCallback, useState } from 'react';
import { InterviewWidget, InterviewResult, InterviewError } from '@interviewrelay/react-sdk';
interface InterviewPageProps {
inviteToken: string;
onInterviewComplete?: (result: InterviewResult) => void;
}
export function InterviewPage({ inviteToken, onInterviewComplete }: InterviewPageProps) {
const [status, setStatus] = useState<'loading' | 'ready' | 'active' | 'completed' | 'error'>('loading');
const [error, setError] = useState<string | null>(null);
const [result, setResult] = useState<InterviewResult | null>(null);
const handleReady = useCallback(() => {
setStatus('ready');
}, []);
const handleStart = useCallback(() => {
setStatus('active');
}, []);
const handleComplete = useCallback((result: InterviewResult) => {
setStatus('completed');
setResult(result);
onInterviewComplete?.(result);
}, [onInterviewComplete]);
const handleError = useCallback((error: InterviewError) => {
setStatus('error');
setError(error.message);
console.error('Interview error:', error);
}, []);
if (status === 'completed' && result) {
return (
<div className="flex flex-col items-center justify-center min-h-[500px] text-center p-8">
<div className="text-green-500 text-5xl mb-4">✓</div>
<h2 className="text-2xl font-bold mb-2">Thank You!</h2>
<p className="text-gray-400 mb-4">Your interview has been recorded successfully.</p>
<div className="text-sm text-gray-500">
<p>Duration: {Math.round(result.duration / 60)} minutes</p>
<p>Questions answered: {result.questionsAnswered}</p>
</div>
</div>
);
}
if (status === 'error') {
return (
<div className="flex flex-col items-center justify-center min-h-[500px] text-center p-8">
<div className="text-red-500 text-5xl mb-4">✕</div>
<h2 className="text-2xl font-bold mb-2">Something Went Wrong</h2>
<p className="text-gray-400 mb-4">{error}</p>
<button
onClick={() => window.location.reload()}
className="px-4 py-2 bg-indigo-600 text-white rounded-lg hover:bg-indigo-700"
>
Try Again
</button>
</div>
);
}
return (
<div className="w-full max-w-4xl mx-auto">
{status === 'loading' && (
<div className="flex items-center justify-center min-h-[500px]">
<div className="animate-spin rounded-full h-8 w-8 border-b-2 border-indigo-500" />
</div>
)}
<InterviewWidget
inviteToken={inviteToken}
theme={{
primaryColor: '#6366f1',
backgroundColor: '#111111',
textColor: '#ffffff',
borderRadius: '12px',
}}
onReady={handleReady}
onStart={handleStart}
onComplete={handleComplete}
onError={handleError}
/>
</div>
);
}
Usage in Your App#
import React from 'react';
import { InterviewPage } from './components/InterviewPage';
function App() {
const handleComplete = (result) => {
console.log('Interview completed:', result);
// Send to your analytics, redirect, etc.
};
return (
<div className="min-h-screen bg-gray-950 text-white">
<header className="p-4 border-b border-gray-800">
<h1 className="text-xl font-semibold">Your Company</h1>
</header>
<main className="p-8">
<InterviewPage
inviteToken="inv_abc123xyz"
onInterviewComplete={handleComplete}
/>
</main>
</div>
);
}
export default App;
With React Router#
Use React Router to create a dedicated interview route with the token in the URL:
import React from 'react';
import { BrowserRouter, Routes, Route, useSearchParams, Navigate } from 'react-router-dom';
import { InterviewPage } from './components/InterviewPage';
function InterviewRoute() {
const [searchParams] = useSearchParams();
const token = searchParams.get('token');
if (!token) {
return (
<div className="flex items-center justify-center min-h-screen">
<div className="text-center">
<h2 className="text-2xl font-bold text-red-400 mb-2">Missing Token</h2>
<p className="text-gray-400">
No invite token found. Please use the link from your invitation email.
</p>
</div>
</div>
);
}
return <InterviewPage inviteToken={token} />;
}
function ThankYouPage() {
return (
<div className="flex items-center justify-center min-h-screen">
<div className="text-center">
<h2 className="text-2xl font-bold text-green-400 mb-2">Thank You!</h2>
<p className="text-gray-400">Your interview has been submitted.</p>
</div>
</div>
);
}
function App() {
return (
<BrowserRouter>
<Routes>
<Route path="/interview" element={<InterviewRoute />} />
<Route path="/thank-you" element={<ThankYouPage />} />
<Route path="*" element={<Navigate to="/interview" />} />
</Routes>
</BrowserRouter>
);
}
export default App;
Invite links would look like: https://yourapp.com/interview?token=inv_abc123xyz
With Next.js#
App Router (Next.js 13+)#
Create a client component for the interview widget:
// app/interview/page.tsx
'use client';
import { useSearchParams } from 'next/navigation';
import { Suspense } from 'react';
import { InterviewPage } from '@/components/InterviewPage';
function InterviewContent() {
const searchParams = useSearchParams();
const token = searchParams.get('token');
if (!token) {
return (
<div className="flex items-center justify-center min-h-screen">
<p className="text-red-400">No invite token provided.</p>
</div>
);
}
return (
<div className="min-h-screen bg-gray-950 text-white p-8">
<InterviewPage inviteToken={token} />
</div>
);
}
export default function InterviewPage() {
return (
<Suspense fallback={<div className="flex items-center justify-center min-h-screen">Loading...</div>}>
<InterviewContent />
</Suspense>
);
}
Server-Side Token Generation (Next.js API Route)#
Generate invite tokens securely on the server:
// app/api/create-invite/route.ts
import { NextRequest, NextResponse } from 'next/server';
export async function POST(request: NextRequest) {
const { email, campaignId } = await request.json();
const response = await fetch('https://api.interviewrelay.com/v1/invites', {
method: 'POST',
headers: {
'Authorization': `Bearer ${process.env.INTERVIEWRELAY_API_KEY}`,
'Content-Type': 'application/json',
},
body: JSON.stringify({
campaign_id: campaignId,
participant_email: email,
}),
});
if (!response.ok) {
return NextResponse.json(
{ error: 'Failed to create invite' },
{ status: response.status }
);
}
const data = await response.json();
return NextResponse.json({
invite_token: data.invite_token,
expires_at: data.expires_at,
});
}
Client Component Required
The InterviewWidget component uses browser APIs (DOM, WebRTC) and must be rendered as a client component. Always add 'use client' at the top of the file or wrap it in a client component boundary.
Fetching Tokens Dynamically#
Fetch invite tokens from your backend on demand:
import React, { useEffect, useState } from 'react';
import { InterviewPage } from './components/InterviewPage';
interface DynamicInterviewProps {
participantEmail: string;
campaignId: string;
}
export function DynamicInterview({ participantEmail, campaignId }: DynamicInterviewProps) {
const [token, setToken] = useState<string | null>(null);
const [loading, setLoading] = useState(true);
const [error, setError] = useState<string | null>(null);
useEffect(() => {
async function fetchToken() {
try {
const response = await fetch('/api/create-invite', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
email: participantEmail,
campaignId: campaignId,
}),
});
if (!response.ok) {
throw new Error('Failed to create invite');
}
const data = await response.json();
setToken(data.invite_token);
} catch (err) {
setError(err instanceof Error ? err.message : 'Unknown error');
} finally {
setLoading(false);
}
}
fetchToken();
}, [participantEmail, campaignId]);
if (loading) {
return (
<div className="flex items-center justify-center min-h-[500px]">
<div className="animate-spin rounded-full h-8 w-8 border-b-2 border-indigo-500" />
</div>
);
}
if (error) {
return (
<div className="flex items-center justify-center min-h-[500px]">
<p className="text-red-400">{error}</p>
</div>
);
}
if (!token) {
return null;
}
return <InterviewPage inviteToken={token} />;
}
TypeScript Types#
The SDK ships with full TypeScript definitions. Here are the key types:
import type {
InterviewResult,
InterviewError,
InterviewTheme,
InterviewWidgetProps,
InterviewStatus,
} from '@interviewrelay/react-sdk';
// Interview result returned on completion
interface InterviewResult {
sessionId: string;
duration: number; // Duration in seconds
questionsAnswered: number;
questionsTotal: number;
completionStatus: 'completed' | 'timed_out' | 'abandoned';
}
// Error object passed to onError callback
interface InterviewError {
code: string; // e.g., 'NETWORK_ERROR', 'TOKEN_EXPIRED'
message: string; // Human-readable error message
details?: Record<string, unknown>;
}
// Theme customization options
interface InterviewTheme {
primaryColor?: string;
backgroundColor?: string;
textColor?: string;
secondaryTextColor?: string;
borderRadius?: string;
fontFamily?: string;
}
// Widget component props
interface InterviewWidgetProps {
inviteToken: string;
theme?: InterviewTheme;
locale?: string;
autoStart?: boolean;
onReady?: () => void;
onStart?: () => void;
onComplete?: (result: InterviewResult) => void;
onError?: (error: InterviewError) => void;
}
Custom Hooks#
useInterview#
Create a reusable hook to manage interview state:
import { useState, useCallback } from 'react';
import type { InterviewResult, InterviewError } from '@interviewrelay/react-sdk';
type InterviewStatus = 'idle' | 'loading' | 'ready' | 'active' | 'completed' | 'error';
interface UseInterviewReturn {
status: InterviewStatus;
result: InterviewResult | null;
error: string | null;
handlers: {
onReady: () => void;
onStart: () => void;
onComplete: (result: InterviewResult) => void;
onError: (error: InterviewError) => void;
};
reset: () => void;
}
export function useInterview(): UseInterviewReturn {
const [status, setStatus] = useState<InterviewStatus>('idle');
const [result, setResult] = useState<InterviewResult | null>(null);
const [error, setError] = useState<string | null>(null);
const onReady = useCallback(() => {
setStatus('ready');
}, []);
const onStart = useCallback(() => {
setStatus('active');
}, []);
const onComplete = useCallback((result: InterviewResult) => {
setStatus('completed');
setResult(result);
}, []);
const onError = useCallback((err: InterviewError) => {
setStatus('error');
setError(err.message);
}, []);
const reset = useCallback(() => {
setStatus('idle');
setResult(null);
setError(null);
}, []);
return {
status,
result,
error,
handlers: { onReady, onStart, onComplete, onError },
reset,
};
}
Usage with the hook:
import { InterviewWidget } from '@interviewrelay/react-sdk';
import { useInterview } from './hooks/useInterview';
function Interview({ token }: { token: string }) {
const { status, result, error, handlers, reset } = useInterview();
if (status === 'completed') {
return (
<div>
<h2>Interview Complete</h2>
<p>Duration: {Math.round(result!.duration / 60)} min</p>
<button onClick={reset}>Start Over</button>
</div>
);
}
if (status === 'error') {
return (
<div>
<p className="text-red-500">{error}</p>
<button onClick={reset}>Retry</button>
</div>
);
}
return (
<InterviewWidget
inviteToken={token}
{...handlers}
/>
);
}
Best Practices#
Error Boundaries#
Wrap the interview widget in an error boundary to catch unexpected rendering errors:
import React, { Component, ErrorInfo, ReactNode } from 'react';
interface ErrorBoundaryProps {
children: ReactNode;
fallback?: ReactNode;
}
interface ErrorBoundaryState {
hasError: boolean;
error: Error | null;
}
class InterviewErrorBoundary extends Component<ErrorBoundaryProps, ErrorBoundaryState> {
constructor(props: ErrorBoundaryProps) {
super(props);
this.state = { hasError: false, error: null };
}
static getDerivedStateFromError(error: Error): ErrorBoundaryState {
return { hasError: true, error };
}
componentDidCatch(error: Error, errorInfo: ErrorInfo) {
console.error('Interview widget error:', error, errorInfo);
// Report to your error tracking service
}
render() {
if (this.state.hasError) {
return this.props.fallback || (
<div className="p-8 text-center">
<h3 className="text-red-400 font-bold">Interview Widget Error</h3>
<p className="text-gray-400 mt-2">
Something went wrong loading the interview.
</p>
<button
onClick={() => this.setState({ hasError: false, error: null })}
className="mt-4 px-4 py-2 bg-indigo-600 rounded-lg"
>
Try Again
</button>
</div>
);
}
return this.props.children;
}
}
// Usage
function App() {
return (
<InterviewErrorBoundary>
<InterviewPage inviteToken="inv_abc123" />
</InterviewErrorBoundary>
);
}
Loading States#
Always show a loading state while the widget initializes:
function InterviewWithLoader({ token }: { token: string }) {
const [isReady, setIsReady] = useState(false);
return (
<div className="relative">
{!isReady && (
<div className="absolute inset-0 flex items-center justify-center bg-gray-950 z-10">
<div className="text-center">
<div className="animate-spin rounded-full h-10 w-10 border-b-2 border-indigo-500 mx-auto mb-4" />
<p className="text-gray-400">Preparing your interview...</p>
</div>
</div>
)}
<InterviewWidget
inviteToken={token}
onReady={() => setIsReady(true)}
/>
</div>
);
}
Cleanup#
The InterviewWidget component handles its own cleanup when unmounted. However, if you need to programmatically destroy the widget:
import { useRef } from 'react';
import { InterviewWidget, InterviewWidgetRef } from '@interviewrelay/react-sdk';
function Interview({ token }: { token: string }) {
const widgetRef = useRef<InterviewWidgetRef>(null);
const handleCancel = () => {
// Gracefully stop the interview
widgetRef.current?.destroy();
};
return (
<div>
<InterviewWidget ref={widgetRef} inviteToken={token} />
<button onClick={handleCancel}>Cancel Interview</button>
</div>
);
}
Automatic Cleanup
You don't need to manually clean up in most cases. The InterviewWidget component automatically disconnects WebRTC connections, stops media streams, and releases resources when the component unmounts.
Next Steps#
- Vue Integration — See the Vue.js equivalent
- Static HTML Example — Use the SDK without a framework
- SDK Reference — Full SDK API documentation
- Webhooks — Get notified server-side when interviews complete
- Authentication — Secure your API requests