Guide
There’s no published @esigkit/react package yet — but the API is simple
enough that the “SDK” is really just a 30-line hook. Below are the patterns
we recommend for embedding signatures in React applications.
The basic hook
import { useEffect, useState } from 'react';
type SignatureStatus = 'live' | 'paused' | 'never_rendered' | 'not_found';
export function useSignature(userId: string) {
const [html, setHtml] = useState<string | null>(null);
const [status, setStatus] = useState<SignatureStatus | null>(null);
const [error, setError] = useState<Error | null>(null);
useEffect(() => {
let cancelled = false;
(async () => {
try {
const statusRes = await fetch(
`https://api.esigkit.com/v1/users/${userId}/signature/status`
);
if (!statusRes.ok) throw new Error(`status ${statusRes.status}`);
const { status: s } = await statusRes.json();
if (cancelled) return;
setStatus(s);
if (s === 'live') {
const htmlRes = await fetch(
`https://api.esigkit.com/v1/users/${userId}/signature`,
{ redirect: 'follow' }
);
if (cancelled) return;
setHtml(await htmlRes.text());
}
} catch (e) {
if (!cancelled) setError(e as Error);
}
})();
return () => {
cancelled = true;
};
}, [userId]);
return { html, status, error };
}
Usage:
function SignaturePreview({ userId }: { userId: string }) {
const { html, status, error } = useSignature(userId);
if (error) return <p>Failed to load signature.</p>;
if (!status) return <p>Loading…</p>;
if (status !== 'live') return <p>Signature not available ({status}).</p>;
return <div dangerouslySetInnerHTML={{ __html: html ?? '' }} />;
}
Iframe variant (CSS isolation)
If your application’s CSS conflicts with the signature’s table layout (e.g.,
a global * { box-sizing: border-box } reset breaks email-style table
widths), iframe it:
function SignatureFrame({ userId }: { userId: string }) {
return (
<iframe
src={`https://api.esigkit.com/v1/users/${userId}/signature`}
width={600}
height={120}
style={{ border: 0 }}
title="Email signature"
/>
);
}
The iframe’s Content-Security-Policy header allows embedding from any
origin — you don’t need to configure anything server-side.
Suspense integration
If you’d rather use <Suspense> boundaries instead of in-component loading
state, wrap the fetch in a resource that throws a promise:
import { use } from 'react';
const cache = new Map<string, Promise<string>>();
function fetchSignature(userId: string) {
if (!cache.has(userId)) {
cache.set(
userId,
fetch(`https://api.esigkit.com/v1/users/${userId}/signature`).then((r) =>
r.text()
)
);
}
return cache.get(userId)!;
}
export function SignatureSuspense({ userId }: { userId: string }) {
const html = use(fetchSignature(userId));
return <div dangerouslySetInnerHTML={{ __html: html }} />;
}
<Suspense fallback={<p>Loading signature…</p>}>
<SignatureSuspense userId={userId} />
</Suspense>
use(...) requires React 19 or React canary. For React 18, the same pattern
works with a small createResource() helper (or use swr / react-query —
both support suspense).
Error boundaries
Pair the suspense version with an error boundary so a 404 / network failure doesn’t crash the parent tree:
import { ErrorBoundary } from 'react-error-boundary';
<ErrorBoundary fallback={<p>Could not load signature.</p>}>
<Suspense fallback={<p>Loading…</p>}>
<SignatureSuspense userId={userId} />
</Suspense>
</ErrorBoundary>
Polling for status
If you’re showing a “deploy progress” widget while signatures render, poll
/signature/status until it flips to live:
function useSignatureStatus(userId: string, intervalMs = 5000) {
const [status, setStatus] = useState<SignatureStatus | null>(null);
useEffect(() => {
let cancelled = false;
let timer: ReturnType<typeof setTimeout>;
async function poll() {
try {
const res = await fetch(
`https://api.esigkit.com/v1/users/${userId}/signature/status`
);
const { status: s } = await res.json();
if (cancelled) return;
setStatus(s);
if (s === 'live') return; // stop polling
} catch {
// swallow; retry
}
timer = setTimeout(poll, intervalMs);
}
poll();
return () => {
cancelled = true;
clearTimeout(timer);
};
}, [userId, intervalMs]);
return status;
}
The status endpoint is edge-cached for 30 seconds, so polling once every 5s mostly hits cache — cheap to keep open while a render job is in flight.
Common pitfalls
- Don’t render the iframe inside a flexbox child without an explicit
height. Iframes have intrinsic
height: 150pxby default; without an override, they collapse oddly inside grid / flex layouts. - Don’t strip the
<base target="_blank">from the rendered HTML. It’s there so links in the signature open in a new tab — preserves UX when embedded inline in your app. - Don’t fetch on every render. Memoize on
userId(theuseEffectdep array above does this). Re-fetching on every parent re-render will flood the API and hit the rate limit.