# Add a waitlist component to React and Next.js > Mount a LaunchList form in React, Next.js, Vue or Svelte with one function call, or build a fully headless form against the capture endpoint. Source: https://getlaunchlist.com/help/docs/integrations/react --- In a single-page app the form's target element usually does not exist when the page loads, so mounting is explicit rather than automatic. You have two paths: **mount our form**, or **build your own and post to the endpoint**. ## Mount our form Load the runtime once in your app shell:  ```html ``` Then mount it from a component: ```jsx // frm_8Kd2Qm4Rp9 is an example ID; use your own from Embed & API → Your website import { useEffect } from 'react'; export function Waitlist() { useEffect(() => { window.LaunchList?.mount('#waitlist', { form: 'frm_8Kd2Qm4Rp9' }); }, []); return
; } ``` The form renders inside a **shadow root**, so it cannot collide with your app's CSS in either direction, and it takes the design you pick in the form editor, under **Design**. > [!NOTE] Mount after the element exists, not before > `mount()` needs a real DOM node. Calling it during render, or in a server component, finds > nothing. `useEffect` runs after commit, which is exactly the right moment. ### Next.js Load the runtime with `next/script` and mount in a client component: ```jsx // app/layout.tsx import Script from 'next/script'; export default function RootLayout({ children }) { return ( {children} ); } ``` ```jsx // components/waitlist.tsx 'use client'; import { useEffect } from 'react'; export function Waitlist() { useEffect(() => { window.LaunchList?.mount('#waitlist', { form: 'frm_8Kd2Qm4Rp9' }); }, []); return ; } ``` ### Several forms, or forms added after navigation `refresh()` rescans the document, but it only finds targets that **carry the `data-launchlist-form` attribute**. A bare `` like the one above is invisible to it, because the form id lives in your `mount()` call rather than in the markup. So pick one of the two: **Keep mounting explicitly.** Every component that renders a target calls `mount()` in its own effect. `mount()` is idempotent per element, so a remount after a route change is safe and a double render costs nothing. ```jsx useEffect(() => { window.LaunchList?.mount('#waitlist', { form: 'frm_8Kd2Qm4Rp9' }); }, []); ``` **Or put the id in the markup**, and let the runtime find it: ```jsx ``` ```js // After inserting new targets, or after a route change. window.LaunchList?.refresh(); ``` With the attribute present, `refresh()` mounts anything new and leaves already-mounted targets alone, so it is safe to call as often as you like. It is also what picks up dynamically added popups, widgets and enhanced forms. ### Vue and Svelte The same two steps, in each framework's post-mount hook: ```js // Vue onMounted(() => window.LaunchList?.mount('#waitlist', { form: 'frm_8Kd2Qm4Rp9' }), ); // Svelte onMount(() => window.LaunchList?.mount('#waitlist', { form: 'frm_8Kd2Qm4Rp9' }), ); ``` ## Build your own form When the signup is part of your product's design system and an embedded form would look transplanted, post directly instead. You keep the markup and the state; the pipeline stays ours. ```jsx import { useState } from 'react'; const ENDPOINT = 'https://embed.getlaunchlist.com/f/frm_8Kd2Qm4Rp9/submit'; export function Waitlist() { const [state, setState] = useState({ status: 'idle', result: null }); async function onSubmit(event) { event.preventDefault(); setState({ status: 'sending', result: null }); const response = await fetch(ENDPOINT, { method: 'POST', headers: { Accept: 'application/json', 'Content-Type': 'application/json', }, body: JSON.stringify({ email: event.target.email.value, _channel: 'api', }), }); const result = await response.json(); setState({ status: response.ok ? 'done' : 'error', result }); } if (state.status === 'done') { returnYou are number {state.result.contact.position} in line.
; } return ( ); } ``` A successful capture returns the contact's position and referral code, so you can show someone their place in line immediately: ```json { "accepted": true, "held": false, "contact": { "status": "pending", "verification_required": true, "referral_code": "AB12CD34", "referral_count": 0, "position": 42 } } ``` > [!TIP] Pass the referral code through yourself > The embed reads `?ref=` from the URL automatically. A hand-built form does not, so read it and send > it as `ref` in the body, or referrals will not credit. ## Server-side capture If the signup happens on your server rather than in the browser, use the [REST API](/help/docs/api/contacts-create) instead. It is authenticated, idempotent, and returns the full contact object including the trust verdict. ## If something is not working | What you see | What to do | | ------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | `LaunchList is undefined` | The runtime has not loaded yet. Use optional chaining as shown, or mount on the script's `load` event. | | The form disappears after a route change | Your framework unmounted the target. Call `mount()` again in the new component's effect. `refresh()` alone will not bring it back unless the target carries `data-launchlist-form`. | | Two forms render | `mount()` ran twice, usually React 18 StrictMode in development. It is idempotent per target, so this is safe; if you see doubles in production, check for a duplicate effect. | | Referrals never credit on a hand-built form | Read `?ref=` yourself and send it in the body. | ## Using the classic LaunchList dashboard A waitlist made in the classic LaunchList dashboard has its own snippet: a `