Skip to content

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: 150px by 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 (the useEffect dep array above does this). Re-fetching on every parent re-render will flood the API and hit the rate limit.