React SDK
Seamless integration for React and Next.js apps ⚛️
The React SDK provides a seamless integration with hooks, context, and automatic cleanup for React and Next.js applications.
Installation
npm install @twenty1tools/whatifBasic Setup
Wrap Your App
import { WhatIfProvider } from '@twenty1tools/whatif';
function App() {
return (
<WhatIfProvider
projectId="your-project-id"
user={{
id: 'user_123', // Required for smart filtering
name: 'John Doe', // Optional display data
}}
org={{
name: 'Acme Corp' // Optional organization tracking
}}
>
<YourApp />
</WhatIfProvider>
);
}Use the Hook
import { useWhatIf } from '@twenty1tools/whatif';
export function FeedbackButton() {
const { ask } = useWhatIf();
return (
<button onClick={() => ask('question-id')}>
Give Feedback
</button>
);
}Create Providers Component
'use client';
import { WhatIfProvider } from '@twenty1tools/whatif';
export function Providers({ children }: { children: React.ReactNode }) {
return (
<WhatIfProvider
projectId="your-project-id"
user={{
id: 'user_123', // Required for smart filtering
name: 'John Doe', // Optional display data
}}
org={{
name: 'Acme Corp' // Optional organization tracking
}}
>
{children}
</WhatIfProvider>
);
}Add to Layout
import { Providers } from './providers';
export default function RootLayout({ children }) {
return (
<html>
<body>
<Providers>{children}</Providers>
</body>
</html>
);
}Use in Components
'use client';
import { useWhatIf } from '@twenty1tools/whatif';
export function FeedbackButton() {
const { ask } = useWhatIf();
return <button onClick={() => ask('question-id')}>Feedback</button>;
}Wrap in _app.tsx
import { WhatIfProvider } from '@twenty1tools/whatif';
import type { AppProps } from 'next/app';
export default function App({ Component, pageProps }: AppProps) {
return (
<WhatIfProvider
projectId="your-project-id"
user={{
id: 'user_123', // Required for smart filtering
name: 'John Doe', // Optional display data
}}
org={{
name: 'Acme Corp' // Optional organization tracking
}}
>
<Component {...pageProps} />
</WhatIfProvider>
);
}Use in Pages
import { useWhatIf } from '@twenty1tools/whatif';
export default function Home() {
const { ask } = useWhatIf();
return (
<button onClick={() => ask('question-id')}>
Give Feedback
</button>
);
}Data Inheritance & Overrides
The provider sets default user/org/context data that gets automatically used by all questions, but you can override it per call:
// Provider sets defaults
<WhatIfProvider
projectId="project-id"
user={{ id: 'default-user', name: 'John', email: 'john@example.com', avatar: 'avatar.jpg' }}
org={{ name: 'Acme Corp', logo: 'logo.png' }}
context={{ app: 'my-app', version: '1.0' }}
>
<App />
</WhatIfProvider>
// In your components
function FeedbackButton() {
const { ask } = useWhatIf();
return (
<>
{/* Uses all provider defaults */}
<button onClick={() => ask('general-feedback')}>
General Feedback
</button>
{/* Overrides context for this specific question */}
<button onClick={() => ask('checkout-feedback', {
context: { page: 'checkout', step: 'completed' }
})}>
Checkout Feedback
</button>
{/* Complete override for admin context */}
<button onClick={() => ask('admin-feedback', {
user: { id: 'admin-user', name: 'Admin' },
context: { role: 'admin', page: 'admin-panel' }
})}>
Admin Feedback
</button>
</>
);
}With Theming
Add theme configuration to the provider:
import { WhatIfProvider, type ThemeConfig } from '@twenty1tools/whatif';
const theme: ThemeConfig = {
darkMode: 'auto',
colors: {
primary: '#3b82f6',
},
typography: {
fontFamily: 'Inter, sans-serif',
},
position: 'bottom-right',
};
function App() {
return (
<WhatIfProvider
projectId="your-project-id"
user={{
id: 'user_123', // Required for smart filtering
name: 'John Doe', // Optional display data
}}
org={{
name: 'Acme Corp' // Optional organization tracking
}}
theme={theme}
>
<YourApp />
</WhatIfProvider>
);
}API Reference
WhatIfProvider
Provider component that initializes the SDK.
Props:
| Prop | Type | Required | Description |
|---|---|---|---|
projectId | string | ✅ | Your project ID from dashboard |
baseUrl | string | ❌ | API base URL (default: "/api") |
user | User | ❌ | Default user data for all questions |
org | Org | ❌ | Default organization data for all questions |
context | Record<string, unknown> | ❌ | Default custom metadata for all questions |
theme | ThemeConfig | ❌ | Theme configuration |
children | ReactNode | ✅ | Your app components |
User & Organization Properties
User Object:
{
id: string; // ⚠️ REQUIRED for question filtering - prevents repeated questions
name?: string; // Optional - displays in dashboard user cards
email?: string; // Optional - displays in dashboard user cards
avatar?: string; // Optional - displays avatar in dashboard user cards
}Organization Object:
{
id?: string; // Optional - for organization tracking/identification
name: string; // Required when org provided - displays in dashboard
logo?: string; // Optional - displays logo in dashboard organization cards
}Context Object:
{
[key: string]: unknown; // Any custom metadata for your business logic
}useWhatIf Hook
Hook that provides access to the SDK.
Returns:
| Property | Type | Description |
|---|---|---|
ask | (questionId: string, options?: AskOptions) => Promise<void> | Trigger a question |
setTheme | (theme: ThemeConfig) => void | Update theme dynamically |
Ask Options:
{
user?: {
id: string; // Required: User's unique identifier for filtering
name?: string; // Optional: User's display name for dashboard
email?: string; // Optional: User's email for dashboard
avatar?: string; // Optional: URL to user's avatar for dashboard
};
org?: {
id?: string; // Optional: Organization's unique identifier
name: string; // Required: Organization's display name for dashboard
logo?: string; // Optional: URL to organization's logo for dashboard
};
context?: {
// Any custom metadata for your business logic
[key: string]: unknown;
};
userFingerprint?: string; // Anonymous user ID
}Automatic Data: WhatIf automatically captures timestamp, browser, device, page info, and previous URL with every response. See the Context Guide for details.
Common Patterns
Trigger on Event
function CheckoutComplete({ user }) {
const { ask } = useWhatIf();
useEffect(() => {
// Ask after checkout completes
if (orderCompleted) {
ask('checkout-satisfaction', {
user: {
id: user.id,
name: user.name
},
context: {
orderId: order.id,
total: order.total
}
});
}
}, [orderCompleted, ask]);
return <div>Thank you for your order!</div>;
}Trigger on User Action
function FeatureButton() {
const { ask } = useWhatIf();
const handleClick = async () => {
await performAction();
// Ask for feedback after action
ask('feature-feedback', {
context: { feature: 'new-dashboard' }
});
};
return <button onClick={handleClick}>Try New Feature</button>;
}Exit Intent
function ExitSurvey() {
const { ask } = useWhatIf();
useEffect(() => {
const handleMouseLeave = (e: MouseEvent) => {
if (e.clientY <= 0) {
ask('exit-survey');
}
};
document.addEventListener('mouseleave', handleMouseLeave);
return () => document.removeEventListener('mouseleave', handleMouseLeave);
}, [ask]);
return null;
}Time-based
function TimedFeedback() {
const { ask } = useWhatIf();
useEffect(() => {
// Ask after 30 seconds on page
const timer = setTimeout(() => {
ask('page-experience');
}, 30000);
return () => clearTimeout(timer);
}, [ask]);
return null;
}Dynamic Theming
Update theme based on your app's state:
function App() {
const { setTheme } = useWhatIf();
const [userTheme, setUserTheme] = useState<'light' | 'dark'>('light');
useEffect(() => {
setTheme({
darkMode: userTheme === 'dark' ? 'dark' : 'light'
});
}, [userTheme, setTheme]);
return (
<button onClick={() => setUserTheme(prev => prev === 'light' ? 'dark' : 'light')}>
Toggle Theme
</button>
);
}With Next-Themes
Perfect integration with next-themes:
'use client';
import { WhatIfProvider } from '@twenty1tools/whatif';
import { useTheme } from 'next-themes';
export function Providers({ children }) {
const { theme } = useTheme();
return (
<WhatIfProvider
projectId="your-project-id"
user={{
id: 'user_123', // Required for smart filtering
name: 'John Doe', // Optional display data
}}
org={{
name: 'Acme Corp' // Optional organization tracking
}}
theme={{
darkMode: theme === 'dark' ? 'dark' : 'light',
colors: { primary: '#3b82f6' }
}}
>
{children}
</WhatIfProvider>
);
}TypeScript Support
Full type safety out of the box:
import {
WhatIfProvider,
useWhatIf,
type ThemeConfig,
type ColorPalette,
type DarkMode
} from '@twenty1tools/whatif';
// All types are exported and fully typed
const theme: ThemeConfig = {
darkMode: 'auto',
colors: {
primary: '#3b82f6', // Full autocomplete
}
};
// Hook is fully typed
const { ask, setTheme } = useWhatIf();
ask('question-id'); // Type-checkedAutomatic Cleanup
The provider automatically:
- ✅ Cleans up event listeners
- ✅ Removes widgets on unmount
- ✅ Destroys SDK instances
- ✅ Prevents memory leaks
No manual cleanup needed!
Error Handling
function SafeFeedback() {
const { ask } = useWhatIf();
const handleFeedback = async () => {
try {
await ask('question-id');
} catch (error) {
console.error('Failed to show feedback:', error);
// Handle error
}
};
return <button onClick={handleFeedback}>Feedback</button>;
}Custom Hook
Create your own hooks for reusable patterns:
function useExitSurvey(questionId: string) {
const { ask } = useWhatIf();
useEffect(() => {
const handleExit = () => ask(questionId);
window.addEventListener('beforeunload', handleExit);
return () => window.removeEventListener('beforeunload', handleExit);
}, [ask, questionId]);
}
// Usage
function Page() {
useExitSurvey('exit-survey-id');
return <div>Page content</div>;
}