React
Ship waitlist features faster in your React apps
Developer-first waitlist integration. Drop-in components, TypeScript support, and full control over your UI. Build production-ready waitlist flows in minutes, not hours.
Trusted by 2,000+
businesses & entrepreneurs
.png?alt=media&token=939637fa-d391-4d15-85ea-7005e07d08eb)







.png?alt=media&token=264537c9-b2e0-44e7-9d78-3b558d4e10c2)






.png?alt=media&token=939637fa-d391-4d15-85ea-7005e07d08eb)







.png?alt=media&token=264537c9-b2e0-44e7-9d78-3b558d4e10c2)






“Waitlister has been amazing; honestly, I don't plan on changing to another provider. Being able to create beautiful landing pages for my waitlist has been amazing. Customer support's response time is amazing, which has helped me deploy quickly.”
What you can build
Popular ways React users implement waitlists
SaaS Product Launches
Build conversion-optimized waitlist pages for your SaaS MVP with custom React components.
Developer Tool Pre-Releases
Collect early adopter signups for APIs, CLI tools, or developer products with technical landing pages.
Feature Beta Programs
Roll out new features gradually with waitlisted access. Manage beta user onboarding programmatically.
Web3 & Crypto Projects
Build waitlists for NFT drops, token launches, or web3 applications with React + Web3 integrations.
Consumer App Launches
Launch consumer-facing React apps with built-in waitlist systems and viral referral mechanics.
Analytics Dashboard Access
Gate premium features or enterprise tiers behind waitlisted access with React components.
Why Waitlister for React?
Built to work seamlessly with React's capabilities
Component-First Architecture
Build custom waitlist UIs with React components. Full control over markup, styling, and behavior. Integrate seamlessly with your component library and design system.
TypeScript Native Support
First-class TypeScript support with full type definitions. Autocomplete, type safety, and IntelliSense for all integration methods. Zero any types.
State Management Ready
Works with Redux, Zustand, Jotai, or any state library. Handle form state, loading states, and error handling your way. Examples provided for popular libraries.
Production-Grade Performance
Optimized for React 18+ with automatic batching. Lazy loading support, code splitting compatible, and bundle size conscious. Under 2KB for embed widget.
Form Library Compatible
Integrate with React Hook Form, Formik, or uncontrolled forms. Built-in validation patterns. Works with your existing form infrastructure.
Developer Experience First
Clear error messages, detailed logging in dev mode, comprehensive examples. Built by developers, for developers. Extensive TypeScript examples in docs.
Which integration is
right for you?
Compare both methods to find the best fit for your React project
Feature | Form Action | Embeddable Widget |
---|---|---|
Setup Complexity | Moderate (custom form) | Simple (one component) |
UI Control | Complete control | Limited to styling |
TypeScript Support | Full type safety | Basic |
State Management | Your choice | Internal only |
Form Validation | Custom logic | Built-in |
Bundle Impact | 0KB (native fetch) | ~2KB (async) |
Best For | Production apps | Quick MVPs |
Choose Form Action if...
- You need pixel-perfect control over every element
- You're integrating with existing form libraries
- You want type-safe forms with TypeScript
- You need custom validation or complex logic
- You're building for production at scale
- You want complete control over state management
Choose Embeddable Widget if...
- You need a working waitlist in under 10 minutes
- You're building an MVP or prototype quickly
- You don't need deep customization
- You want a ready-made UI component
- Your design system is flexible
How to integrate
Follow these React-specific instructions
Get your Waitlister API endpoint
Sign into Waitlister → Overview → copy your waitlist key. Your submission endpoint is:https://waitlister.me/s/YOUR_WAITLIST_KEY
Create a custom waitlist form component
Build a form component with React state management:
// components/WaitlistForm.jsx
import { useState } from 'react';
export default function WaitlistForm() {
const [email, setEmail] = useState('');
const [name, setName] = useState('');
const [loading, setLoading] = useState(false);
const [success, setSuccess] = useState(false);
const [error, setError] = useState(null);
const handleSubmit = async (e) => {
e.preventDefault();
setLoading(true);
setError(null);
try {
const response = await fetch('https://waitlister.me/s/YOUR_WAITLIST_KEY', {
method: 'POST',
headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
body: new URLSearchParams({ email, name })
});
if (response.ok) {
setSuccess(true);
setEmail('');
setName('');
} else {
throw new Error('Submission failed');
}
} catch (err) {
setError('Failed to join waitlist. Please try again.');
} finally {
setLoading(false);
}
};
if (success) {
return <div className="success">Thanks! You're on the list.</div>;
}
return (
<form onSubmit={handleSubmit}>
<input
type="email"
value={email}
onChange={(e) => setEmail(e.target.value)}
placeholder="Your email"
required
/>
<input
type="text"
value={name}
onChange={(e) => setName(e.target.value)}
placeholder="Your name (optional)"
/>
<button type="submit" disabled={loading}>
{loading ? 'Joining...' : 'Join Waitlist'}
</button>
{error && <div className="error">{error}</div>}
</form>
);
}
TypeScript version with full types
Here's a production-ready TypeScript version:
// components/WaitlistForm.tsx
import { useState, FormEvent, ChangeEvent } from 'react';
interface FormData {
email: string;
name: string;
}
interface WaitlistFormProps {
onSuccess?: () => void;
className?: string;
}
export default function WaitlistForm({ onSuccess, className }: WaitlistFormProps) {
const [formData, setFormData] = useState<FormData>({ email: '', name: '' });
const [loading, setLoading] = useState<boolean>(false);
const [success, setSuccess] = useState<boolean>(false);
const [error, setError] = useState<string | null>(null);
const handleChange = (e: ChangeEvent<HTMLInputElement>) => {
setFormData(prev => ({ ...prev, [e.target.name]: e.target.value }));
};
const handleSubmit = async (e: FormEvent) => {
e.preventDefault();
setLoading(true);
setError(null);
try {
const response = await fetch(
`https://waitlister.me/s/${process.env.REACT_APP_WAITLIST_KEY}`,
{
method: 'POST',
headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
body: new URLSearchParams(formData)
}
);
if (response.ok) {
setSuccess(true);
setFormData({ email: '', name: '' });
onSuccess?.();
} else {
throw new Error('Submission failed');
}
} catch (err) {
setError('Failed to join waitlist. Please try again.');
} finally {
setLoading(false);
}
};
if (success) {
return (
<div className="success">
<h3>You're on the list!</h3>
<p>We'll notify you when we launch.</p>
</div>
);
}
return (
<form onSubmit={handleSubmit} className={className}>
<input
type="email"
name="email"
value={formData.email}
onChange={handleChange}
placeholder="Your email"
required
/>
<input
type="text"
name="name"
value={formData.name}
onChange={handleChange}
placeholder="Your name (optional)"
/>
<button type="submit" disabled={loading}>
{loading ? 'Joining...' : 'Join Waitlist'}
</button>
{error && <div className="error">{error}</div>}
</form>
);
}
Integrate with React Hook Form (recommended)
For production apps, use React Hook Form for better validation and UX:
// npm install react-hook-form
import { useForm } from 'react-hook-form';
type FormData = {
email: string;
name?: string;
};
export default function WaitlistForm() {
const { register, handleSubmit, formState: { errors, isSubmitting }, reset } = useForm<FormData>();
const [success, setSuccess] = useState(false);
const onSubmit = async (data: FormData) => {
const response = await fetch('https://waitlister.me/s/YOUR_KEY', {
method: 'POST',
headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
body: new URLSearchParams(data as any)
});
if (response.ok) {
setSuccess(true);
reset();
}
};
if (success) return <div>Success! You're on the list.</div>;
return (
<form onSubmit={handleSubmit(onSubmit)}>
<input
{...register('email', {
required: 'Email is required',
pattern: {
value: /^[A-Z0-9._%+-]+@[A-Z0-9.-]+\.[A-Z]{2,}$/i,
message: 'Invalid email address'
}
})}
placeholder="Your email"
/>
{errors.email && <span>{errors.email.message}</span>}
<input {...register('name')} placeholder="Your name (optional)" />
<button type="submit" disabled={isSubmitting}>
{isSubmitting ? 'Joining...' : 'Join Waitlist'}
</button>
</form>
);
}
Add custom fields and metadata
Pass additional data like UTM parameters, referral codes, or user context:
const handleSubmit = async (data: FormData) => {
// Get URL parameters
const urlParams = new URLSearchParams(window.location.search);
const payload = {
...data,
utm_source: urlParams.get('utm_source') || '',
utm_campaign: urlParams.get('utm_campaign') || '',
referral_code: urlParams.get('ref') || '',
signup_page: window.location.pathname,
user_agent: navigator.userAgent
};
await fetch('https://waitlister.me/s/YOUR_KEY', {
method: 'POST',
headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
body: new URLSearchParams(payload)
});
};
Whitelist your React app domain
In Waitlister → Settings → Configure → Whitelisted domains, add:
Development: localhost:3000
Staging: staging.yourdomain.com
Production: yourdomain.com
For Vercel: your-app.vercel.app
For Netlify: your-app.netlify.app
Test across your component tree
Import and test your form component in different routes and contexts. Verify state management, error handling, and success flows work correctly.
Need more details?
Check out our complete form action endpoint documentation.
View full documentationCommon issues & solutions
Quick fixes for React-specific problems
If you added the script to public/index.html, ensure it's before </body>. For SPAs, the script loads once on initial page load. If using React Router, the script persists across route changes automatically.
Waitlister API supports CORS for whitelisted domains. Ensure your domain (including localhost for dev) is added to Whitelist domains in Waitlister settings. Check browser console for specific domain that's being blocked.
Check browser Network tab for the API request. Verify the endpoint URL is correct, method is POST, and Content-Type is application/x-www-form-urlencoded. Enable error logging in your catch block.
Ensure the script tag in public/index.html is loaded before the component mounts. You can verify in browser DevTools → Network tab. The widget div needs the exact class name "waitlister-form" to be recognized.
URLSearchParams expects Record<string, string>. Cast your data or ensure all values are strings before passing to URLSearchParams constructor.
Make sure you're using the correct state setter syntax. For nested state, use functional updates: setFormData(prev => ({...prev, email: ''})) instead of direct mutation.
Add disabled={loading || isSubmitting} to your submit button to prevent double-clicks. Also ensure you're not calling the submit handler multiple times.
In Create React App, use REACT_APP_ prefix (REACT_APP_WAITLIST_KEY). In Vite, use VITE_ prefix (VITE_WAITLIST_KEY). Restart dev server after adding env variables.
If using styled-components or emotion, styles may conflict with the embed widget. Use className prop to add container styles, but don't try to style the internal .waitlister-form element.
Common questions
About React integration
Yes! Works perfectly with Create React App, Vite, Next.js, Remix, and any React setup. The embed method uses a standard script tag. The form action method uses native fetch - no build tool specifics.
Absolutely! Full TypeScript support with complete type definitions in our examples. Type-safe forms, proper event handlers, and IntelliSense for all integration methods.
Yes! Fully compatible with React 18, including concurrent features, automatic batching, and Suspense. Also works with React 17 and 16.8+ (hooks required).
Yes! Works seamlessly with React Hook Form, Formik, or any form library. Use the custom form approach and integrate with your existing form infrastructure. Examples provided in integration guide.
Use React state (useState) to track loading, success, and error states. Show spinners, disable buttons during submission, and display error messages. Full examples provided in the form action integration steps.
Yes! The form action method gives you complete control. Dispatch Redux actions on success/failure, or update Zustand store. The embed method manages its own state internally.
Yes! Works perfectly with React Router (v5 and v6), Reach Router, or any routing solution. The embed script loads once and persists across route changes. Forms work on any route.
For custom forms (form action method), style however you want - Tailwind, styled-components, emotion, CSS modules, or plain CSS. For embed method, you can style the container but internal widget styling is managed in Waitlister dashboard.
Add localhost:3000 (or your dev port) to whitelisted domains in Waitlister settings. Forms will work exactly the same in development as production. Test all success/error flows thoroughly.
Yes! Both methods work in modals, drawers, popovers, or any overlay component. Just render the component inside your modal. The embed method auto-adjusts height for modal contexts.
Works great! For the embed method, load the script in _document.js or _app.js. For custom forms, use the form action method which works in both SSR and CSR contexts. Check our Next.js specific guide for details.
Yes! With the custom form approach, you have full control. Call your submit function from anywhere - button clicks, keyboard shortcuts, or other user interactions. Perfect for multi-step forms.
Get started for free
Start collecting sign ups for your
product launch in minutes — no coding required.