Building blocksInstallFramework guides

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.

src/lib/medos.ts
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);
  });
}
src/BookingPage.tsx
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.

Drop the containerId, switch mode to "modal", and wire a button. Guard the call, since the script may not have loaded yet:

<button onClick={() => window.MedosBooking?.open()}>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.

On this page