React
Load the CDN bundle in a plain React app (Vite, CRA, etc.).
The widget is client-only. Load the CDN script, wait for it, then initialize.
Load once, init once
React runs effects twice in development (Strict Mode), and client-side
routing can remount your page. Both lead to init() being called more than
once, which mounts a second widget into the same container. The helper below
reuses an existing script tag and clears the container on unmount.
const CDN_URL = "https://widgets.medos.one/v2/unified.js";
declare global {
interface Window {
MedosBooking?: {
init: (config: Record<string, unknown>) => void;
open: (config?: Record<string, unknown>) => void;
close: () => void;
};
}
}
/** Resolves once `window.MedosBooking` is available. Safe to call repeatedly. */
export function loadMedosBooking(): Promise<void> {
if (window.MedosBooking) return Promise.resolve();
const existing = document.querySelector<HTMLScriptElement>(
`script[src="${CDN_URL}"]`,
);
if (existing) {
return new Promise((resolve, reject) => {
existing.addEventListener("load", () => resolve(), { once: true });
existing.addEventListener(
"error",
() => reject(new Error("Medos Booking failed to load")),
{ once: true },
);
});
}
return new Promise((resolve, reject) => {
const script = document.createElement("script");
script.src = CDN_URL;
script.async = true;
script.onload = () => resolve();
script.onerror = () => reject(new Error("Medos Booking failed to load"));
document.head.appendChild(script);
});
}import { useEffect } from "react";
import { loadMedosBooking } from "./lib/medos";
const CONTAINER_ID = "medos-booking";
export function BookingPage() {
useEffect(() => {
let cancelled = false;
loadMedosBooking()
.then(() => {
if (cancelled) return;
window.MedosBooking!.init({
apiKey: "mk_your_publishable_key",
mode: "inline",
containerId: CONTAINER_ID,
});
})
.catch((err) => console.error(err));
return () => {
cancelled = true;
// Inline mode has no close() — clear the container so a remount
// doesn't stack a second widget on top.
const el = document.getElementById(CONTAINER_ID);
if (el) el.innerHTML = "";
};
}, []);
return <div id={CONTAINER_ID} style={{ minHeight: 600 }} />;
}Note the script tag is left in the document on unmount. That's deliberate — it stays cached and the next mount reuses it instead of downloading again.
Modal mode
init({ mode: "modal" }) opens the modal as soon as it runs, so don't call it
on mount. Load the script the same way, then start the widget from the button —
init() on the first click, open() after that:
const started = useRef(false);
function book() {
if (!window.MedosBooking) return; // bundle still loading
if (started.current) return window.MedosBooking.open();
started.current = true;
window.MedosBooking.init({ apiKey: "mk_your_publishable_key", mode: "modal" });
}
return <button onClick={book}>Book appointment</button>;In modal mode MedosBooking.close() does tear the widget down, so you can call
it from your own cleanup if you need to.