React & Next.js Integration
Framework GuideDrop Widgetfied widgets into your React app in minutes with our simple component pattern.
The Simplest Way
Just import to your React app & use it like any other component. 1. Copy our hook file 2. Import the widget you need 3. Drop it in your JSX That's it.
Quick Start (2 Minutes)
Copy this hook file, then use widgets like any React component:
1. Create the Hook File
// hooks/useWidgetfied.jsx
import React, { useEffect } from 'react';
// Script loading state (shared globally)
let scriptLoaded = false;
let scriptLoading = false;
function loadScript() {
if (scriptLoaded || scriptLoading) return;
scriptLoading = true;
const script = document.createElement('script');
script.src = 'https://cdn.widgetfied.com/portal.js';
script.async = false;
script.onload = () => {
scriptLoaded = true;
scriptLoading = false;
setTimeout(() => window.Widgetfied?.init?.(), 100);
};
script.onerror = () => {
scriptLoading = false;
};
document.body.appendChild(script);
}
function useWidgetInit() {
useEffect(() => {
if (window.Widgetfied) {
window.Widgetfied.init();
} else {
loadScript();
}
}, []);
}
// Booking Widget Component
export function BookingWidget({ className = '' }) {
useWidgetInit();
return (
<div
id="booking-widget"
data-widget="booking"
data-tenant="YOUR_TENANT_ID"
data-container="booking-widget"
data-display-mode="modal"
className={className}
/>
);
}
// Job Portal Widget Component
export function JobPortalWidget({ className = '' }) {
useWidgetInit();
return (
<div
id="portal-widget"
data-widget="jobportal"
data-tenant="YOUR_TENANT_ID"
data-container="portal-widget"
data-display-mode="modal"
className={className}
/>
);
}
// Estimate Widget Component
export function EstimateWidget({ className = '' }) {
useWidgetInit();
return (
<div
id="estimate-widget"
data-widget="estimate"
data-tenant="YOUR_TENANT_ID"
data-container="estimate-widget"
data-display-mode="modal"
className={className}
/>
);
}
// Payment Widget Component
export function PaymentWidget({ className = '' }) {
useWidgetInit();
return (
<div
id="payment-widget"
data-widget="payment"
data-tenant="YOUR_TENANT_ID"
data-container="payment-widget"
data-display-mode="modal"
className={className}
/>
);
}2. Use It Like Any Component
// pages/booking.jsx
import { BookingWidget } from '../hooks/useWidgetfied';
export default function BookingPage() {
return (
<div className="container mx-auto">
<h1 className="text-2xl font-bold mb-6">Book a Service</h1>
<BookingWidget className="min-h-[400px]" />
</div>
);
}That's It!
The hook handles:
- Script loading (only once)
- Widget initialization
- Lifecycle management
- Multiple widgets on the same page
- Navigation between pages
// Customize with data attributes:
<BookingWidget
className="min-h-[500px] border rounded-lg p-4"
data-display-mode="modal"
/>
// Or create your own variant:
export function MyCustomWidget({ tenantId, displayMode = 'modal', ...props }) {
useWidgetInit();
return (
<div
data-widget="booking"
data-tenant={tenantId}
data-display-mode={displayMode}
data-global-name="myCustomWidget"
className={props.className}
/>
);
}Display Modes
Choose how the widget renders: modal | inline | button
modal ✓Opens as a fullscreen overlay at the document root level. Escapes all CSS constraints.
inlineRenders directly inside the container element. Subject to parent CSS (overflow, transforms, etc).
buttonShows a trigger button. When clicked, opens the widget as a modal overlay.
Use modal or button mode when embedding in sites with deep nesting (10+ layers), overflow: hidden, transform, or backdrop-filter CSS properties. These modes use React portals to render at the document root, escaping any CSS containing blocks.
// Modal mode (default) - opens as overlay, escapes CSS constraints
<BookingWidget data-display-mode="modal" />
// Inline mode - renders in place, affected by parent CSS
<BookingWidget data-display-mode="inline" />
// Button mode - shows button, opens modal on click
<BookingWidget data-display-mode="button" />SPA Navigation (Important)
In Single Page Applications (React Router, Next.js App Router, etc.), route changes don't trigger a full page reload. The Widgetfied script loads once, but when React unmounts and remounts widget containers during navigation, the script must re-scan the DOM to pick up new containers.
The Problem: If you use useEffect with an empty dependency array [], the init call only runs on first mount. Navigating away and back causes widgets to disappear because Widgetfied doesn't know about the newly-mounted containers.
Solution: Remove the dependency array
Let the effect run on every render so Widgetfied.init() is called whenever a widget component mounts after SPA navigation:
// SPA-safe hook — re-initializes on every mount/navigation
function useWidgetInit() {
const mountedRef = useRef(false);
const initWidget = useCallback(() => {
if (window.Widgetfied?.init) {
window.Widgetfied.init();
}
}, []);
useEffect(() => {
mountedRef.current = true;
if (scriptLoaded && window.Widgetfied) {
// Script loaded — re-init after a tick so DOM container is painted
const timer = setTimeout(initWidget, 50);
return () => { mountedRef.current = false; clearTimeout(timer); };
}
// Script not ready — load it, init when done
loadScript(() => {
if (mountedRef.current) setTimeout(initWidget, 50);
});
return () => { mountedRef.current = false; };
}); // ← No dependency array! Runs on every render/mount
}This is safe because Widgetfied.init() is idempotent — calling it multiple times only processes uninitialized containers. The 50ms delay ensures the DOM element is fully painted before scanning.
Bonus: Prefetch the script in your HTML
Add these to your index.html <head> so the browser starts fetching before React even mounts:
<!-- Preload widget script + DNS prefetch for API calls -->
<link rel="preload" href="https://cdn.widgetfied.com/portal.js" as="script" crossorigin />
<link rel="dns-prefetch" href="https://cdn.widgetfied.com" />
<link rel="dns-prefetch" href="https://www.widgetfied.com" />Advanced Configuration
Global Variable Names
Use data-global-name for multiple widget instances or custom programmatic access:
// Multiple instances with different global names
<BookingWidget data-global-name="bookingWidget1" />
<BookingWidget data-global-name="bookingWidget2" />
// Access programmatically
window.bookingWidget1?.open?.();
window.bookingWidget2?.close?.();Default global name is Widgetfied
All Data Attributes
data-widgetRequired: Widget type (booking, jobportal, etc.)data-tenantRequired: Your tenant IDdata-containerRequired: Unique container identifierdata-display-modeOptional: modal | inline | button (default: modal)data-global-nameOptional: Custom global variable nameLocal Development Setup
For local development, add this proxy to your Vite config:
Required: All these proxy routes must be configured for widgets to work locally.
// vite.config.js
export default defineConfig({
server: {
proxy: {
'/api/tenant-config': {
target: 'https://www.widgetfied.com',
changeOrigin: true,
secure: true,
},
'/api/calendar': {
target: 'https://www.widgetfied.com',
changeOrigin: true,
secure: true,
},
'/api/payments': {
target: 'https://www.widgetfied.com',
changeOrigin: true,
secure: true,
},
'/api/portal': {
target: 'https://www.widgetfied.com',
changeOrigin: true,
secure: true,
},
'/api/email': {
target: 'https://www.widgetfied.com',
changeOrigin: true,
secure: true,
},
'/api/tenants': {
target: 'https://www.widgetfied.com',
changeOrigin: true,
secure: true,
}
}
}
})These proxy routes are required for widgets to communicate with Widgetfied APIs during development.
Content Security Policy
If using CSP headers, add these required domains for Widgetfied:
Essential CSP Directives:
// Minimal CSP additions for Widgetfied widgets:
'Content-Security-Policy': `
script-src 'self' 'unsafe-inline' 'unsafe-eval'
https://cdn.widgetfied.com https://*.widgetfied.com;
connect-src 'self'
https://widgetfied.com https://*.widgetfied.com;
`Full Vite Development CSP Example
// vite.config.js - Complete Development CSP
server: {
headers: {
'Content-Security-Policy': `
default-src 'self';
script-src 'self' 'unsafe-inline' 'unsafe-eval'
https://cdn.widgetfied.com https://*.widgetfied.com
https://js.stripe.com
https://www.googletagmanager.com
https://www.google-analytics.com;
connect-src 'self' ws: wss:
https://widgetfied.com https://*.widgetfied.com;
style-src 'self' 'unsafe-inline';
frame-src 'self'
https://js.stripe.com
https://hooks.stripe.com;
`
}
}Next.js Setup
Same pattern works in Next.js. Just mark your component as client:
// app/booking/page.js
'use client';
import { BookingWidget } from '@/hooks/useWidgetfied';
export default function BookingPage() {
return (
<div className="container mx-auto p-8">
<h1 className="text-3xl font-bold mb-6">Schedule Your Service</h1>
<BookingWidget className="min-h-[500px]" />
</div>
);
}Required: Add this configuration to your next.config.js for widgets to work:
// next.config.js - Required API Proxy & CSP
module.exports = {
async rewrites() {
return [
{
source: '/api/tenant-config/:path*',
destination: 'https://www.widgetfied.com/api/tenant-config/:path*',
},
{
source: '/api/calendar/:path*',
destination: 'https://www.widgetfied.com/api/calendar/:path*',
},
{
source: '/api/payments/:path*',
destination: 'https://www.widgetfied.com/api/payments/:path*',
},
{
source: '/api/portal/:path*',
destination: 'https://www.widgetfied.com/api/portal/:path*',
},
{
source: '/api/email/:path*',
destination: 'https://www.widgetfied.com/api/email/:path*',
},
{
source: '/api/tenants/:path*',
destination: 'https://www.widgetfied.com/api/tenants/:path*',
}
];
},
async headers() {
return [
{
source: '/:path*',
headers: [
{
key: 'Content-Security-Policy',
value: `
script-src 'self' 'unsafe-inline' 'unsafe-eval'
https://cdn.widgetfied.com https://*.widgetfied.com
https://js.stripe.com;
connect-src 'self'
https://widgetfied.com https://*.widgetfied.com;
`.replace(/\s+/g, ' ').trim()
}
]
}
];
}
};Common Issues
Widget not showing?
- →Check your tenant ID is correct
- →Make sure the container has a minimum height
- →Verify the script loaded (check Network tab)
CORS or API errors?
- →Add the proxy config to your dev server
- →Check your tenant is active in the dashboard
Widgets disappear after SPA navigation?
- →Remove the empty [] dependency array from useEffect — see "SPA Navigation" section above
- →Ensure useWidgetInit() runs on every render so Widgetfied.init() re-scans for new containers
- →Add <link rel="preload"> in your HTML <head> for faster script availability