# Lifecycle

The SDK ↔ host handshake, the React wrapper that runs it, and the auto-resize hook that keeps the iframe in sync with your content.

## Handshake

`initCustomBlock()` posts `ready` to `window.parent` and awaits the host's `init` (theme, block ID/parent/page ID, current user, and `dataSources: { bindings }` keyed by semantic data-source key, which the SDK resolves against the manifest). The promise resolves with the normalized initial state, captured in `CustomBlockInitial` — `await` it before mounting React so hooks always see populated state.

- Default `timeoutMs` is 15000; rejects with `CustomBlockInitError` code `init_timeout` if the host doesn't respond.
- In a top-level browser tab (no parent frame), rejects with `NotInIframeError` code `not_in_iframe`. `<NotionCustomBlock>` catches this, seeds placeholders, and renders `children` behind a warning banner so dev-time previews still work.
- After init, `*Changed` events (e.g. `themeChanged`, `parentChanged`, `dataSourcesChanged`) push updates and the relevant hooks re-render.
- `initCustomBlock` is idempotent; subsequent calls return the same promise.

## Sizing

The host owns width and height. Inside the iframe, `100vh` ≠ a screen and there's no meaningful "device width" — only iframe width. Layouts must reflow from a phone column to a desktop block.

- **Self-sizing content** is the default — `<NotionCustomBlock>` measures `#root` and posts `resize` messages so the iframe tracks your content. Pass `autoResize={false}` for full-bleed views, or to drive `useCustomBlockAutoResize` yourself.
- Prefer container queries (`@container`) over viewport queries.

## API

Import framework-neutral APIs from `ncblock`; import React hooks and components from `ncblock/react`. The runtime APIs below assume `initCustomBlock()` has resolved — initialized-only hooks and `customBlock` getters throw if called before that. Inside `<NotionCustomBlock>` (or past the `isLoaded` gate of `useCustomBlockInit`), single-value hooks return non-nullable values.

### `<NotionCustomBlock>`

```ts
type NotionCustomBlockProps = InitCustomBlockOptions & {
  children: ReactNode;
  fallback?: ReactNode;
  errorFallback?: ReactNode | ((error: Error) => ReactNode);
  autoResize?: boolean; // defaults to true
};
```

Top-level wrapper. Runs the handshake, gates `children`, and (by default) drives auto-resize. `fallback` replaces the loading view (default `null`); `errorFallback` replaces the inline `<p role="alert">` shown if init rejects. `timeoutMs` flows through to `initCustomBlock`. Pass `autoResize={false}` for full-bleed views or to call `useCustomBlockAutoResize` yourself.

### `useCustomBlockInit(opts?)`

```ts
function useCustomBlockInit(
  opts?: InitCustomBlockOptions,
): UseCustomBlockInitResult;

type UseCustomBlockInitResult =
  | { isLoaded: false; error: undefined }
  | { isLoaded: false; error: CustomBlockInitFailure }
  | { isLoaded: true; error: undefined; initial: CustomBlockInitial };
```

React wrapper around `initCustomBlock` for templates that prefer not to use top-level `await`. Multiple components calling it share the same handshake.

```tsx
function Root() {
  const init = useCustomBlockInit();
  if (init.error) return <p role="alert">Init failed: {init.error.message}</p>;
  if (!init.isLoaded) return null;
  return <App />;
}
```

### `initCustomBlock(opts?)`

```ts
function initCustomBlock(
  opts?: InitCustomBlockOptions,
): Promise<CustomBlockInitial>;

type InitCustomBlockOptions = { timeoutMs?: number };
```

The lower-level promise API. `<NotionCustomBlock>` and `useCustomBlockInit` both call it for you. Reach for it directly only when you want to `await` init at module scope (e.g. before `ReactDOM.createRoot`). If the host replies with an init error, the promise rejects with `CustomBlockInitError`.

### `CustomBlockInitError`

```ts
class CustomBlockInitError extends Error {
  code: CustomBlockInitErrorCode;
  isRetryable: boolean;
}

type CustomBlockErrorInfo<TCode extends string = string> = {
  code: TCode;
  message: string;
  isRetryable: boolean;
};

type CustomBlockInitErrorInfo = CustomBlockErrorInfo<CustomBlockInitErrorCode>;

type CustomBlockInitErrorCode =
  | "no_ready"
  | "invalid_ready"
  | "manifest_unavailable"
  | "manifest_invalid"
  | "invalid_protocol_version"
  | "unsupported_protocol_version"
  | "context_unavailable"
  | "current_user_unavailable"
  | "missing_data_source_binding"
  | "data_source_unavailable"
  | "missing_property_binding"
  | "invalid_property_binding"
  | "not_in_iframe"
  | "init_timeout"
  | "unknown_error";
```

Thrown when the host rejects initialization instead of returning the initial theme, block location, current user, and data sources. `CustomBlockInitErrorInfo` uses the same structured error payload as every other API, with `code` narrowed to `CustomBlockInitErrorCode`. Use `error instanceof CustomBlockInitError`, `error.code`, and `error.isRetryable` to branch on host-reported setup failures.

Runtime APIs use the same `CustomBlockErrorInfo<TCode>` envelope with their own code unions, such as `CustomBlockCreatePageErrorCode`, `CustomBlockUpdatePageErrorCode`, and `CustomBlockQueryDataSourceErrorCode`.

The public API-specific runtime aliases are `CustomBlockCreatePageErrorInfo`, `CustomBlockGetPageErrorInfo`, `CustomBlockGetUserErrorInfo`, `CustomBlockListUsersErrorInfo`, `CustomBlockQueryDataSourceErrorInfo`, and `CustomBlockUpdatePageErrorInfo`. Their `code` fields are narrowed by `CustomBlockCreatePageErrorCode`, `CustomBlockGetPageErrorCode`, `CustomBlockGetUserErrorCode`, `CustomBlockListUsersErrorCode`, `CustomBlockQueryDataSourceErrorCode`, and `CustomBlockUpdatePageErrorCode`. Shared categories include `CustomBlockPropertyErrorCode` / `CustomBlockPropertyErrorInfo` and `CustomBlockDataSourceResolutionErrorCode` / `CustomBlockDataSourceResolutionErrorInfo`.

### `customBlock`

Framework-neutral runtime APIs for renderers that do not use React hooks. `customBlock.getState()` returns a `CustomBlockState` snapshot that hides internal query cache details. Initialized-only getters (`getTheme`, `getBlockId`, `getParent`, `getPage`, and `getCurrentUser`) throw until `initCustomBlock()` resolves. `getManifest()` is not gated on init — it returns the declared manifest (or `null`) regardless.

`customBlock` covers runtime state and sizing. Row querying still goes through `useDataSource`, while imperative APIs such as `pages.*` and `users.*` are already framework-neutral functions.

```ts
await initCustomBlock();

const theme = customBlock.getTheme();
const unsubscribe = customBlock.subscribe(() => {
  render(customBlock.getState());
});
```

For non-React auto-resize, pass the element whose content height should drive the host iframe. The helper posts one initial measurement, observes later size changes when `ResizeObserver` is available, dedupes unchanged heights, and returns a cleanup function:

```ts
const stopAutoResize = customBlock.autoResize({
  target: document.getElementById("root"),
});

// Later, if your renderer unmounts:
stopAutoResize();
```

### `NotInIframeError`

Thrown when `initCustomBlock` is called in a top-level tab (no parent frame). It has `code: "not_in_iframe"` and `isRetryable: false`. `<NotionCustomBlock>` catches it and falls back to a standalone preview with a warning banner; direct callers can `instanceof NotInIframeError` to apply their own policy.

### `useCustomBlockAutoResize({ enabled? })`

```ts
function useCustomBlockAutoResize(args?: { enabled?: boolean }): void;
```

React wrapper around `customBlock.autoResize({ target: document.getElementById("root") })`. Measures `#root`'s height and posts `resize` messages, deduping unchanged values. `<NotionCustomBlock>` runs this for you — only call it directly when you want to drive `enabled` yourself (e.g. a debug toggle), and pair with `autoResize={false}` so it doesn't run twice. The target must have intrinsic height; do not give it `height: 100%` or `100vh`.

```tsx
<NotionCustomBlock autoResize={false}>
  <App />
</NotionCustomBlock>;

function App() {
  const [enabled, setEnabled] = useState(true);
  useCustomBlockAutoResize({ enabled });
  return <div>…</div>;
}
```

## Debug console

Press `\` while focused in a custom block to toggle a debug overlay that replaces the block's children with a `<pre>` log of every `postMessage` sent and received over the bridge. Each line is formatted as:

```
[ISO timestamp] sent/received: {"type":"ready", …}
```

The log is intentionally plain — no filtering or decoration — so it can be copied and pasted directly to a local coding agent for debugging.
