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

app/book/booking-widget.tsx
"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 }} />
    </>
  );
}
app/book/page.tsx
import { BookingWidget } from "./booking-widget";

export default function BookPage() {
  return (
    <>
      <h1>Book an appointment</h1>
      <BookingWidget />
    </>
  );
}

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".

On this page