Frontend Integration

React Integration

Mount the EmbedAuth iframe in a React app with a reusable component and a custom hook that handles the postMessage lifecycle and cleanup.

1 min readUpdated October 5, 2026

This guide wraps the EmbedAuth embed in an idiomatic React component. The pattern is small: an <iframe>, a message listener registered in useEffect, and clean teardown so you never leak listeners across renders.

A reusable hook

The subtle part of any postMessage integration is lifecycle: you must add exactly one listener, verify the origin, and remove the listener on unmount. A hook keeps that logic in one place.

import { useEffect } from 'react';

// The embed URL on embedauth.com redirects to the widget's copy on the CDN,
// so its messages come from the CDN's origin.
const EMBED_ORIGIN = 'https://cdn.embedauth.com';

type AuthUser = { id: string; email: string; name?: string };
type AuthSuccess = { token: string; refreshToken: string; user: AuthUser };

export function useEmbedAuth(onSuccess: (result: AuthSuccess) => void) {
  useEffect(() => {
    function handleMessage(event: MessageEvent) {
      if (event.origin !== EMBED_ORIGIN) return;
      const message = event.data;
      if (message?.type === 'authframe:auth-success') {
        onSuccess(message.data as AuthSuccess);
      }
    }
    window.addEventListener('message', handleMessage);
    return () => window.removeEventListener('message', handleMessage);
  }, [onSuccess]);
}

Wrap onSuccess in useCallback at the call site (or keep it stable) so the effect does not re-subscribe on every render.

The embed component

import { useCallback } from 'react';
import { useEmbedAuth } from './useEmbedAuth';

const CLIENT_ID = 'YOUR_CLIENT_ID';

export function AuthEmbed({ mode = 'signin' }: { mode?: 'signin' | 'signup' | 'forgot-password' }) {
  const handleSuccess = useCallback(({ token, user }) => {
    // Send the token to your API to establish a first-party session
    fetch('/api/session', {
      method: 'POST',
      headers: { 'Content-Type': 'application/json' },
      body: JSON.stringify({ token }),
    }).then(() => {
      window.location.assign('/dashboard');
    });
    console.log('Signed in as', user.email);
  }, []);

  useEmbedAuth(handleSuccess);

  const src = `https://embedauth.com/embed/${CLIENT_ID}?mode=${mode}`;
  return (
    <iframe
      title="Sign in"
      src={src}
      width={420}
      height={600}
      style={{ border: 'none', width: '100%', maxWidth: 420 }}
    />
  );
}

Auto-resizing the frame

The embed emits an authframe:resize event with its content height. Track it in state to avoid inner scrollbars:

import { useEffect, useState } from 'react';

export function useEmbedHeight(fallback = 600) {
  const [height, setHeight] = useState(fallback);
  useEffect(() => {
    function onMessage(event: MessageEvent) {
      if (event.origin !== 'https://cdn.embedauth.com') return;
      if (event.data?.type === 'authframe:resize') {
        setHeight(event.data.data.height);
      }
    }
    window.addEventListener('message', onMessage);
    return () => window.removeEventListener('message', onMessage);
  }, []);
  return height;
}

Pass the returned height to the iframe's height prop and the frame grows and shrinks with the form (sign-up is taller than sign-in, error states add rows, and so on).

Handling errors

Listen for authframe:auth-error alongside success so you can surface a message or trigger analytics. The payload is { error: string, code: string }. Do not try to recover the user's input — it lives inside the iframe by design.

Production notes

  • Verify the token on your server, never in the browser. The client hook is only for UX; trust is established in your API. See Verifying JWTs in Node.js.
  • Add the EmbedAuth origin to your app's Content Security Policy frame-src, or the iframe will be blocked in production.
  • Using the App Router? See the Next.js Integration guide for the 'use client' boundary and route-handler session pattern.