Features Pricing
Start My Free Trial

React Integration

Add a MakeEmWait waitlist to your React application.

There are two ways to add a MakeEmWait waitlist to your React app: use the pre-built embed widget for a quick setup, or call the API directly for full control over the UI.

i
Works on All Plans The public signup API is free to use on all plans, including during your trial. Server-side management features (listing signups, analytics, exports) require an API key and may be gated by your subscription tier.

Using the Embed Widget in React

The embed widget is the fastest way to get a waitlist into your React app. It loads a pre-built form inside a Shadow DOM, so your existing styles won’t interfere with it. Load the embed script in a React component using useEffect:

import { useEffect, useRef } from 'react';

function WaitlistWidget({ waitlistId, theme = 'dark', compact = false }) {
  const containerRef = useRef(null);

  useEffect(() => {
    const script = document.createElement('script');
    script.src = 'https://makeemwait.com/assets/js/embed.js';
    script.setAttribute('data-waitlist-id', waitlistId);
    script.setAttribute('data-theme', theme);
    if (compact) script.setAttribute('data-compact', 'true');

    containerRef.current.appendChild(script);

    return () => {
      // Clean up widget on unmount
      if (containerRef.current) {
        containerRef.current.innerHTML = '';
      }
    };
  }, [waitlistId, theme, compact]);

  return <div ref={containerRef} />;
}

export default WaitlistWidget;

Then use it anywhere in your app:

<WaitlistWidget waitlistId="abc123" theme="light" />

The widget renders inside a Shadow DOM, so your app’s styles won’t conflict with it. All embed features work as normal — themes, compact mode, referral tracking, UTM passthrough, returning visitor detection, and social sharing. See the Embed Widget docs for the full list of data- attributes.

*
The cleanup function in useEffect removes the widget when the component unmounts or when props change, preventing duplicate forms from stacking up during re-renders.

Using the API Directly

If you want full control over the look and feel, skip the embed widget and call the MakeEmWait API directly with fetch. This lets you build a completely custom signup form that matches your app’s design system.

*
No API Key Needed Public signup endpoints don't require authentication. Your waitlist ID is all you need for the examples below.
import { useState } from 'react';

const API_URL = 'https://makeemwait.com/api/v1';

function WaitlistForm({ waitlistId }) {
  const [email, setEmail] = useState('');
  const [firstName, setFirstName] = useState('');
  const [lastName, setLastName] = useState('');
  const [status, setStatus] = useState('idle'); // idle | loading | success | error
  const [result, setResult] = useState(null);
  const [error, setError] = useState('');

  const handleSubmit = async (e) => {
    e.preventDefault();
    setStatus('loading');
    setError('');

    try {
      // Read referral token from URL if present
      const params = new URLSearchParams(window.location.search);
      const referredByToken = params.get('ref');

      const response = await fetch(
        `${API_URL}/public/waitlists/${waitlistId}/signups`,
        {
          method: 'POST',
          headers: { 'Content-Type': 'application/json' },
          body: JSON.stringify({
            email,
            first_name: firstName,
            last_name: lastName,
            timezone: Intl.DateTimeFormat().resolvedOptions().timeZone,
            ...(referredByToken && { referred_by_token: referredByToken }),
          }),
        }
      );

      const data = await response.json();

      if (!response.ok) {
        throw new Error(data.error || 'Something went wrong');
      }

      setResult(data);
      setStatus('success');
    } catch (err) {
      setError(err.message);
      setStatus('error');
    }
  };

  if (status === 'success' && result) {
    return (
      <div>
        <h3>You're on the list!</h3>
        <p>You're #{result.position} in line.</p>
        {result.referral_token && (
          <p>
            Share your referral link to move up:{' '}
            <code>
              {window.location.origin + window.location.pathname}?ref=
              {result.referral_token}
            </code>
          </p>
        )}
      </div>
    );
  }

  return (
    <form onSubmit={handleSubmit}>
      <input
        type="email"
        value={email}
        onChange={(e) => setEmail(e.target.value)}
        placeholder="you@example.com"
        required
      />
      <input
        type="text"
        value={firstName}
        onChange={(e) => setFirstName(e.target.value)}
        placeholder="First name"
      />
      <input
        type="text"
        value={lastName}
        onChange={(e) => setLastName(e.target.value)}
        placeholder="Last name"
      />
      <button type="submit" disabled={status === 'loading'}>
        {status === 'loading' ? 'Joining...' : 'Join the Waitlist'}
      </button>
      {error && <p style=>{error}</p>}
    </form>
  );
}

export default WaitlistForm;

The public signup endpoint does not require authentication, so you can call it directly from the browser. The timezone field is optional but recommended — it lets you segment signups by time zone later.

i
The default API URL points to the MakeEmWait service at https://makeemwait.com/api/v1.

TypeScript Types

If your project uses TypeScript, add these interfaces to get type safety for the signup API responses. They cover the success response, error shape, and waitlist configuration object:

interface SignupResponse {
  email: string;
  first_name: string;
  last_name: string;
  position: number;
  referral_token: string;
  referral_count: number;
  created_at: string;
  total_signups: number;
  redirect_url?: string;
}

interface SignupError {
  error: string;
  error_code: string;
  request_id: string;
}

interface WaitlistConfig {
  name: string;
  headline: string;
  subheadline: string;
  hero_image_url: string | null;
  name_field: 'optional' | 'required' | 'hidden';
  phone_field: 'optional' | 'required' | 'hidden';
  signup_count: number;
  custom_questions: CustomQuestion[];
  show_social_proof: boolean;
  require_consent: boolean;
  consent_text: string | null;
  status: 'open' | 'closed';
  referral_boost: number;
  hide_branding: boolean;
  hide_position: boolean;
  position_offset: number;
  require_email_verification: boolean;
}

interface CustomQuestion {
  id: string;
  label: string;
  type: 'text' | 'dropdown' | 'checkbox';
  required: boolean;
  options?: string[];
}

SignupResponse is what the POST /public/waitlists/{id}/signups endpoint returns on success. SignupError is the shape of all error responses. WaitlistConfig is what GET /public/waitlists/{id}/config returns — use it to dynamically render form fields based on the waitlist’s settings.

Custom Hook

For a cleaner separation of concerns, extract the signup logic into a reusable hook. This is useful when you have multiple components that need to trigger signups, or when you want to keep your form components focused on rendering:

import { useState, useCallback } from 'react';

const API_URL = 'https://makeemwait.com/api/v1';

interface SignupFields {
  email: string;
  first_name?: string;
  last_name?: string;
  phone?: string;
  referred_by_token?: string;
}

export function useWaitlist(waitlistId: string) {
  const [status, setStatus] = useState<
    'idle' | 'loading' | 'success' | 'error'
  >('idle');
  const [data, setData] = useState<SignupResponse | null>(null);
  const [error, setError] = useState<string>('');

  const signup = useCallback(
    async (fields: SignupFields) => {
      setStatus('loading');
      setError('');

      try {
        const response = await fetch(
          `${API_URL}/public/waitlists/${waitlistId}/signups`,
          {
            method: 'POST',
            headers: { 'Content-Type': 'application/json' },
            body: JSON.stringify({
              ...fields,
              timezone: Intl.DateTimeFormat().resolvedOptions().timeZone,
            }),
          }
        );

        const result = await response.json();

        if (!response.ok) throw new Error(result.error);

        setData(result);
        setStatus('success');
        return result;
      } catch (err) {
        setError(
          err instanceof Error ? err.message : 'Something went wrong'
        );
        setStatus('error');
        return null;
      }
    },
    [waitlistId]
  );

  return { signup, status, data, error };
}

Then use it in any component:

import { useWaitlist } from './useWaitlist';

function SignupForm() {
  const { signup, status, data, error } = useWaitlist('abc123');

  const handleSubmit = async (e: React.FormEvent<HTMLFormElement>) => {
    e.preventDefault();
    const form = new FormData(e.currentTarget);
    await signup({
      email: form.get('email') as string,
      first_name: form.get('first_name') as string,
    });
  };

  if (status === 'success' && data) {
    return <p>You're #{data.position} in line!</p>;
  }

  return (
    <form onSubmit={handleSubmit}>
      <input name="email" type="email" placeholder="you@example.com" required />
      <input name="first_name" type="text" placeholder="First name" />
      <button type="submit" disabled={status === 'loading'}>
        {status === 'loading' ? 'Joining...' : 'Join the Waitlist'}
      </button>
      {error && <p style=>{error}</p>}
    </form>
  );
}

The hook manages loading state, error handling, and the API call. Your component only deals with rendering.

These guides cover other frameworks and the full API surface: