React quick start
Embed ImportFlow in a React app with one drop-in component.
Paste one React component into your app. It loads the real hosted ImportFlow widget and cleans itself up on unmount.
Before you paste code
- Create and verify a dashboard project connected to your customer Supabase app.
- Create an import kit from one canonical template and confirm the destination table.
- Copy the kit embed token from the kit detail page.
- Run the staging SQL from the kit page before testing a real import.
Quick start: React drop-in component
Paste this into an existing React page or component, then replace the example token with your kit token.
'use client'
import { useEffect, useRef } from 'react'
const IMPORTFLOW_APP_ORIGIN = "https://your-importflow-domain.example"
const IMPORTFLOW_TOKEN = "if_pub_xxxxxxxxx"
const IMPORTFLOW_APPEARANCE = {} as const
const IMPORTFLOW_SCRIPT_ID = 'importflow-embed-loader'
type ImportFlowSummary = {
importJobId: string
requestedRows: number
validRows: number
insertedRows: number
failedRows: number
completedAt: string
}
type ImportFlowMount = {
destroy: () => void
}
type ImportFlowGlobal = {
mount: (options: {
token: string
target: HTMLElement
theme?: 'light' | 'dark' | 'system'
primaryColor?: string
surfaceColor?: string
textColor?: string
radius?: 'none' | 'sm' | 'md' | 'lg' | 'xl'
onComplete?: (summary: ImportFlowSummary) => void
onClose?: (event: { reason: string; phase: string }) => void
}) => ImportFlowMount
}
declare global {
interface Window {
ImportFlow?: ImportFlowGlobal
}
}
function clearTarget(target: HTMLElement) {
target.replaceChildren()
}
function loadImportFlowScript() {
if (window.ImportFlow) {
return Promise.resolve()
}
const existing = document.getElementById(IMPORTFLOW_SCRIPT_ID) as HTMLScriptElement | null
if (existing) {
return new Promise<void>((resolve, reject) => {
existing.addEventListener('load', () => resolve(), { once: true })
existing.addEventListener('error', () => reject(new Error('ImportFlow failed to load.')), {
once: true,
})
})
}
return new Promise<void>((resolve, reject) => {
const script = document.createElement('script')
script.id = IMPORTFLOW_SCRIPT_ID
script.src = new URL('/embed/v1/importflow.js', IMPORTFLOW_APP_ORIGIN).toString()
script.async = true
script.addEventListener('load', () => resolve(), { once: true })
script.addEventListener('error', () => reject(new Error('ImportFlow failed to load.')), {
once: true,
})
document.head.appendChild(script)
})
}
export function ImportFlowDropIn() {
const targetRef = useRef<HTMLDivElement | null>(null)
const mountRef = useRef<ImportFlowMount | null>(null)
useEffect(() => {
const target = targetRef.current
if (!target) {
return
}
let cancelled = false
mountRef.current?.destroy()
mountRef.current = null
clearTarget(target)
loadImportFlowScript()
.then(() => {
if (cancelled || !window.ImportFlow) {
return
}
mountRef.current?.destroy()
clearTarget(target)
mountRef.current = window.ImportFlow.mount({
token: IMPORTFLOW_TOKEN,
target,
...IMPORTFLOW_APPEARANCE,
onComplete(summary) {
console.log('ImportFlow complete', summary)
},
onClose(event) {
console.log('ImportFlow closed', event)
},
})
})
.catch((error: unknown) => {
if (!cancelled) {
console.error(error)
}
})
return () => {
cancelled = true
mountRef.current?.destroy()
mountRef.current = null
clearTarget(target)
}
}, [])
return <div ref={targetRef} />
}
Quick start vs advanced
The drop-in snippet is the primary path from the kit page: one component, one token, and built-in mount cleanup. The helper file below is optional for teams that want to centralize callbacks or wrapper styling.
Appearance options
Appearance options are passed through hosted widget URLs and generated snippets. Use theme: 'light' | 'dark' | 'system', radius: 'none' | 'sm' | 'md' | 'lg' | 'xl', and optional strict hex color overrides. System follows the visitor device/browser color mode as the base appearance; manual overrides still apply on top.
Advanced customization helper
Advanced path for apps that prefer one shared wrapper component.
'use client'
import { useEffect, useRef } from 'react'
const DEFAULT_IMPORTFLOW_APP_ORIGIN = "https://your-importflow-domain.example"
const SCRIPT_ID = 'importflow-embed-loader'
type ImportFlowReadyPayload = {
importKitId: string
template: string
showWatermark: boolean
}
type ImportFlowCompletePayload = {
importJobId: string
requestedRows: number
validRows: number
insertedRows: number
failedRows: number
completedAt: string
}
type ImportFlowClosePayload = {
reason: 'user' | 'done' | 'fatal_error'
phase: string
}
type ImportFlowTheme = 'light' | 'dark' | 'system'
type ImportFlowRadius = 'none' | 'sm' | 'md' | 'lg' | 'xl'
type ImportFlowMount = {
destroy: () => void
}
type ImportFlowGlobal = {
mount: (options: {
token: string
target: HTMLElement
theme?: ImportFlowTheme
primaryColor?: string
surfaceColor?: string
textColor?: string
radius?: ImportFlowRadius
onReady?: (payload: ImportFlowReadyPayload) => void
onComplete?: (summary: ImportFlowCompletePayload) => void
onClose?: (event: ImportFlowClosePayload) => void
}) => ImportFlowMount
}
declare global {
interface Window {
ImportFlow?: ImportFlowGlobal
}
}
export type ImportFlowEmbedProps = {
token: string
appOrigin?: string
className?: string
theme?: ImportFlowTheme
primaryColor?: string
surfaceColor?: string
textColor?: string
radius?: ImportFlowRadius
onReady?: (payload: ImportFlowReadyPayload) => void
onComplete?: (summary: ImportFlowCompletePayload) => void
onClose?: (event: ImportFlowClosePayload) => void
}
function clearTarget(target: HTMLElement) {
target.replaceChildren()
}
function getScriptUrl(appOrigin: string) {
return new URL('/embed/v1/importflow.js', appOrigin).toString()
}
function loadImportFlowScript(src: string): Promise<void> {
if (window.ImportFlow) {
return Promise.resolve()
}
const existing = document.getElementById(SCRIPT_ID) as HTMLScriptElement | null
if (existing) {
return new Promise((resolve, reject) => {
existing.addEventListener('load', () => resolve(), { once: true })
existing.addEventListener('error', () => reject(new Error('Failed to load ImportFlow.')), {
once: true,
})
})
}
return new Promise((resolve, reject) => {
const script = document.createElement('script')
script.id = SCRIPT_ID
script.src = src
script.async = true
script.addEventListener('load', () => resolve(), { once: true })
script.addEventListener('error', () => reject(new Error('Failed to load ImportFlow.')), {
once: true,
})
document.head.appendChild(script)
})
}
export function ImportFlowEmbed({
token,
appOrigin = DEFAULT_IMPORTFLOW_APP_ORIGIN,
className,
theme,
primaryColor,
surfaceColor,
textColor,
radius,
onReady,
onComplete,
onClose,
}: ImportFlowEmbedProps) {
const targetRef = useRef<HTMLDivElement | null>(null)
const mountRef = useRef<ImportFlowMount | null>(null)
useEffect(() => {
const target = targetRef.current
if (!target || !token.trim()) {
return
}
let cancelled = false
mountRef.current?.destroy()
mountRef.current = null
clearTarget(target)
loadImportFlowScript(getScriptUrl(appOrigin))
.then(() => {
if (cancelled) {
return
}
if (!window.ImportFlow) {
throw new Error('ImportFlow loaded without exposing window.ImportFlow.')
}
mountRef.current?.destroy()
clearTarget(target)
mountRef.current = window.ImportFlow.mount({
token,
target,
...(theme ? { theme } : {}),
...(primaryColor ? { primaryColor } : {}),
...(surfaceColor ? { surfaceColor } : {}),
...(textColor ? { textColor } : {}),
...(radius ? { radius } : {}),
...(onReady ? { onReady } : {}),
...(onComplete ? { onComplete } : {}),
...(onClose ? { onClose } : {}),
})
})
.catch((error: unknown) => {
if (!cancelled) {
console.error(error)
}
})
return () => {
cancelled = true
mountRef.current?.destroy()
mountRef.current = null
clearTarget(target)
}
}, [appOrigin, token, theme, primaryColor, surfaceColor, textColor, radius, onReady, onComplete, onClose])
return <div className={className} ref={targetRef} />
}
Advanced customization usage
Use this only if you added the advanced helper file above.
'use client'
import { ImportFlowEmbed } from '@/components/importflow/importflow-embed'
export function LeadsImport() {
return <ImportFlowEmbed token="if_pub_xxxxxxxxx" />
}
What the drop-in does
It loads /embed/v1/importflow.js, calls window.ImportFlow.mount(...), and cleans up the hosted widget frame whenever React unmounts or remounts the component.
What stays server-authoritative
The widget still bootstraps through ImportFlow, validates the public embed token, enforces inserted-row limits, writes through the staging table, and finalizes imports server-side.
Need plain JavaScript instead?
Use the same hosted loader without React. The contract is the same widget and token.
Read the JavaScript quick start