> ## Documentation Index
> Fetch the complete documentation index at: https://docs.frogbot.ai/llms.txt
> Use this file to discover all available pages before exploring further.

# Client-side Live Preview

> Implement Live Preview in a client-side front-end application.

> [!NOTE]
>
> If your frontend supports Server Components, use [server-side Live Preview](https://docs.frogbot.ai/live-preview/server.md).

The Admin Panel sends a `window.postMessage` event whenever document form state changes. Your front-end application listens for these events and re-renders with the latest data.

FrogBot provides `useLivePreview` integrations for React and Vue. Other frameworks can use the framework-independent package described in [Building your own hook](#building-your-own-hook).

`useLivePreview` accepts these options:

| Option | Type | Default | Description |
| - | - | - | - |
| `serverURL` | `string` | Required | Origin of your FrogBot server, such as `https://cms.example.com`. Messages from any other origin are ignored. |
| `initialData` | `T` | Required | The document data fetched for the first render. It is returned until the first update arrives. |
| `depth` | `number` | The server's `defaultDepth` (`1` unless configured) | Relationship population depth for each update. |
| `apiRoute` | `string` | `'/api'` | Your FrogBot REST API route, used to populate updates. |
| `requestHandler` | `CollectionPopulationRequestHandler` | A `fetch` with `credentials: 'include'` | Replaces the request that populates each update, for example to route it through your own middleware. See [Custom population requests](#custom-population-requests). |

The hooks return `data`, the latest populated document, and `isLoading`, which is `true` until the first update arrives. The Vue composable returns both as refs.

> [!NOTE]
>
> Guard against required fields being removed while editing. Use conditional rendering, optional chaining, or defaults instead of assuming nested data always exists.

> [!NOTE]
>
> The hook's `depth` must exactly match the depth of the initial request. See [Depth](https://docs.frogbot.ai/queries/depth.md).

## Frameworks

### React

```bash theme={"theme":"tokyo-night"}
npm install @frogbotai/live-preview-react
```

```tsx theme={"theme":"tokyo-night"}
'use client'

import type { Page } from '@/frogbot-types'

import { useLivePreview } from '@frogbotai/live-preview-react'

export function PageClient({ page }: { page: Page }) {
  const { data } = useLivePreview<Page>({
    initialData: page,
    serverURL: process.env.NEXT_PUBLIC_FROGBOT_URL as string,
    depth: 2,
  })

  return <h1>{data.title}</h1>
}
```

> [!WARNING]
>
> For React Server Components, use [server-side Live Preview](https://docs.frogbot.ai/live-preview/server.md) instead.

### Vue

```bash theme={"theme":"tokyo-night"}
npm install @frogbotai/live-preview-vue
```

```vue theme={"theme":"tokyo-night"}
<script setup lang="ts">
import type { Page } from '~/frogbot-types'

import { useLivePreview } from '@frogbotai/live-preview-vue'

const props = defineProps<{ initialData: Page }>()

const { data } = useLivePreview<Page>({
  initialData: props.initialData,
  serverURL: 'http://localhost:3000',
  depth: 2,
})
</script>

<template>
  <h1>{{ data.title }}</h1>
</template>
```

## Building your own hook

```bash theme={"theme":"tokyo-night"}
npm install @frogbotai/live-preview
```

| Export | Description |
| - | - |
| `subscribe(options)` | Listens for Admin Panel messages, populates each update, and passes the merged document to `callback`. Returns the listener to pass to `unsubscribe`. |
| `unsubscribe(listener)` | Removes a listener returned by `subscribe`. |
| `ready({ serverURL })` | Tells the Admin Panel that the frontend is ready to receive messages. It posts to the parent frame, or to the opening window when the preview runs in a separate window. |
| `isLivePreviewEvent(event, serverURL)` | Returns whether a `MessageEvent` came from `serverURL` and contains Live Preview form state. |
| `handleMessage(options)` | Populates one message event and returns the merged document. `subscribe` calls it for you. It accepts the `subscribe` options with `event` in place of `callback`. |
| `mergeData(options)` | Sends unsaved form data to the server and returns the populated document. `handleMessage` calls it for you. |

`subscribe` accepts these options:

| Option | Type | Default | Description |
| - | - | - | - |
| `serverURL` | `string` | Required | Origin of your FrogBot server. Messages from any other origin are ignored. |
| `initialData` | `T` | Required | The document data fetched for the first render. |
| `callback` | `(data: T) => void` | Required | Receives the merged document after each update. |
| `depth` | `number` | The server's `defaultDepth` (`1` unless configured) | Relationship population depth for each update. |
| `apiRoute` | `string` | `'/api'` | Your FrogBot REST API route. |
| `requestHandler` | `CollectionPopulationRequestHandler` | A `fetch` with `credentials: 'include'` | Replaces the population request. See [Custom population requests](#custom-population-requests). |

Each call to `subscribe` resets the merged data cached by an earlier subscription, so a new page doesn't inherit the previous page's changes.

```tsx theme={"theme":"tokyo-night"}
import { ready, subscribe, unsubscribe } from '@frogbotai/live-preview'
import { useEffect, useRef, useState } from 'react'

export function useLivePreview<T>({
  depth = 0,
  initialData,
  serverURL,
}: {
  depth?: number
  initialData: T
  serverURL: string
}) {
  const [data, setData] = useState(initialData)
  const [isLoading, setIsLoading] = useState(true)
  const hasSentReadyMessage = useRef(false)

  useEffect(() => {
    const subscription = subscribe({
      callback: (mergedData: T) => {
        setData(mergedData)
        setIsLoading(false)
      },
      depth,
      initialData,
      serverURL,
    })

    if (!hasSentReadyMessage.current) {
      hasSentReadyMessage.current = true
      ready({ serverURL })
    }

    return () => unsubscribe(subscription)
  }, [depth, initialData, serverURL])

  return { data, isLoading }
}
```

### Custom population requests

Each update is sent back to your FrogBot server so relationships and uploads can be populated to `depth`. Pass `requestHandler` to change how that request is made. It must return a `Response` whose JSON body is the populated document.

```ts theme={"theme":"tokyo-night"}
import type { CollectionPopulationRequestHandler } from '@frogbotai/live-preview'

export const requestHandler: CollectionPopulationRequestHandler = ({ apiPath, data, endpoint, serverURL }) =>
  fetch(`${serverURL}${apiPath}/${endpoint}`, {
    method: 'POST',
    body: JSON.stringify(data),
    credentials: 'include',
    headers: {
      'Content-Type': 'application/json',
      'X-HTTP-Method-Override': 'GET',
    },
  })
```

| Argument | Type | Description |
| - | - | - |
| `serverURL` | `string` | The `serverURL` you passed. |
| `apiPath` | `string` | The `apiRoute` you passed, or `'/api'`. |
| `endpoint` | `string` | Path of the document below the API route, such as `pages/123`. |
| `data` | `{ data, depth, flattenLocales: false, locale }` | Request body: the unsaved form data, the population depth, and the edited locale. |

## Troubleshooting

### Relationships or uploads are not populated

When the frontend and FrogBot server use different domains, ports, or subdomains, configure [CORS](https://docs.frogbot.ai/configuration/overview.md). If protected resources require cookies, also configure [CSRF](https://docs.frogbot.ai/authentication/cookies.md#csrf-prevention).

```ts theme={"theme":"tokyo-night"}
import { buildConfig } from 'frogbot'

export default buildConfig({
  cors: ['http://localhost:3001'],
  csrf: ['http://localhost:3001'],
})
```

### Relationships or uploads disappear after editing

Ensure the initial request and `useLivePreview` use the same [depth](https://docs.frogbot.ai/queries/depth.md):

```tsx theme={"theme":"tokyo-night"}
const { docs } = await frogbot.find({
  collection: 'pages',
  overrideAccess: true,
  depth: 1,
  where: {
    slug: {
      equals: 'home',
    },
  },
})
```

```tsx theme={"theme":"tokyo-night"}
const { data } = useLivePreview<Page>({
  initialData: initialPage,
  serverURL: FROGBOT_SERVER_URL,
  depth: 1,
})
```

### Iframe refuses to connect

If your frontend's [Content Security Policy](https://developer.mozilla.org/en-US/docs/Web/HTTP/CSP) prevents the Admin Panel from embedding it, allow the Admin Panel domain with `frame-ancestors`:

```plaintext theme={"theme":"tokyo-night"}
frame-ancestors: "self" localhost:* https://your-site.com;
```


This documentation is built and hosted on [Mintlify](https://mintlify.com), a developer documentation platform.