Building blocksInstallFramework guides
Next.js
SSR-safe integration with Next.js App Router.
The widget mounts client-side, so it lives in a Client Component.
Initialize from the script's onLoad, not from useEffect
next/script injects the bundle after hydration. A useEffect that
checks window.MedosBooking on mount usually runs before the bundle has
loaded, so the check fails and the widget never starts. Drive init() from
the script's onLoad instead — that fires exactly when the global is ready.
Inline
"use client";
import Script from "next/script";
import { useCallback, useEffect, useRef } from "react";
const CONTAINER_ID = "medos-booking";
declare global {
interface Window {
MedosBooking?: {
init: (config: Record<string, unknown>) => void;
open: (config?: Record<string, unknown>) => void;
close: () => void;
};
}
}
export function BookingWidget() {
const started = useRef(false);
const start = useCallback(() => {
// Strict Mode and re-renders can fire this more than once; init() is not
// idempotent, so guard it.
if (started.current || !window.MedosBooking) return;
started.current = true;
window.MedosBooking.init({
apiKey: process.env.NEXT_PUBLIC_MEDOS_API_KEY!,
mode: "inline",
containerId: CONTAINER_ID,
});
}, []);
useEffect(() => {
// Covers the case where the script was already loaded by a previous page.
start();
return () => {
started.current = false;
const el = document.getElementById(CONTAINER_ID);
if (el) el.innerHTML = "";
};
}, [start]);
return (
<>
<Script
src="https://widgets.medos.one/v2/unified.js"
strategy="afterInteractive"
onLoad={start}
/>
<div id={CONTAINER_ID} style={{ minHeight: 600 }} />
</>
);
}import { BookingWidget } from "./booking-widget";
export default function BookPage() {
return (
<>
<h1>Book an appointment</h1>
<BookingWidget />
</>
);
}Modal
Same structure — drop containerId, use mode: "modal", and open from a
button:
<button onClick={() => window.MedosBooking?.open()}>Book appointment</button>Environment variables
Client-visible env vars must be prefixed with NEXT_PUBLIC_. A key in a
non-NEXT_PUBLIC_ variable will be undefined in the browser, and init()
will throw "API key is required".