import { type ReactNode, useEffect, useState } from "react" import { type InitCustomBlockOptions, NotInIframeError } from "../index.js" import { DebugMessageLog } from "./DebugMessageLog.js" import { seedStandalonePreviewState } from "./standalonePreview.js" import { useCustomBlockAutoResize } from "./useCustomBlockAutoResize.js" import { useCustomBlockInit } from "./useCustomBlockInit.js" import { useCustomBlockHost } from "./useHostState.js" import "./NotionCustomBlock.css" /** * Props accepted by {@link NotionCustomBlock}. */ export type NotionCustomBlockProps = InitCustomBlockOptions & { children: ReactNode /** * Rendered while the SDK ↔ host handshake is in progress. Defaults to * `null` (nothing). */ fallback?: ReactNode /** * Rendered when the handshake fails. Either a node, or a function that * receives the `Error`. When omitted, a friendly default error card is * rendered with developer details — replace it for production templates. */ errorFallback?: ReactNode | ((error: Error) => ReactNode) /** * Whether the provider should automatically post resize messages so the * host iframe matches the content height of `#root`. Defaults to `true`. * Pass `false` when you want to use the default block size and are ok * with scrollbars within the Notion client. * * @default true */ autoResize?: boolean } /** * Top-level wrapper that runs the SDK ↔ host handshake and gates `children` * until it resolves. Passes `timeoutMs` straight through to * {@link initCustomBlock}. * * Templates that prefer not to write a `Root` gating component (or top-level * `await`) can mount their app entirely inside this provider: * * ```tsx * ReactDOM.createRoot(root).render( * * * , * ) * ``` * * Inside `children`, every SDK hook is guaranteed to return a populated * value. Outside the provider (or during the loading window), they throw. */ export function NotionCustomBlock({ children, timeoutMs, fallback = null, errorFallback, autoResize = true, }: NotionCustomBlockProps) { const init = useCustomBlockInit({ timeoutMs }) useCustomBlockAutoResize({ enabled: autoResize }) const isStandalone = init.error instanceof NotInIframeError const host = useCustomBlockHost() const [debugOpen, setDebugOpen] = useState(false) useEffect(() => { const onKeyDown = (e: KeyboardEvent) => { if (e.key === "\\") { setDebugOpen(prev => !prev) } } window.addEventListener("keydown", onKeyDown) return () => window.removeEventListener("keydown", onKeyDown) }, []) useEffect(() => { if (!isStandalone) { return } console.warn(`[notion-custom-sdk] ${init.error?.message}`) seedStandalonePreviewState() }, [isStandalone, init.error]) if (debugOpen) { return } if (init.error && !isStandalone) { if (errorFallback === undefined) { return } return ( <> {typeof errorFallback === "function" ? errorFallback(init.error) : errorFallback} ) } if (isStandalone) { // Wait for the effect to seed placeholder host state; otherwise hooks // in `children` would throw. if (host.status !== "initialized") { return <>{fallback} } return ( <>
Notion host not detected — running in standalone preview. SDK hooks return placeholder values until embedded in Notion.
{children} ) } if (!init.isLoaded) { return <>{fallback} } return <>{children} } // TODO(custom-blocks): Align the styling of this with the host-side error states. function DefaultInitErrorFallback({ error }: { error: Error }) { useEffect(() => { console.warn( `[notion-custom-sdk] Custom block init failed: ${error.message}`, ) }, [error]) return (
Custom block

Couldn't connect to Notion

This custom block loaded, but the setup handshake didn't finish. Try again from Notion. If this keeps happening, check the browser console for SDK details.

Developer details
{error.message}
) }