[{"data":1,"prerenderedAt":378},["ShallowReactive",2],{"blog-how-to\u002Fwaitlist-react":3},{"id":4,"title":5,"body":6,"category":352,"date":353,"dateModified":354,"description":355,"draft":356,"extension":357,"faq":358,"featured":356,"keywords":365,"meta":366,"navigation":367,"ogDescription":368,"ogTitle":5,"path":369,"readTime":370,"schemaType":371,"seo":372,"sitemap":373,"stem":374,"tags":375,"twitterCard":376,"__hash__":377},"blog\u002Fblog\u002Fhow-to\u002Fwaitlist-react.md","React Waitlist: Add Signups to a Vite or Create React App",{"type":7,"value":8,"toc":344},"minimark",[9,30,59,64,70,107,111,114,141,145,148,185,198,202,215,221,246,250,259,270,285,319],[10,11,12,13,17,18,21,22,25,26,29],"p",{},"A Vite or Create React App project has no root layout file and no server render step, so the Next.js integration pattern (",[14,15,16],"code",{},"\"use client\"",", SSR guards, ",[14,19,20],{},"next\u002Fscript",") does not apply. The script tag goes in ",[14,23,24],{},"index.html"," like any other browser script, and ",[14,27,28],{},"window.OperatorStack"," is available the moment your components mount. This guide covers the setup, a custom form component, and what happens automatically when React Router changes routes.",[31,32,33],"tldr",{},[10,34,35,36,38,39,42,43,46,47,50,51,54,55,58],{},"Add the OperatorStack script tag to ",[14,37,24],{}," before ",[14,40,41],{},"\u003C\u002Fbody>",". Use the ",[14,44,45],{},"data-os-widget=\"waitlist\""," div for the default form, or call ",[14,48,49],{},"window.OperatorStack.joinWaitlist()"," from a ",[14,52,53],{},"useState","-backed component for a custom UI. React Router's client-side navigation is tracked automatically since OperatorStack patches ",[14,56,57],{},"history.pushState",".",[60,61,63],"h2",{"id":62},"step-1-add-the-script-tag-to-indexhtml","Step 1: Add the Script Tag to index.html",[10,65,66,67,69],{},"Unlike Next.js or Astro, a Vite\u002FCRA app serves one static ",[14,68,24],{}," that never re-renders on the server. Add the script there once.",[71,72,74,90,100],"step",{"number":73},"1",[10,75,76,85,86,89],{},[77,78,79,80,82,83,58],"strong",{},"Open ",[14,81,24],{}," and add the script before ",[14,84,41],{}," Replace ",[14,87,88],{},"pk_your_project_key"," with your project key from the OperatorStack dashboard.",[91,92,98],"pre",{"className":93,"code":95,"language":96,"meta":97},[94],"language-html","\u003C!-- index.html -->\n\u003C!doctype html>\n\u003Chtml lang=\"en\">\n  \u003Chead>\n    \u003Cmeta charset=\"UTF-8\" \u002F>\n    \u003Ctitle>Your App\u003C\u002Ftitle>\n  \u003C\u002Fhead>\n  \u003Cbody>\n    \u003Cdiv id=\"root\">\u003C\u002Fdiv>\n    \u003Cscript type=\"module\" src=\"\u002Fsrc\u002Fmain.tsx\">\u003C\u002Fscript>\n    \u003Cscript src=\"https:\u002F\u002Foperatorstack.dev\u002Fos.js\" data-project=\"pk_your_project_key\">\u003C\u002Fscript>\n  \u003C\u002Fbody>\n\u003C\u002Fhtml>\n","html","",[14,99,95],{"__ignoreMap":97},[10,101,102,103,106],{},"There is no ",[14,104,105],{},"strategy"," prop to configure and no hydration timing to worry about. The browser loads and executes the script the same way it would for any other page, after your React bundle has already started mounting.",[60,108,110],{"id":109},"step-2-use-the-default-widget-div","Step 2: Use the Default Widget Div",[10,112,113],{},"For a standard signup form with no custom styling logic, drop the widget div directly in JSX. OperatorStack finds it once the script loads and renders the form inside a Shadow DOM, so it will not collide with your app's CSS.",[71,115,117,122,130],{"number":116},"2",[10,118,119],{},[77,120,121],{},"Render the widget div in any component.",[91,123,128],{"className":124,"code":126,"language":127,"meta":97},[125],"language-tsx","\u002F\u002F src\u002Fcomponents\u002FWaitlistSection.tsx\nexport function WaitlistSection() {\n  return (\n    \u003Csection>\n      \u003Ch2>Join the waitlist\u003C\u002Fh2>\n      \u003Cdiv data-os-widget=\"waitlist\">\u003C\u002Fdiv>\n    \u003C\u002Fsection>\n  );\n}\n","tsx",[14,129,126],{"__ignoreMap":97},[10,131,132,133,136,137,140],{},"No ",[14,134,135],{},"useEffect",", no ref, no cleanup function. The script tag from Step 1 scans the DOM for ",[14,138,139],{},"data-os-widget"," attributes once it initializes.",[60,142,144],{"id":143},"step-3-build-a-custom-form-with-usestate","Step 3: Build a Custom Form with useState",[10,146,147],{},"If you want the form to match your design system, call the SDK directly from a controlled component.",[71,149,151,164,170],{"number":150},"3",[10,152,153],{},[77,154,155,156,159,160,163],{},"Create a ",[14,157,158],{},"WaitlistForm"," component that calls ",[14,161,162],{},"joinWaitlist"," on submit.",[91,165,168],{"className":166,"code":167,"language":127,"meta":97},[125],"\u002F\u002F src\u002Fcomponents\u002FWaitlistForm.tsx\nimport { useState, type FormEvent } from \"react\";\n\ndeclare global {\n  interface Window {\n    OperatorStack: {\n      ready: Promise\u003Cvoid>;\n      joinWaitlist: (params: {\n        email: string;\n        name?: string;\n      }) => Promise\u003C{ success: boolean; referral_code: string; referral_link: string }>;\n      getShareLinks: (\n        code: string,\n        opts?: { text?: string },\n      ) => { twitter: string; linkedin: string; copy: string; email: string; whatsapp: string };\n      trackEvent: (name: string, data?: Record\u003Cstring, unknown>) => void;\n    };\n  }\n}\n\nexport function WaitlistForm() {\n  const [email, setEmail] = useState(\"\");\n  const [loading, setLoading] = useState(false);\n  const [shareLinks, setShareLinks] = useState\u003C{ twitter: string; copy: string } | null>(null);\n\n  async function handleSubmit(e: FormEvent) {\n    e.preventDefault();\n    setLoading(true);\n\n    const result = await window.OperatorStack.joinWaitlist({ email });\n    window.OperatorStack.trackEvent(\"waitlist_signup\", { source: document.referrer || \"direct\" });\n\n    const links = window.OperatorStack.getShareLinks(result.referral_code, {\n      text: \"I just joined the waitlist, check it out:\",\n    });\n    setShareLinks({ twitter: links.twitter, copy: links.copy });\n    setLoading(false);\n  }\n\n  if (shareLinks) {\n    return (\n      \u003Cdiv>\n        \u003Cp>You are on the list. Share to move up.\u003C\u002Fp>\n        \u003Ca href={shareLinks.twitter}>Share on Twitter\u003C\u002Fa>\n        \u003Cbutton onClick={() => navigator.clipboard.writeText(shareLinks.copy)}>\n          Copy referral link\n        \u003C\u002Fbutton>\n      \u003C\u002Fdiv>\n    );\n  }\n\n  return (\n    \u003Cform onSubmit={handleSubmit}>\n      \u003Cinput\n        type=\"email\"\n        value={email}\n        onChange={(e) => setEmail(e.target.value)}\n        placeholder=\"your@email.com\"\n        required\n      \u002F>\n      \u003Cbutton type=\"submit\" disabled={loading}>\n        {loading ? \"Joining...\" : \"Join waitlist\"}\n      \u003C\u002Fbutton>\n    \u003C\u002Fform>\n  );\n}\n",[14,169,167],{"__ignoreMap":97},[10,171,172,173,176,177,180,181,184],{},"The ",[14,174,175],{},"Window"," interface declaration is optional but removes the ",[14,178,179],{},"Property 'OperatorStack' does not exist on type 'Window'"," TypeScript error everywhere you reference it. Put it in one ",[14,182,183],{},"global.d.ts"," file at the project root instead of repeating it per component.",[186,187,188],"tip-box",{},[10,189,190,193,194,197],{},[14,191,192],{},"window.OperatorStack.ready"," is a promise that resolves once the SDK has fetched your project config. If your form renders before the script finishes loading, guard the submit handler with ",[14,195,196],{},"await window.OperatorStack.ready"," first, or show a disabled state until it resolves.",[60,199,201],{"id":200},"route-changes-are-tracked-automatically","Route Changes Are Tracked Automatically",[10,203,204,205,207,208,210,211,214],{},"React Router (and most other client-side routers) call ",[14,206,57],{}," to change the URL without a full page reload. OperatorStack's embed script patches ",[14,209,57],{}," and listens for the ",[14,212,213],{},"popstate"," event, then fires a page-view event 100ms after either one, debounced to avoid double-counting when a router triggers both in the same navigation. That means a single-page React app gets per-route analytics in the dashboard without wiring up a router-specific tracking call.",[91,216,219],{"className":217,"code":218,"language":127,"meta":97},[125],"\u002F\u002F No extra code needed -- this navigation is already tracked\nimport { useNavigate } from \"react-router-dom\";\n\nfunction GoToPricing() {\n  const navigate = useNavigate();\n  return \u003Cbutton onClick={() => navigate(\"\u002Fpricing\")}>See pricing\u003C\u002Fbutton>;\n}\n",[14,220,218],{"__ignoreMap":97},[222,223,224],"warning-box",{},[10,225,226,227,229,230,232,233,235,236,239,240,242,243,58],{},"Do not call ",[14,228,28],{}," at module scope (outside a component or hook). The script tag in ",[14,231,24],{}," loads asynchronously, so ",[14,234,28],{}," may still be ",[14,237,238],{},"undefined"," when your module first evaluates. Always read it inside an event handler, ",[14,241,135],{},", or after checking ",[14,244,245],{},"window.OperatorStack?.ready",[60,247,249],{"id":248},"common-mistakes","Common Mistakes",[10,251,252,255,256,258],{},[77,253,254],{},"Putting the script tag inside a component instead of index.html."," A script tag rendered in JSX only executes once, the first time that component mounts, and React's reconciliation can skip re-inserting it on re-renders. Keep it in ",[14,257,24],{}," where it loads exactly once, guaranteed.",[10,260,261,264,265,267,268,58],{},[77,262,263],{},"Forgetting the TypeScript global declaration."," Without it, every ",[14,266,28],{}," reference is a type error. Add it once in ",[14,269,183],{},[10,271,272,278,279,281,282,284],{},[77,273,274,275,277],{},"Assuming you need ",[14,276,135],{}," for the default widget."," The ",[14,280,45],{}," div works without any React lifecycle code. Reach for the SDK and ",[14,283,53],{}," only when you need a custom UI.",[286,287,288,295,301,307,313],"faq-section",{},[289,290,292],"faq-item",{"question":291},"Where does the OperatorStack script tag go in a React app?",[10,293,294],{},"In index.html, right before the closing  tag. A plain client-side React app has no server render step, so there is no SSR guard to worry about -- window is always available by the time your components mount.",[289,296,298],{"question":297},"Do I need a package or npm install for OperatorStack in React?",[10,299,300],{},"No. OperatorStack is a script tag, not an npm package. It attaches window.OperatorStack once it loads, and you call its methods from your components like any other browser global.",[289,302,304],{"question":303},"Does OperatorStack track page views across React Router routes?",[10,305,306],{},"Yes, automatically. React Router's client-side navigation calls history.pushState under the hood. OperatorStack patches pushState and listens for popstate to catch every route change, debounced by 100ms, so single-page navigation is tracked without extra configuration.",[289,308,310],{"question":309},"Can I use TypeScript with the OperatorStack SDK in React?",[10,311,312],{},"Yes. Add a global Window interface declaration once in a .d.ts file so window.OperatorStack has types across your components, or cast to any if you only call it in one place.",[289,314,316],{"question":315},"How do I avoid a flash of unstyled waitlist form before the script loads?",[10,317,318],{},"Render a lightweight loading state or skeleton until window.OperatorStack.ready resolves. The ready promise resolves once the SDK has fetched your project config, which typically takes under a second.",[320,321,322,323],"content-related-articles",{},"\n  ",[324,325,322,329],"contentrelatedcard",{"href":326,"title":327,"description":328},"\u002Fblog\u002Fguides\u002Fjavascript-sdk","OperatorStack JavaScript SDK: All Methods with Examples","Complete reference for joinWaitlist, getShareLinks, trackEvent, and all other SDK methods with runnable examples.",[324,330,322,334],{"href":331,"title":332,"description":333},"\u002Fblog\u002Fhow-to\u002Fwaitlist-sveltekit","How to Add a Waitlist to a SvelteKit App","The same script-tag-plus-SDK pattern in SvelteKit, where server rendering adds a browser guard React SPAs skip.",[324,335,339],{"href":336,"title":337,"description":338},"\u002Fblog\u002Fhow-to\u002Fwaitlist-mobile-app","Mobile App Waitlist: Capture Pre-Launch Signups by Platform","The same SDK calls aimed at an app launch, with an iOS vs Android split before a build exists.",[340,341],"cta-box",{"href":342,"label":343},"\u002F","Get Started Free",{"title":97,"searchDepth":345,"depth":345,"links":346},2,[347,348,349,350,351],{"id":62,"depth":345,"text":63},{"id":109,"depth":345,"text":110},{"id":143,"depth":345,"text":144},{"id":200,"depth":345,"text":201},{"id":248,"depth":345,"text":249},"how-to","2026-07-15","2026-08-13","Add a waitlist to a Vite or Create React App project with one script tag, a useState form component, and automatic React Router page tracking.",false,"md",[359,361,362,363,364],{"question":291,"answer":360},"In index.html, right before the closing \u003C\u002Fbody> tag. A plain client-side React app has no server render step, so there is no SSR guard to worry about -- window is always available by the time your components mount.",{"question":297,"answer":300},{"question":303,"answer":306},{"question":309,"answer":312},{"question":315,"answer":318},"react waitlist,add waitlist to react app,operatorstack react,vite react waitlist,create react app waitlist,react spa analytics",{},true,"One script tag in index.html plus a useState form component gets you a waitlist, referral links, and page-view tracking in a React SPA. No SSR to worry about.","\u002Fblog\u002Fhow-to\u002Fwaitlist-react","6 min","HowTo",{"title":5,"description":355},{"loc":369},"blog\u002Fhow-to\u002Fwaitlist-react",[],"summary_large_image","TB5KXTJK5LkbLZFVYJFApLqHlVWUplUWHxfOYAYS0ak",1793197856077]