# Click to Edit - Advanced API

Advanced Preview SDK APIs for Click to Edit, including React hooks, Preview methods, DOM events, and helpers for dynamic content.

This page covers advanced Preview SDK APIs beyond the standard [Click to Edit setup](https://hygraph.com/docs/developer-guides/schema/click-to-edit). Use it when you need custom save handling, connection status, dynamic DOM updates, live updates for scalar lists, or lower-level control than `PreviewWrapper` provides.

For framework walkthroughs, see:
- [Next.js App Router](https://hygraph.com/docs/developer-guides/schema/click-to-edit-next-js-app-router)
- [Next.js Pages Router](https://hygraph.com/docs/developer-guides/schema/click-to-edit-next-js-pages-router)
- [Remix](https://hygraph.com/docs/developer-guides/schema/click-to-edit-remix)
- [Vue / Nuxt](https://hygraph.com/docs/developer-guides/schema/click-to-edit-vue-nuxt)
- [Vanilla JavaScript](https://hygraph.com/docs/developer-guides/schema/click-to-edit-vanilla-js)

## Additional configuration

These options extend the [configuration properties](https://hygraph.com/docs/developer-guides/schema/click-to-edit#configuration-properties) on the parent setup page.

| Property | Required | Description |
|---|---|---|
| `overlayEnabled` | Optional | Set to `false` to disable hover overlays and **Edit** buttons while keeping the Studio connection active. Defaults to `true`. Available on both `HygraphPreview` and the core `Preview` constructor. |
| `onFieldUpdate` | Optional | Handle live field updates yourself. Receives the update (`entryId`, `fieldApiId`, `fieldType`, `newValue`, and `isList` or `componentChain` wherever applicable). **Setting it replaces the built-in updater for every field**, not just the ones you handle: nothing is written to the DOM unless you write it. Leave it unset to keep the built-in behavior and listen for [`preview:field-updated`](#dom-events) instead. |

```tsx
<HygraphPreview
  endpoint={process.env.NEXT_PUBLIC_HYGRAPH_ENDPOINT!}
  studioUrl={process.env.NEXT_PUBLIC_HYGRAPH_STUDIO_URL}
  overlayEnabled={false}
  onSave={() => router.refresh()}
>
  {children}
</HygraphPreview>
```

## React hooks

Import hooks from `@hygraph/preview-sdk/react`. They must run inside a tree wrapped by `HygraphPreview`.

### `usePreview`

Returns the Preview instance and connection state.

```tsx
import { usePreview } from '@hygraph/preview-sdk/react';

function PreviewStatus() {
  const { preview, isReady, isConnected } = usePreview();

  return (
    <p>
      Ready: {String(isReady)} · Connected: {String(isConnected)} · Mode:{' '}
      {preview?.getMode() ?? 'n/a'}
    </p>
  );
}
```

### `usePreviewSave`

Subscribes to save events. Use this instead of (or in addition to) the `onSave` prop when save handling lives in a child component.

```tsx
import { usePreviewSave } from '@hygraph/preview-sdk/react';
import { useRouter } from 'next/navigation';

function SaveListener() {
  const router = useRouter();

  usePreviewSave((entryId) => {
    console.log('Saved entry:', entryId);
    router.refresh();
  });

  return null;
}
```

### `usePreviewEvent`

Subscribes to any [DOM event](#dom-events) the SDK dispatches on `document`.

```tsx
import { usePreviewEvent } from '@hygraph/preview-sdk/react';

function FieldClickListener() {
  usePreviewEvent('preview:field-click', (event) => {
    console.log('Field clicked:', event.detail);
  });

  return null;
}
```

### Other React hooks

| Hook | Description |
|---|---|
| `usePreviewRefresh` | Returns a framework-aware `refresh()` helper. Falls back to `window.location.reload()` when no framework integration is detected. |
| `usePreviewRemix` | Subscribes to save events and revalidates with the Remix revalidator when available. |
| `usePreviewFieldUpdates` | Callbacks for `preview:field-updated` and `preview:update-failed` when `sync.fieldUpdate` is enabled. |
| `usePreviewConnection` | Returns `{ isConnected, isReady, mode }`. |
| `usePreviewActions` | Returns `{ refresh, destroy, getVersion, getMode }` for manual control. |
| `usePreviewDebug` | Returns registry stats and framework detection for development. |

### `HygraphPreviewNextjs`

Optional Next.js helper that wires `refresh` for you. Most apps use `HygraphPreview` with `next/dynamic` and `onSave={() => router.refresh()}` instead, as shown in the [App Router guide](https://hygraph.com/docs/developer-guides/schema/click-to-edit-next-js-app-router).

```tsx
import { HygraphPreviewNextjs } from '@hygraph/preview-sdk/react';
import { useRouter } from 'next/navigation';

export function PreviewWrapper({ children }: { children: React.ReactNode }) {
  const router = useRouter();

  return (
    <HygraphPreviewNextjs
      endpoint={process.env.NEXT_PUBLIC_HYGRAPH_ENDPOINT!}
      studioUrl={process.env.NEXT_PUBLIC_HYGRAPH_STUDIO_URL}
      refresh={router.refresh}
    >
      {children}
    </HygraphPreviewNextjs>
  );
}
```

## Core `Preview` methods

When you initialize the SDK with `new Preview()` (vanilla JavaScript, Vue, Nuxt), use these methods on the instance.

| Method | Description |
|---|---|
| `subscribe('save', { callback })` | Listens for Studio save events. Returns an unsubscribe function. |
| `refresh()` | Re-scans the DOM for `data-hygraph-*` attributes. Call after you inject or replace HTML without a full page reload. |
| `getMode()` | Returns `'iframe'` or `'standalone'`. |
| `isConnected()` | Returns whether the SDK is connected to Studio. |
| `getVersion()` | Returns the SDK version string. |
| `configureOverlay(config)` | Updates overlay styles at runtime. |
| `destroy()` | Tears down listeners, overlays, and the Studio connection. Call on page unload in SPAs. |
| `getFrameworkIntegration()` | Returns the SDK's framework integration helper used for framework-aware refresh. |
| `getFieldRegistryStats()` | Returns counts and diagnostics for registered preview fields. Useful when debugging missing overlays. |
| `getFieldRegistryKeys()` | Returns the registry keys for currently tracked fields. |

```js
import { Preview } from '@hygraph/preview-sdk/core';

const preview = new Preview({
  endpoint: process.env.HYGRAPH_ENDPOINT,
  studioUrl: process.env.HYGRAPH_STUDIO_URL,
  debug: true,
});

const unsubscribe = preview.subscribe('save', {
  callback: (entryId) => {
    console.log('Saved:', entryId);
    window.location.reload();
  },
});

window.addEventListener('beforeunload', () => {
  unsubscribe();
  preview.destroy();
});
```

### Dynamic content without a full reload

If your app updates the DOM without navigating away, call `refresh()` after new marked-up HTML is in the page:

```js
container.innerHTML = `
  <h1
    data-hygraph-entry-id="${entry.id}"
    data-hygraph-field-api-id="title"
  >
    ${entry.title}
  </h1>
`;

preview.refresh();
```

## Live preview for component arrays

When `sync.fieldUpdate` is enabled, the SDK can update component arrays (modular content) in the preview without a full page refresh:

- **Reordering** (drag-and-drop) — existing DOM elements move in place
- **Deletion** — removed components disappear immediately
- **Addition** — new unsaved components are skipped until you save and refresh the page

### Set up a component array container

The SDK needs:

1. A **container element** with `data-hygraph-entry-id` and `data-hygraph-field-api-id` pointing to the component array field
2. **Direct children** with `data-hygraph-component-chain` so the SDK can identify each component instance

```tsx
import {
  createPreviewAttributes,
  createComponentChainLink,
} from '@hygraph/preview-sdk/core';

function ArticlePage({ article }) {
  return (
    <main>
      <h1 {...createPreviewAttributes({ entryId: article.id, fieldApiId: 'title' })}>
        {article.title}
      </h1>

      {/* Container — the SDK targets this for reordering */}
      <div
        {...createPreviewAttributes({
          entryId: article.id,
          fieldApiId: 'content', // Must match the component array field API ID
        })}
      >
        {article.content.map((block) => {
          const componentChain = [createComponentChainLink('content', block.id)];

          return (
            <div
              key={block.id}
              data-hygraph-component-chain={JSON.stringify(componentChain)}
            >
              <ContentBlock
                block={block}
                articleId={article.id}
                componentChain={componentChain}
              />
            </div>
          );
        })}
      </div>
    </main>
  );
}
```

### How it works

When Studio detects a structural change (reorder, add, or delete) in a component array, it sends a `COMPONENT_ARRAY` field update. The SDK then:

1. Finds the container via `data-hygraph-entry-id` and `data-hygraph-field-api-id`
2. Reads `data-hygraph-component-chain` from each direct child to map component IDs to DOM elements
3. Reorders existing DOM elements to match the new array order
4. Removes elements for deletions
5. Skips new unsaved components until save and refresh

For Rich Text fields rendered as HTML inside components, add `data-hygraph-rich-text-format="html"` so live field updates use the correct format.

### Troubleshooting component arrays

- Confirm the **container** has `data-hygraph-entry-id` and `data-hygraph-field-api-id` matching the component array field.
- Confirm each direct child has `data-hygraph-component-chain` with the component instance ID.
- Enable `debug={true}` (or `debug: true`) to see `[ContentUpdater] COMPONENT_ARRAY` logs.
- New unsaved components appear only after saving and refreshing. Reordering and deletion of existing components work immediately when `sync.fieldUpdate` is enabled.

## Live preview for scalar lists

When `sync.fieldUpdate` is enabled, a field marked as a list in the schema, such as String, ID, Enumeration, Int, Float, Boolean, Date, or DateTime, updates in the preview as an editor types. This needs `@hygraph/preview-sdk` 1.1.0 or later: Studio withholds list updates from earlier versions, which have no way to render them.

Studio sends the whole array on every change, so each update carries the full list, not a delta. You can bind that list to a single element, tag one element per item, or render it yourself.

### Bind the whole list to one element

An element tagged with the entry ID and the field API ID receives every item, joined with `, `:

```tsx
<p {...createPreviewAttributes({ entryId: recipe.id, fieldApiId: 'tags' })}>
  {recipe.tags.join(', ')}
</p>
```

The SDK only patches the elements your page rendered. So keep these two things in mind:

- Join your own render with `,`. Any other separator is replaced by `,` on the first live update, and the preview stops matching the page a reload would produce.
- Items are written as plain values, not formatted ones. A Date list renders the stored values, such as `2026-01-15`, where a single Date field is formatted for the browser's locale. An item that is `null` renders as an empty string, so `[10, null, 30]` reads `10, , 30`. Use per-item bindings or your own rendering if the formatting matters.

An `<input>` or `<textarea>` receives the joined string as its `value`.

The SDK cannot rebuild markup it did not render, so it refuses to write a list into an element that has child elements, such as a `<ul>` of `<li>` items. It dispatches `preview:update-failed` instead, and your markup stays as it is.

### Bind each item to its own element

Pass `listIndex` as a non-negative integer to tag one element per item. Each element then receives the item at its index:

```tsx
<ul>
  {recipe.tags.map((tag, index) => (
    <li
      key={`${tag}-${index}`}
      {...createPreviewAttributes({
        entryId: recipe.id,
        fieldApiId: 'tags',
        listIndex: index,
      })}
    >
      {tag}
    </li>
  ))}
</ul>
```

The SDK only ever patches the elements your page rendered. It does not add or remove any elements. So the item count does not follow the editor:

- An item removed in Studio empties the element bound to that index rather than removing it, which leaves a blank row (an empty bullet, for instance) until the next reload.
- An item added in Studio appears only if an element is already tagged with that index. Tagging one spare index renders nothing until an item exists there.

### Render the list yourself

For a list whose length changes as editors work, or whose items need your own formatting, take the array and render it. Either listen for the event:

```js
document.addEventListener('preview:field-updated', (event) => {
  if (event.detail.fieldApiId === 'tags' && event.detail.isList) {
    renderTags(event.detail.newValue); // the full array
  }
});
```

Or pass `onFieldUpdate`, which hands you every update before the DOM is touched. Note that it replaces the built-in updater for every field, so a page that sets it is responsible for rendering all of its live updates, not just its lists:

```tsx
<HygraphPreview
  endpoint={process.env.NEXT_PUBLIC_HYGRAPH_ENDPOINT!}
  sync={{ fieldUpdate: true }}
  onFieldUpdate={(update) => {
    // A scalar list arrives as the full array, every other field as a single value.
    // Render from state and the item count follows the editor.
    setFields((current) => ({ ...current, [update.fieldApiId]: update.newValue }));
  }}
>
  {children}
</HygraphPreview>
```

```tsx
<ul>
  {(fields.tags ?? recipe.tags).map((tag, index) => (
    <li key={`${tag}-${index}`}>{tag}</li>
  ))}
</ul>
```

### Fields that are lists but do not update

`isList` covers scalar lists only. A Rich Text list and a JSON field holding an array are skipped deliberately: their values are arrays of rich text ASTs or of arbitrary JSON, which Studio cannot tell apart from a String list and the SDK could not render either way. Those fields change in the preview after a save and a refresh. Component arrays have their own path; see [live preview for component arrays](#live-preview-for-component-arrays).

### Localized lists

This SDK carries no locale. The `data-hygraph-*` attributes identify an entry and a field, but not a locale. A binding for a localized list is therefore patched by an edit in any locale, including one the preview is not showing. If your page renders a single locale, re-fetch on save rather than trusting a live update that may have come from another one.

### Troubleshooting scalar lists

- Confirm the installed `@hygraph/preview-sdk` is 1.1.0 or later. Earlier versions do not receive list updates.
- Confirm `sync={{ fieldUpdate: true }}` is set on your `PreviewWrapper` component.
- Confirm the element you tagged has no child elements, or that each item carries `data-hygraph-list-index`.
- Enable `debug` and watch for `preview:update-failed`. It names the field and, for a container the SDK refused, says which element it would have destroyed.
- Nothing at all in the log for a list field? Check that the field really is a scalar list in the schema. A Rich Text or JSON field sends no list update.

## DOM events

The SDK dispatches these events on `document`. Listen with `addEventListener` or `usePreviewEvent`.

| Event | `event.detail` | When it fires |
|---|---|---|
| `preview:ready` | `{ preview }` | SDK finished initializing. |
| `preview:connected` | `{ studioOrigin }` | Connected to Studio in iframe mode. |
| `preview:disconnected` | `{}` | Connection to Studio ended. |
| `preview:content-saved` | `{ entryId, timestamp }` | Studio reported a save. |
| `preview:field-click` | `{ entryId, fieldApiId?, componentChain?, mode? }` | Editor clicked an **Edit** overlay. |
| `preview:field-focus` | `{ entryId, fieldApiId }` | Studio requested field focus sync. |
| `preview:field-updated` | `{ entryId, fieldApiId, newValue, fieldType?, componentChain?, isList?, transformedValue? }` | A live field update reached the DOM (`sync.fieldUpdate: true`). `isList` is `true` when `newValue` is a list; `transformedValue` carries the flattened array for a component array. With `onFieldUpdate` set, this fires for every update the handler receives. |
| `preview:update-failed` | `{ entryId, fieldApiId, error }` | A live update could not be applied. The element does not carry the field, or the value cannot be rendered into the element that does. Only the built-in updater reports this; a page using `onFieldUpdate` never sees it. |

```js
document.addEventListener('preview:ready', () => {
  console.log('Preview SDK ready');
});

document.addEventListener('preview:content-saved', (event) => {
  console.log('Content saved:', event.detail.entryId);
});

document.addEventListener('preview:field-click', (event) => {
  console.log('Field clicked:', event.detail);
});
```

## Attribute helpers

These helpers from `@hygraph/preview-sdk/core` are used throughout the framework guides. The parent [attribute reference](https://hygraph.com/docs/developer-guides/schema/click-to-edit#add-data-attributes-to-content-elements) covers the HTML attributes they produce.

| Helper | Description |
|---|---|
| `createPreviewAttributes({ entryId, fieldApiId?, componentChain?, listIndex? })` | Returns an object of `data-hygraph-*` attributes for JSX or `v-bind`. Pass `listIndex` for one item of a scalar list; see [live preview for scalar lists](#live-preview-for-scalar-lists). |
| `createComponentChainLink(fieldApiId, instanceId)` | Builds one `{ fieldApiId, instanceId }` link for nested components. |
| `withFieldPath(attributes, fieldPath)` | Adds `data-hygraph-field-path` (for example, `ingredients.0.quantity`) for debugging nested fields. It does not change Studio focus behavior. |

```tsx
import {
  createPreviewAttributes,
  createComponentChainLink,
  withFieldPath,
} from '@hygraph/preview-sdk/core';

const chain = [createComponentChainLink('ingredients', ingredient.id)];

const quantityAttributes = withFieldPath(
  createPreviewAttributes({
    entryId: recipe.id,
    fieldApiId: 'quantity',
    componentChain: chain,
  }),
  `ingredients.${index}.quantity`
);
```

Attributes identify an entry and a field, not a locale, so the same tagged element serves every locale of a localized field. Click to Edit opens the field in the default locale; see the [known limitation](https://hygraph.com/docs/developer-guides/schema/click-to-edit#known-limitation), and [localized lists](#localized-lists) for what that means for live updates.

## What's next

- [Click to Edit setup](https://hygraph.com/docs/developer-guides/schema/click-to-edit): Install steps, attribute reference, Studio widget, and troubleshooting.
- [Click to Edit - Vanilla JavaScript](https://hygraph.com/docs/developer-guides/schema/click-to-edit-vanilla-js): UMD setup and server-rendered attribute examples.
- [Preview SDK on GitHub](https://github.com/hygraph/preview-sdk): Source, changelog, and runnable examples.
