# Migrating from Nutrient Web SDK

Source: https://sdk.pdf.net/docs/v1/migration/from-nutrient



This guide moves a React application built on Nutrient Web SDK (formerly PSPDFKit for Web) to `@sdk-pdf/react`. Nutrient Web SDK mounts a complete viewer application into a container element and hands you an `Instance` with a view state, events, and document APIs; `@sdk-pdf/react` ships the document engine and the page view and leaves the rest of the UI to you. Most of the migration is rebuilding the parts of the Nutrient UI your users rely on as React components, then moving your `instance` calls to hooks. The guide covers the standalone setup, where the document is processed in the browser; [Collaboration](https://sdk.pdf.net/docs/v1/migration/from-nutrient#collaboration) covers setups built on Document Engine and Instant. Work through the checklist below; each step links to the section that covers it.

> **Beta**: `@sdk-pdf/react` is in beta, and its exports and APIs may change between releases. Pin the exact package version and review the changes when you update.

## Migration checklist [#migration-checklist]

* [ ] Read the [overview](https://sdk.pdf.net/docs/v1/migration/from-nutrient#overview): how the two integrations differ, and the API map for every `NutrientViewer` and `instance` call you use.
* [ ] List the Nutrient UI your users rely on (toolbar items, sidebars, annotation toolbars, the Document Editor, the content editor, dialogs): each becomes a React component.
* [ ] [Install the package and replace the `NutrientViewer.load()` effect with `PdfProviders`](https://sdk.pdf.net/docs/v1/migration/from-nutrient#install-and-set-up).
* [ ] [Load and switch documents](https://sdk.pdf.net/docs/v1/migration/from-nutrient#load-and-reset-a-document) with `loadPdf` and `resetPdf`, including the password flow.
* [ ] [Lay out `Viewport` with your own toolbar and sidebar](https://sdk.pdf.net/docs/v1/migration/from-nutrient#display-the-document).
* [ ] [Move page navigation from indexes to page ids](https://sdk.pdf.net/docs/v1/migration/from-nutrient#pages-and-navigation).
* [ ] [Move `applyOperations` page operations to the page hooks](https://sdk.pdf.net/docs/v1/migration/from-nutrient#page-manipulation).
* [ ] [Move zoom and the zoom modes](https://sdk.pdf.net/docs/v1/migration/from-nutrient#zoom-and-fit-modes).
* [ ] [Rebuild the thumbnails sidebar with `PageCanvas`](https://sdk.pdf.net/docs/v1/migration/from-nutrient#thumbnails).
* [ ] [Move form fields and values to widgets](https://sdk.pdf.net/docs/v1/migration/from-nutrient#forms).
* [ ] [Map interaction modes and read-only mode](https://sdk.pdf.net/docs/v1/migration/from-nutrient#editing-and-interaction-tools), then [content editing](https://sdk.pdf.net/docs/v1/migration/from-nutrient#content-editing) and [annotations](https://sdk.pdf.net/docs/v1/migration/from-nutrient#annotations).
* [ ] [Replace the History API and save state tracking](https://sdk.pdf.net/docs/v1/migration/from-nutrient#undo-redo-and-change-tracking).
* [ ] [Replace `save()`, `exportPDF()`, and Instant JSON or XFDF storage](https://sdk.pdf.net/docs/v1/migration/from-nutrient#saving-and-downloading).
* [ ] Check [collaboration](https://sdk.pdf.net/docs/v1/migration/from-nutrient#collaboration) and convert stored positions using [coordinates](https://sdk.pdf.net/docs/v1/migration/from-nutrient#coordinates).
* [ ] [Move error handling and logging](https://sdk.pdf.net/docs/v1/migration/from-nutrient#errors-and-logging) and [cleanup](https://sdk.pdf.net/docs/v1/migration/from-nutrient#cleanup-and-lifecycle).
* [ ] [Plan for features without an equivalent](https://sdk.pdf.net/docs/v1/migration/from-nutrient#features-without-an-equivalent).
* [ ] Remove `@nutrient-sdk/viewer`, the self-hosted assets folder or the `useCDN` setting, the license key or session, and any Document Engine client settings. Test encrypted, malformed, large, and form-heavy PDFs, page operations, and the save round-trip.

## Overview [#overview]

<details>
  <summary>
    How the two integrations differ
  </summary>

  **Nutrient Web SDK** is a packaged application:

  * `NutrientViewer.load(configuration)` mounts the viewer into a `container` element, which needs an explicit width and height, and resolves with an `Instance`. `NutrientViewer.unload(container)` tears it down. In React, the pair lives in a `useEffect`.
  * The viewer brings its own UI: the main toolbar, sidebars, annotation toolbars, the Document Editor, the content editor, and dialogs, rendered inside a shadow root. You shape it from outside with `toolbarItems` and `setToolbarItems`, the `ui` slot configuration (`preset: 'minimal'` hides all of it), `theme`, and `styleSheets`.
  * The UI state is an immutable `ViewState` (current page, zoom, interaction mode, sidebar, read-only mode): read it from `instance.viewState`, change it with `instance.setViewState()`, and observe it through events on the instance, such as `viewState.change`. Document changes arrive the same way: `annotations.change`, `formFieldValues.update`, `document.saveStateChange`.
  * In standalone mode the engine runs as WebAssembly in the browser. Its runtime assets load from the Nutrient CDN (`useCDN: true`) or from your server (`baseUrl`), and a `licenseKey`, or a DWS Viewer API `session` such as a publishable key, removes the trial watermark. Several features are separate license components, such as Form Creator, Document Editor, Content Editor, and comments.
  * Annotations, form fields, and form field values are Immutable.js records that you change with `instance.create()`, `update()`, and `delete()`. Changes are saved as `autoSaveMode` sets, then exported as a PDF (`exportPDF`) or as a diff on top of it (Instant JSON or XFDF).
  * Pages are zero-based indexes, and positions are page coordinates with the origin at the top left of the page.

  **@sdk-pdf/react** is a library: the SDK owns the document, and you own the UI.

  * There are no prebuilt toolbars, sidebars, or dialogs. [`Viewport`](https://sdk.pdf.net/docs/v1/reference/Viewport) is the main view and [`PageCanvas`](https://sdk.pdf.net/docs/v1/reference/PageCanvas) renders single pages; everything around them is your React code driven by hooks.
  * [`PdfProviders`](https://sdk.pdf.net/docs/v1/reference/PdfProviders) hosts the state and configuration inside your React tree. The SDK renders unstyled, and your design system supplies the look through the `theme` configuration.
  * Edits don't rewrite the PDF file. Each change is an operation in a CRDT, which powers the history; [`useDocumentSaving`](https://sdk.pdf.net/docs/v1/reference/useDocumentSaving) builds the final PDF when you need it.
  * Pages and blocks are addressed by session-scoped ids, and positions are user-space units with the origin at the bottom-left corner of the page.

  | Concern       | Nutrient Web SDK                                                        | @sdk-pdf/react                                                                |
  | ------------- | ----------------------------------------------------------------------- | ----------------------------------------------------------------------------- |
  | UI            | Prebuilt, shaped from outside (`toolbarItems`, `ui` slots, `theme`)     | `Viewport` and `PageCanvas` only; you build toolbars, sidebars, and dialogs   |
  | Mounting      | `load()` into a sized container in a `useEffect`, `unload()` in cleanup | `PdfProviders` and components in your React tree                              |
  | Deployment    | Runtime assets from the CDN or `baseUrl`, `licenseKey`                  | The package from a private registry                                           |
  | State access  | `ViewState` and `setViewState()`, events on the instance                | State hooks that re-render, command hooks, and event hooks                    |
  | Pages         | Zero-based indexes                                                      | Session-scoped page ids                                                       |
  | Forms         | Form fields by fully qualified name, widgets as annotations             | Widgets with a `fieldName`, values bound with `useWidgetValue`                |
  | Editing text  | Content Editor component, in a session you save or discard              | Built in under the `PDF_EDITOR` content policy, with no session to save       |
  | Annotations   | Tools and APIs for every common type                                    | Existing annotations display; borders restyle; image stamps and ink from code |
  | Persistence   | `save()` per `autoSaveMode`, then `exportPDF()`, Instant JSON, or XFDF  | `prepareDocument()` for the PDF                                               |
  | Collaboration | Instant, with Document Engine                                           | Coming soon                                                                   |
  | File types    | PDF, Office documents, and images                                       | PDF                                                                           |

  What this means for your code:

  1. **Build the UI.** Every toolbar item, sidebar, and dialog your users rely on becomes a React component built from the hooks in the [API reference](https://sdk.pdf.net/docs/v1/reference).
  2. **Drop the mount effect.** The `load()` and `unload()` effect, the container sizing, the instance reference, and any context that shares it are replaced by `PdfProviders` and hooks.
  3. **Replace the view state.** `instance.viewState` reads become state hooks, and `setViewState()` calls become command hooks: `useScrollToPage`, `useViewportScale`, `useSetInteractionMode`.
  4. **Replace instance events with hooks.** View state and document changes you listened for come from state hooks (`useCurrentPageId`, `useViewportScale`, `useSelection`, `useDocumentRevision`) that re-render on change. `useViewportEvent` reports only user interactions in the viewport, such as typing, drags, and form input, and its subscriptions follow the component lifecycle.
  5. **Replace diff persistence.** Storing Instant JSON or XFDF next to the original file becomes storing the PDF produced by `prepareDocument()`.
  6. **Replace indexes and page coordinates** with page ids and user-space coordinates; check stored positions after converting them.
  7. **Remove deployment pieces:** the assets setup (`useCDN` or `baseUrl`), the license key, and `NutrientViewer.preloadWorker()`.
</details>

<details>
  <summary>
    API map
  </summary>

  Rows marked **No equivalent** are covered in [Features without an equivalent](https://sdk.pdf.net/docs/v1/migration/from-nutrient#features-without-an-equivalent).

  | Nutrient Web SDK                                                                              | @sdk-pdf/react                                                                                                                                   | Notes                                                                                                    |
  | --------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------ | -------------------------------------------------------------------------------------------------------- |
  | `NutrientViewer.load({ container, licenseKey, useCDN })`, or `session` with a publishable key | `<PdfProviders>`                                                                                                                                 | No container, assets setup, license key, or session.                                                     |
  | `document` as a URL or an `ArrayBuffer`                                                       | `loadPdf(Uint8Array)` from [`useLoadPdf`](https://sdk.pdf.net/docs/v1/reference/useLoadPdf)                                                                         | `fetch` the URL into bytes first.                                                                        |
  | `password`, `onPasswordRequired`, `maxPasswordRetries`                                        | `loadPdf(data, { password })`, catch `InvalidPasswordError`                                                                                      | No built-in password prompt.                                                                             |
  | `httpHeaders`, `signal`                                                                       | **No equivalent**                                                                                                                                | Headers and cancellation go on your own `fetch`.                                                         |
  | The resolved `load()` promise                                                                 | `loadState.status === 'ready'`                                                                                                                   |                                                                                                          |
  | `NutrientViewer.unload(container)`, then `load()` with another document                       | `resetPdf()` from [`useResetPdf`](https://sdk.pdf.net/docs/v1/reference/useResetPdf), then `loadPdf()`                                                              | The rest of your UI stays mounted.                                                                       |
  | `instance.totalPageCount`                                                                     | [`usePageCount`](https://sdk.pdf.net/docs/v1/reference/usePageCount)                                                                                                |                                                                                                          |
  | `viewState.currentPageIndex`, `viewState.currentPageIndex.change`                             | [`useCurrentPageId`](https://sdk.pdf.net/docs/v1/reference/useCurrentPageId)                                                                                        | Re-renders with the id of the page in the middle of the viewport.                                        |
  | `setViewState()` with `currentPageIndex`, `goToNextPage()`, `goToPreviousPage()`              | `scrollToPage(pageId)` from [`useScrollToPage`](https://sdk.pdf.net/docs/v1/reference/useScrollToPage)                                                              |                                                                                                          |
  | `instance.pageInfoForIndex(pageIndex)`                                                        | The entries of [`usePages`](https://sdk.pdf.net/docs/v1/reference/usePages)                                                                                         | `id`, `width`, `height`, `rotation`.                                                                     |
  | `jumpToRect()`, `jumpAndZoomToRect()`                                                         | **No equivalent**                                                                                                                                | `scrollToBlock` scrolls to a block, not to a rectangle.                                                  |
  | `applyOperations()` with `addPage`                                                            | `createPage(width, height, insertAfterPageId?)` from [`usePageCreate`](https://sdk.pdf.net/docs/v1/reference/usePageCreate)                                         | Inserts after a page, or appends.                                                                        |
  | `removePages`, `keepPages`                                                                    | `deletePages(pageIdOrIds)` from [`usePageDelete`](https://sdk.pdf.net/docs/v1/reference/usePageDelete)                                                              | For `keepPages`, delete the other ids.                                                                   |
  | `movePages`                                                                                   | `moveBefore` or `moveAfter` from [`usePageMove`](https://sdk.pdf.net/docs/v1/reference/usePageMove)                                                                 |                                                                                                          |
  | `rotatePages`                                                                                 | `setPageRotation(pageIdOrIds, rotation)` from [`useSetPageRotation`](https://sdk.pdf.net/docs/v1/reference/useSetPageRotation)                                      | Absolute degrees, not an added rotation.                                                                 |
  | `duplicatePages`                                                                              | `duplicatePage(sourcePageId, insertAfterPageId?)` from [`usePageDuplicate`](https://sdk.pdf.net/docs/v1/reference/usePageDuplicate)                                 | One page per call.                                                                                       |
  | `document.change` event                                                                       | Track the page commands where you call them; [`usePages`](https://sdk.pdf.net/docs/v1/reference/usePages) re-renders on page changes                                | Page operations emit no events.                                                                          |
  | Extracting pages to a new file                                                                | `prepareDocument({ pageIds })`                                                                                                                   | Saves only the listed pages, in the listed order.                                                        |
  | `importDocument`, `cropPages`, `addPageMargins`, `setPageLabel`                               | **No equivalent**                                                                                                                                |                                                                                                          |
  | `viewState.zoom` as a number, `zoomIn()`, `zoomOut()`, `instance.currentZoomLevel`            | [`useViewportScale`](https://sdk.pdf.net/docs/v1/reference/useViewportScale)                                                                                        |                                                                                                          |
  | `ZoomMode.FIT_TO_WIDTH`                                                                       | [`useViewportAutoFitScale`](https://sdk.pdf.net/docs/v1/reference/useViewportAutoFitScale), [`useViewportFitScale`](https://sdk.pdf.net/docs/v1/reference/useViewportFitScale)         | A one-time fit, not a mode that stays applied.                                                           |
  | `ZoomMode.FIT_TO_VIEWPORT`                                                                    | `Math.min(fitWidthScale, fitHeightScale)`                                                                                                        | See [Zoom and fit modes](https://sdk.pdf.net/docs/v1/migration/from-nutrient#zoom-and-fit-modes).                                                           |
  | `ZoomMode.AUTO`                                                                               | **No equivalent**                                                                                                                                |                                                                                                          |
  | `viewState.zoom.change` event                                                                 | `scale` from `useViewportScale` in a `useEffect`                                                                                                 | `zoomGestureEnd` from `useViewportEvent` reports pinch and wheel zoom.                                   |
  | `layoutMode`, `scrollMode`, `pagesRotation`                                                   | **No equivalent**                                                                                                                                | `Viewport` has one scrollable layout.                                                                    |
  | `SidebarMode.THUMBNAILS`, `renderPageAsImageURL()`                                            | [`PageCanvas`](https://sdk.pdf.net/docs/v1/reference/PageCanvas)                                                                                                    |                                                                                                          |
  | `instance.getFormFields()`                                                                    | [`useWidgets`](https://sdk.pdf.net/docs/v1/reference/useWidgets), [`useWidgetsGetter`](https://sdk.pdf.net/docs/v1/reference/useWidgetsGetter)                                         | Widgets, not fields.                                                                                     |
  | `getFormFieldValues()`, `setFormFieldValues()`                                                | [`useWidgetValue`](https://sdk.pdf.net/docs/v1/reference/useWidgetValue)                                                                                            | One widget per hook call.                                                                                |
  | `formFieldValues.update` event                                                                | `widgetInput` from [`useViewportEvent`](https://sdk.pdf.net/docs/v1/reference/useViewportEvent); `useWidgetValue` re-renders                                        | `widgetInput` reports values the user changes in the viewport.                                           |
  | `WidgetAnnotation` and a `FormField` passed to `instance.create()`                            | `createWidgets(props)` from [`useWidgetsCreate`](https://sdk.pdf.net/docs/v1/reference/useWidgetsCreate)                                                            |                                                                                                          |
  | `InteractionMode.TEXT_WIDGET` and the other widget modes, `FORM_CREATOR`                      | The `{ type: 'addWidget', widgetProps }` tool                                                                                                    |                                                                                                          |
  | `setViewState()` with `interactionMode`                                                       | [`useSetInteractionMode`](https://sdk.pdf.net/docs/v1/reference/useSetInteractionMode)                                                                              | See [Editing and interaction tools](https://sdk.pdf.net/docs/v1/migration/from-nutrient#editing-and-interaction-tools).                                     |
  | `ViewState#readOnly`                                                                          | `PDF_PRINTER` from [`CONTENT_POLICY_PRESET`](https://sdk.pdf.net/docs/v1/reference/CONTENT_POLICY_PRESET)                                                           | `PDF_VIEWER` keeps widgets fillable.                                                                     |
  | `InteractionMode.CONTENT_EDITOR`, `beginContentEditingSession()`                              | The `PDF_EDITOR` content policy, [`useProgrammaticEditor`](https://sdk.pdf.net/docs/v1/reference/useProgrammaticEditor)                                             | See [Content editing](https://sdk.pdf.net/docs/v1/migration/from-nutrient#content-editing).                                                                 |
  | `instance.create()` for ink and image annotations                                             | [`useInkAnnotationCreate`](https://sdk.pdf.net/docs/v1/reference/useInkAnnotationCreate), [`useStampAnnotationCreate`](https://sdk.pdf.net/docs/v1/reference/useStampAnnotationCreate) | See [Annotations](https://sdk.pdf.net/docs/v1/migration/from-nutrient#annotations).                                                                         |
  | `instance.create()` for other annotation types                                                | **No equivalent**                                                                                                                                | See [Annotations](https://sdk.pdf.net/docs/v1/migration/from-nutrient#annotations).                                                                         |
  | `instance.update()` for annotations                                                           | [`useSelectionAnnotationProps`](https://sdk.pdf.net/docs/v1/reference/useSelectionAnnotationProps)                                                                  | Border color and width of selected annotations only.                                                     |
  | `annotations.change` and the other annotation events                                          | [`useDocumentRevision`](https://sdk.pdf.net/docs/v1/reference/useDocumentRevision) in a `useEffect`                                                                 | No annotation-specific events.                                                                           |
  | `exportInstantJSON()`, `exportXFDF()`, the `instantJSON` and `XFDF` options                   | **No equivalent**                                                                                                                                | Save the PDF instead.                                                                                    |
  | `enableHistory`, `instance.history.undo()`, `redo()`, `canUndo()`, `canRedo()`                | [`useUndoRedo`](https://sdk.pdf.net/docs/v1/reference/useUndoRedo)                                                                                                  | No switch to turn it on.                                                                                 |
  | `instance.history.clear()`                                                                    | **No equivalent**                                                                                                                                |                                                                                                          |
  | `hasUnsavedChanges()`, `document.saveStateChange` event                                       | [`useDocumentRevision`](https://sdk.pdf.net/docs/v1/reference/useDocumentRevision), [`useDocumentRevisionGetter`](https://sdk.pdf.net/docs/v1/reference/useDocumentRevisionGetter)     | Compare with the revision you last saved.                                                                |
  | `autoSaveMode`, `instance.save()`                                                             | Nothing to call                                                                                                                                  | `prepareDocument()` includes every change.                                                               |
  | `instance.exportPDF()`                                                                        | `prepareDocument()` from [`useDocumentSaving`](https://sdk.pdf.net/docs/v1/reference/useDocumentSaving)                                                             | Resolves with a `Uint8Array`, not an `ArrayBuffer`.                                                      |
  | The `export-pdf` toolbar item                                                                 | `downloadDocument()`                                                                                                                             | No file name option.                                                                                     |
  | `exportPDF({ flatten })`, `exportPDFWithOperations()`                                         | **No equivalent**                                                                                                                                |                                                                                                          |
  | `NutrientViewer.Error` and the rejected `load()` promise                                      | Typed error classes from the call that failed                                                                                                    | See [Errors and logging](https://sdk.pdf.net/docs/v1/migration/from-nutrient#errors-and-logging).                                                           |
  | `logLevel: NutrientViewer.LogLevel.DEBUG`                                                     | [`logMessages`](https://sdk.pdf.net/docs/v1/reference/logMessages)                                                                                                  | The SDK never prints to the console.                                                                     |
  | `addEventListener()`, `removeEventListener()`                                                 | State hooks, and `useViewportEvent` for user interactions                                                                                        | Subscriptions follow the component lifecycle.                                                            |
  | `toolbarItems`, `setToolbarItems()`, `ui` slots, `theme`, `styleSheets`                       | Your own components and the `theme` configuration, see [Theming and customization](https://sdk.pdf.net/docs/v1/guides/theming-and-customization)                    |                                                                                                          |
  | `instance.search()`, `startUISearch()`                                                        | [`useDocumentSearch`](https://sdk.pdf.net/docs/v1/reference/useDocumentSearch)                                                                                      | Highlights the matches and scrolls to the active one; the search UI is yours.                            |
  | `instance.print()`                                                                            | **No equivalent**                                                                                                                                | `PageCanvas` can render pages for your own print layout, as loaded from the file, without session edits. |
  | `transformClientToPageSpace()`, `transformPageToClientSpace()`                                | **No equivalent**                                                                                                                                | See [Coordinates](https://sdk.pdf.net/docs/v1/migration/from-nutrient#coordinates).                                                                         |
  | `instant: true` with Document Engine                                                          | **Coming soon**                                                                                                                                  | See [Collaboration](https://sdk.pdf.net/docs/v1/migration/from-nutrient#collaboration).                                                                     |
</details>

## Install and set up [#install-and-set-up]

### Before: Nutrient Web SDK [#before-nutrient-web-sdk]

Nutrient Web SDK installs as `@nutrient-sdk/viewer` and mounts into a `ref` from an effect. The runtime assets (WebAssembly, fonts, icons) load from the Nutrient CDN with `useCDN: true`, or from your server with `baseUrl` after you extract the assets archive into your public folder:

```sh
npm install @nutrient-sdk/viewer
```

```jsx
import { useEffect, useRef } from 'react'
import NutrientViewer from '@nutrient-sdk/viewer'

function App() {
  const containerRef = useRef(null)

  useEffect(() => {
    const container = containerRef.current

    // Make sure only one viewer is mounted in the container.
    NutrientViewer.unload(container)

    NutrientViewer.load({
      container,
      // Or baseUrl, pointing at the self-hosted assets.
      useCDN: true,
      licenseKey: 'YOUR_LICENSE_KEY',
      document: '/files/contract.pdf',
      initialViewState: new NutrientViewer.ViewState({ readOnly: true }),
    }).then((instance) => {
      // instance.viewState, instance.setViewState(), instance.addEventListener(), ...
    })

    return () => {
      NutrientViewer.unload(container)
    }
  }, [])

  // The container needs an explicit width and height before load() runs.
  return <div ref={containerRef} style={{ height: '100vh', width: '100vw' }} />
}
```

Under React Strict Mode the effect runs twice in development; unloading the container before each `load` keeps one viewer mounted. The `signal` version in [Cleanup and lifecycle](https://sdk.pdf.net/docs/v1/migration/from-nutrient#cleanup-and-lifecycle) also cancels a load still in progress.

### After: @sdk-pdf/react [#after-sdk-pdfreact]

Add the `@sdk-pdf` scope to an `.npmrc` next to your `package.json`, keep the token in an environment variable, and install; [Getting started](https://sdk.pdf.net/docs/v1/guides/getting-started#installation) has the details:

```ini
@sdk-pdf:registry=https://registry.sdk.pdf.net
//registry.sdk.pdf.net/:_authToken=${PDF_NET_REGISTRY_TOKEN}
```

```sh
npm install @sdk-pdf/react
```

Set up the project as [Getting started](https://sdk.pdf.net/docs/v1/guides/getting-started#project-setup) describes: import the stylesheet, render the SDK on the client only, alias `canvas` in the bundler, and turn off React Strict Mode.

Mount [`PdfProviders`](https://sdk.pdf.net/docs/v1/reference/PdfProviders) in your tree. Its props must keep their identity between renders, so define static values outside the component and memoize the configuration:

```tsx
import { useMemo } from 'react'
import { CONTENT_POLICY_PRESET, InteractionTool, PdfProviders } from '@sdk-pdf/react'
import '@sdk-pdf/react/style.css'

// Like interactionMode null in Nutrient: select text and scroll.
const textSelectTool: InteractionTool = { type: 'textSelect' }

export function App({ readOnly }: { readOnly: boolean }) {
  // The PDF application re-renders whenever a PdfProviders prop changes identity.
  const config = useMemo(
    () => ({
      // Nutrient's default viewportPadding is 20 pixels on every side.
      theme: { viewportPadding: 20 },
      initialInteractionTool: textSelectTool,
      // Applied when the viewport first mounts; switch later with useSetInteractionMode.
      initialContentPolicy: readOnly
        ? CONTENT_POLICY_PRESET.PDF_PRINTER
        : CONTENT_POLICY_PRESET.PDF_VIEWER,
    }),
    [readOnly]
  )

  return (
    <PdfProviders {...config}>
      <DocumentLoader url="/files/contract.pdf" />
    </PdfProviders>
  )
}
```

### What to know [#what-to-know]

* **No assets, no license key in code.** The install is the registry scope and the package. There is no CDN setting, no assets archive to serve, no `baseUrl` or `licenseKey` to pass, and no `preloadWorker()` call to warm up.
* **No mount effect.** The SDK renders as components, so the `useRef` and `useEffect` mount, the `unload` cleanup, and sharing the instance through React context go away: any component under `PdfProviders` reaches the document through hooks. `Viewport` still fills its parent, so the parent needs an explicit size, as the Nutrient container did.
* **Pick the content policy deliberately.** `PdfProviders` defaults to the `select` tool and `PDF_EDITOR`, where text, annotations, links, and widgets are all editable. A Nutrient viewer opens with no interaction mode (text selection and scrolling) and, unless `readOnly` is set, with its annotation tools in the toolbar. Start a reader with `textSelect` and `PDF_VIEWER`, as above, and switch to `PDF_EDITOR` when users edit, as in [Editing and interaction tools](https://sdk.pdf.net/docs/v1/migration/from-nutrient#editing-and-interaction-tools).
* **readOnly maps to PDF_PRINTER.** Nutrient's read-only mode also stops form filling, which matches `PDF_PRINTER` (everything read-only, links clickable). `PDF_VIEWER` keeps widgets fillable.

## Load and reset a document [#load-and-reset-a-document]

### Before: Nutrient Web SDK [#before-nutrient-web-sdk-1]

The `document` option takes a URL or an `ArrayBuffer`. A wrong or missing password calls `onPasswordRequired` before the built-in password prompt, and `maxPasswordRetries` limits the prompt. To switch documents, you unload the viewer and load again into the same container:

```js
const container = document.querySelector('#viewer')
let instance

async function openDocument(arrayBuffer) {
  // Remember the page, then unload the current viewer before loading the next document.
  const lastPageIndex = instance?.viewState.currentPageIndex ?? 0
  if (instance) NutrientViewer.unload(container)

  instance = await NutrientViewer.load({
    container,
    useCDN: true,
    licenseKey: 'YOUR_LICENSE_KEY',
    // load() takes ownership of the buffer and detaches it: pass a copy to reuse the
    // bytes.
    document: arrayBuffer.slice(0),
    // Resolve with a password to try it, or with null to show the built-in prompt.
    onPasswordRequired: async () => lookUpPassword(),
  })

  // UI state is per instance: restore the page yourself, within the new page count.
  const pageIndex = Math.min(lastPageIndex, instance.totalPageCount - 1)
  instance.setViewState((viewState) => viewState.set('currentPageIndex', pageIndex))
}
```

### After: @sdk-pdf/react [#after-sdk-pdfreact-1]

`loadPdf` from [`useLoadPdf`](https://sdk.pdf.net/docs/v1/reference/useLoadPdf) takes the file bytes, and the load state decides what to render. A URL becomes a `fetch`:

```tsx
import { useRef, useState } from 'react'
import { InvalidPasswordError, useLoadPdf, useResetPdf } from '@sdk-pdf/react'

function DocumentLoader({ url }: { url: string }) {
  const [loadState, loadPdf] = useLoadPdf()
  const bytesRef = useRef<Uint8Array | null>(null)
  const [isPasswordNeeded, setIsPasswordNeeded] = useState(false)
  const [loadError, setLoadError] = useState<unknown>()

  async function open(password?: string) {
    setLoadError(undefined)
    try {
      if (!bytesRef.current) {
        // Replaces document: url. Headers, credentials, and an AbortSignal go on this
        // fetch.
        const response = await fetch(url)
        if (!response.ok) throw new Error(`Request failed: ${response.status}`)
        bytesRef.current = new Uint8Array(await response.arrayBuffer())
      }

      // loadPdf reads from the buffer it receives, so keep the original for a retry and
      // pass a copy.
      await loadPdf(bytesRef.current.slice(), { password })
      setIsPasswordNeeded(false)
    } catch (error) {
      // Replaces onPasswordRequired and the built-in password prompt.
      if (error instanceof InvalidPasswordError) setIsPasswordNeeded(true)
      else setLoadError(error)
    }
  }

  if (isPasswordNeeded)
    return <PasswordPrompt onSubmit={(password) => void open(password)} />
  if (loadError) return <p role="alert">The document can't be opened.</p>
  if (loadState.status === 'initial')
    return <button onClick={() => void open()}>Open</button>
  if (loadState.status === 'loading') return <p>Loading the document…</p>

  return <PdfApplication />
}

function CloseDocumentButton() {
  const resetPdf = useResetPdf()

  // Replaces NutrientViewer.unload(container): the load state returns to 'initial'.
  return <button onClick={() => resetPdf()}>Close</button>
}
```

### What to know [#what-to-know-1]

* **Bytes, not URLs.** `loadPdf` takes a `Uint8Array`. The network options of `load()` (`httpHeaders`, linearized loading, `tempStorage`) move to your own `fetch` or have no equivalent, and `loadPdf` takes no `signal`: abort the `fetch` instead.
* **The same buffer habit.** Nutrient Web SDK takes ownership of the `ArrayBuffer` you pass and detaches it. `loadPdf` reads from the given buffer for the document's whole lifetime, and anything else touching it can detach it. Either way, pass a copy when you keep the bytes: `loadPdf(data.slice())`.
* **Passwords are a rejection, not a callback.** Without a password, the empty password is tried, as the PDF specification prescribes; a missing or wrong password rejects with `InvalidPasswordError`. There is no built-in prompt and no `maxPasswordRetries`: render your own prompt and count attempts yourself.
* **Switching documents keeps the UI.** Instead of unloading and mounting a new viewer, call `resetPdf()` and then `loadPdf()`; `PdfProviders` and your components stay mounted. Restoring the page or zoom after a switch is still yours: call `scrollToPage` and `setScale` once the new document is ready.
* **PDF only.** `useLoadPdf` loads PDF documents; Office documents and images have no equivalent, and there is no `instantJSON` or `XFDF` to apply on load.
* **Closing invalidates ids.** After `resetPdf()`, page and block ids, widget snapshots, and document revisions from the old document are no longer valid.

## Display the document [#display-the-document]

### Before: Nutrient Web SDK [#before-nutrient-web-sdk-2]

The viewer renders the document with its toolbar and sidebars, and integrations trim and theme it through the configuration. Custom toolbar buttons are items with `type: 'custom'` and an `onPress` callback; the `ui` slots replace or hide built-in components, down to `preset: 'minimal'`, which leaves only the pages:

```js
NutrientViewer.load({
  container,
  useCDN: true,
  document: '/files/contract.pdf',
  toolbarItems: [
    ...NutrientViewer.defaultToolbarItems.filter((item) => item.type !== 'print'),
    { type: 'custom', id: 'share', title: 'Share', onPress: () => openShareDialog() },
  ],
  toolbarPlacement: NutrientViewer.ToolbarPlacement.TOP,
  theme: NutrientViewer.Theme.DARK,
  // The viewer renders in a shadow root, so your page CSS has to be passed in.
  styleSheets: ['/viewer-overrides.css'],
  initialViewState: new NutrientViewer.ViewState({
    sidebarMode: NutrientViewer.SidebarMode.THUMBNAILS,
    zoom: NutrientViewer.ZoomMode.FIT_TO_WIDTH,
  }),
})
```

### After: @sdk-pdf/react [#after-sdk-pdfreact-2]

[`Viewport`](https://sdk.pdf.net/docs/v1/reference/Viewport) renders the pages, and your components render everything else. [Building a PDF application](https://sdk.pdf.net/docs/v1/guides/building-a-pdf-application) maps the usual parts to hooks:

```tsx
import { Config, useViewportAutoFitScale, Viewport } from '@sdk-pdf/react'

// A fuller theme than the one in App: pass it as PdfProviders' theme, defined outside
// components.
export const theme: Config['theme'] = {
  viewportPadding: [20, 20],
  classNames: { scrollViewport: 'viewer', page: 'viewer-page' },
  styles: {
    scrollViewport: { '--selection-color': 'rebeccapurple' } as React.CSSProperties,
  },
}

function PdfApplication() {
  // Like zoom: ZoomMode.FIT_TO_WIDTH in initialViewState, applied once per loaded
  // document.
  useViewportAutoFitScale('width')

  return (
    <div style={{ display: 'flex', flexDirection: 'column', height: '100vh' }}>
      {/* Your main toolbar: the pager, zoom, tool, history, and save controls from this
         guide. */}
      <MainToolbar />
      <div style={{ display: 'flex', flex: 1, minHeight: 0 }}>
        {/* Like SidebarMode.THUMBNAILS: render it conditionally to toggle it. */}
        <ThumbnailsSidebar />
        {/* The viewport fills its parent, so the parent needs an explicit size. */}
        <div style={{ flex: 1, minWidth: 0 }}>
          <Viewport />
        </div>
      </div>
    </div>
  )
}
```

### What to know [#what-to-know-2]

* **There is nothing to hide.** `toolbarItems`, `setToolbarItems()`, `showToolbar`, `toolbarPlacement`, `sidebarMode`, and the `ui` slots have no counterpart. A custom toolbar item becomes a plain `button`, and a sidebar mode becomes a component you render or don't. Selection, text editing, form filling, links, and zoom gestures work inside `Viewport` and are limited by the content policy; every control around it exists only if you build it.
* **Theming is CSS you own.** `theme.classNames` and `theme.styles` target the SDK's elements (`scrollViewport`, `page`, `pageCanvas`, `resizable`, `widgetOverlay`, and more), and CSS variables such as `--selection-color` and `--widget-overlay-background` tune the selection and widget visuals; [Theming and customization](https://sdk.pdf.net/docs/v1/guides/theming-and-customization) lists them. There are no built-in light, dark, or high-contrast themes, and nothing like `styleSheets` to inject.
* **Render only when ready.** `Viewport` throws `DocumentNotLoadedError` when no document is loaded.
* **One layout.** `layoutMode` (`SINGLE`, `DOUBLE`, `AUTO`), `scrollMode` (`CONTINUOUS`, `PER_SPREAD`, `DISABLED`), `keepFirstSpreadAsSinglePage`, and `spreadSpacing` have no equivalent: `Viewport` shows the pages in one scrollable area. `theme.viewportPadding` replaces `ViewState#viewportPadding`.

## Pages and navigation [#pages-and-navigation]

### Before: Nutrient Web SDK [#before-nutrient-web-sdk-3]

The current page is a zero-based index in the view state. You move it with `setViewState()`, and changes arrive as `viewState.currentPageIndex.change`:

```js
pageCountLabel.textContent = instance.totalPageCount

instance.addEventListener('viewState.currentPageIndex.change', (pageIndex) => {
  pageInput.value = pageIndex + 1
})

nextButton.onclick = () => instance.setViewState((viewState) => viewState.goToNextPage())

// An index out of range throws a NutrientViewer.Error.
pageInput.onchange = () =>
  instance.setViewState((viewState) =>
    viewState.set('currentPageIndex', Number(pageInput.value) - 1)
  )
```

### After: @sdk-pdf/react [#after-sdk-pdfreact-3]

[`usePages`](https://sdk.pdf.net/docs/v1/reference/usePages) returns the pages in document order, so a page number is an index in that list plus one. [`useScrollToPage`](https://sdk.pdf.net/docs/v1/reference/useScrollToPage) scrolls to a page id, and [`useCurrentPageId`](https://sdk.pdf.net/docs/v1/reference/useCurrentPageId) re-renders with the page in the middle of the viewport:

```tsx
import { useCurrentPageId, usePages, useScrollToPage } from '@sdk-pdf/react'

function Pager() {
  const pages = usePages()
  // Like viewState.currentPageIndex, with a page id instead of an index.
  const currentPageId = useCurrentPageId()
  const scrollToPage = useScrollToPage()

  // There is no current page until the viewport shows one, so start on the first page.
  const currentIndex = Math.max(
    0,
    pages.findIndex((page) => page.id === currentPageId)
  )

  function goToPageNumber(pageNumber: number) {
    // Clamp the number instead of throwing on an index out of range.
    const page = pages[Math.min(Math.max(pageNumber, 1), pages.length) - 1]
    if (page) scrollToPage(page.id, { behavior: 'smooth' })
  }

  return (
    <div role="toolbar">
      <button disabled={currentIndex <= 0} onClick={() => goToPageNumber(currentIndex)}>
        Previous
      </button>
      <label>
        Page
        <input
          type="number"
          min={1}
          max={pages.length}
          value={currentIndex + 1}
          onChange={(event) => goToPageNumber(event.currentTarget.valueAsNumber)}
        />
        of {pages.length}
      </label>
      <button
        disabled={currentIndex >= pages.length - 1}
        onClick={() => goToPageNumber(currentIndex + 2)}
      >
        Next
      </button>
    </div>
  )
}
```

When you need a page's index inside a handler, such as for analytics, `getPageIndex(pageId)` from [`usePageIndexGetter`](https://sdk.pdf.net/docs/v1/reference/usePageIndexGetter) resolves an id to its zero-based index without re-rendering on order changes. For a page count alone, [`usePageCount`](https://sdk.pdf.net/docs/v1/reference/usePageCount) replaces `instance.totalPageCount`.

### What to know [#what-to-know-3]

* **Ids survive reordering; indexes don't.** An id keeps pointing at the same page while pages are added, removed, and rearranged by the user, where an index would drift to another page. Ids are session-scoped: persist page numbers, not ids, and resolve them once the document is ready.
* **Stale ids throw.** `scrollToPage` throws `InvalidInputError` for an unknown or deleted id, as `setViewState()` throws for an index out of range; `usePageIndexGetter` returns `undefined` instead.
* **Page sizes.** `pageInfoForIndex(pageIndex).width` and `height` become the `width` and `height` of the `usePages()` entries, in user-space units with the rotation applied, next to the page's `rotation`.
* **Rotation edits the document.** `pagesRotation`, `rotateLeft()`, and `rotateRight()` turn the view only and aren't saved. `setPageRotation` from [`useSetPageRotation`](https://sdk.pdf.net/docs/v1/reference/useSetPageRotation) rotates pages in the document as an undoable edit; there is no view-only rotation.
* **Scrolling to a spot.** `jumpToRect()` and `jumpAndZoomToRect()` have no equivalent. `scrollToBlock(blockId, options?)` from [`useScrollToBlock`](https://sdk.pdf.net/docs/v1/reference/useScrollToBlock) scrolls to a block, such as a form widget or a paragraph. Page labels (`PageInfo#label`) have no equivalent either.

## Page manipulation [#page-manipulation]

### Before: Nutrient Web SDK [#before-nutrient-web-sdk-4]

Page changes are document operations by index, applied with `applyOperations()` when the license includes the Document Editor component. Each operation in the array works on the result of the previous one, and the Document Editor UI (`InteractionMode.DOCUMENT_EDITOR`) offers the same actions with page thumbnails:

```js
await instance.applyOperations([
  {
    type: 'addPage',
    afterPageIndex: 1,
    pageWidth: 612,
    pageHeight: 792,
    rotateBy: 0,
    backgroundColor: new NutrientViewer.Color({ r: 255, g: 255, b: 255 }),
  },
  { type: 'duplicatePages', pageIndexes: [0] },
  { type: 'movePages', pageIndexes: [5], beforePageIndex: 0 },
  // Adds to the page's current rotation.
  { type: 'rotatePages', pageIndexes: [0], rotateBy: 90 },
  { type: 'removePages', pageIndexes: [4] },
])

instance.addEventListener('document.change', (operations) => {
  trackPageOperations(operations)
})
```

### After: @sdk-pdf/react [#after-sdk-pdfreact-4]

Each page operation is a hook that returns a command taking page ids: [`usePageCreate`](https://sdk.pdf.net/docs/v1/reference/usePageCreate), [`usePageDuplicate`](https://sdk.pdf.net/docs/v1/reference/usePageDuplicate), [`usePageMove`](https://sdk.pdf.net/docs/v1/reference/usePageMove), [`useSetPageRotation`](https://sdk.pdf.net/docs/v1/reference/useSetPageRotation), and [`usePageDelete`](https://sdk.pdf.net/docs/v1/reference/usePageDelete). The commands apply one at a time, and the ids don't shift as the order changes:

```tsx
import {
  usePageCreate,
  usePageDelete,
  usePageDuplicate,
  usePageMove,
  usePages,
  useSetPageRotation,
} from '@sdk-pdf/react'

function PageActions({ pageId }: { pageId: string }) {
  const pages = usePages()
  const createPage = usePageCreate()
  const duplicatePage = usePageDuplicate()
  const { moveAfter } = usePageMove()
  const setPageRotation = useSetPageRotation()
  const deletePages = usePageDelete()

  // Like the document.change listener: page commands emit no events, so track them where
  // you call them.
  function deletePage() {
    deletePages(pageId)
    track('pages-deleted', 1)
  }

  const index = pages.findIndex((page) => page.id === pageId)
  const page = pages[index]
  if (!page) return null

  return (
    <div role="toolbar">
      {/* Like addPage with afterPageIndex. A4 is roughly 595 × 842 user-space units. */}
      <button onClick={() => createPage(595, 842, pageId)}>Insert page after</button>
      {/* Like duplicatePages: the copy goes right after the source page. */}
      <button onClick={() => void duplicatePage(pageId)}>Duplicate</button>
      <button
        disabled={index === pages.length - 1}
        onClick={() => moveAfter(pageId, pages[index + 1]?.id)}
      >
        Move down
      </button>
      {/* rotatePages adds to the current angle; setPageRotation sets an absolute one. */}
      <button onClick={() => setPageRotation(pageId, page.rotation + 90)}>
        Rotate clockwise
      </button>
      <button disabled={pages.length <= 1} onClick={deletePage}>
        Delete
      </button>
    </div>
  )
}
```

### What to know [#what-to-know-4]

| Operation | Nutrient Web SDK                                                   | @sdk-pdf/react                                                                                                  |
| --------- | ------------------------------------------------------------------ | --------------------------------------------------------------------------------------------------------------- |
| Insert    | `addPage` before or after an index, with size, color, and rotation | After `insertAfterPageId`, or appended; `width` and `height` in user-space units (1/72 of an inch), no defaults |
| Duplicate | `duplicatePages`, each copy after its original                     | `duplicatePage` is asynchronous, one page per call, after the source by default                                 |
| Move      | `movePages` before or after an index                               | `moveBefore(ids, beforePageId?)` (start when omitted) or `moveAfter(ids, afterPageId?)` (end when omitted)      |
| Rotate    | `rotatePages` adds `rotateBy` to the current rotation              | Sets an absolute angle; `-90` and `450` are normalized; angles off a quarter turn throw `InvalidInputError`     |
| Remove    | `removePages` by index, or `keepPages` for the complement          | `deletePages` by id; keeping a subset means deleting the other ids                                              |

* **Undoable.** Nutrient's History API tracks annotations, and the Document Editor UI has its own undo and redo for the changes you then save or cancel. Here, deleting, moving, or rotating several pages in one call is a single undo step of the document history, and an undo can restore a deleted page.
* **No events.** `document.change` delivers the operations array; here, page commands emit no events. Track them where you call them, and read the current pages from `usePages`, which re-renders on every page change, undo and redo included.
* **Extracting pages.** `prepareDocument({ pageIds })` from [`useDocumentSaving`](https://sdk.pdf.net/docs/v1/reference/useDocumentSaving) saves only the listed pages, in the listed order, without changing the loaded document.
* **No import, cropping, or margins.** `importDocument`, `cropPages`, `addPageMargins`, `setPageLabel`, `flattenAnnotations`, and `exportPDFWithOperations()` have no equivalent: the page hooks change the loaded document.

## Zoom and fit modes [#zoom-and-fit-modes]

### Before: Nutrient Web SDK [#before-nutrient-web-sdk-5]

`ViewState#zoom` is a number, a `ZoomMode`, or a `ZoomConfiguration` (the default, with `ZoomMode.AUTO`). A zoom mode stays applied whenever the viewport resizes: `FIT_TO_WIDTH` fits the width of the broadest page, and `FIT_TO_VIEWPORT` fits the current page completely. The toolbar zoom buttons step by `zoomStep` (1.25 by default); `zoomIn()` and `zoomOut()` step by 25%. `instance.currentZoomLevel` is always the resulting number:

```js
NutrientViewer.load({
  // ...
  initialViewState: new NutrientViewer.ViewState({
    zoom: NutrientViewer.ZoomMode.FIT_TO_WIDTH,
  }),
})

zoomInButton.onclick = () => instance.setViewState((viewState) => viewState.zoomIn())
fitPageButton.onclick = () =>
  instance.setViewState((viewState) =>
    viewState.set('zoom', NutrientViewer.ZoomMode.FIT_TO_VIEWPORT)
  )

// The zoom can be a mode or a configuration: persist the resulting number.
instance.addEventListener('viewState.zoom.change', () =>
  persistZoom(instance.currentZoomLevel)
)
```

### After: @sdk-pdf/react [#after-sdk-pdfreact-5]

[`useViewportScale`](https://sdk.pdf.net/docs/v1/reference/useViewportScale) reads and sets the scale, [`useViewportFitScale`](https://sdk.pdf.net/docs/v1/reference/useViewportFitScale) calculates fit scales on demand, and `useViewportAutoFitScale` (in `PdfApplication` above) fits once per loaded document:

```tsx
import { useEffect } from 'react'
import { useViewportFitScale, useViewportScale } from '@sdk-pdf/react'

// Like zoomIn() and zoomOut(), and the default ViewState#zoomStep.
const ZOOM_STEP = 1.25

function ZoomControls() {
  const [scale, setScale] = useViewportScale()
  const getFitScale = useViewportFitScale()

  // Like viewState.zoom.change: every scale change, from controls, gestures, or an auto
  // fit.
  useEffect(() => persistZoom(scale), [scale])

  function fitPage() {
    // Call the getter in the handler: the result reflects the viewport size at that
    // moment.
    const { fitWidthScale, fitHeightScale } = getFitScale()
    setScale(Math.min(fitWidthScale, fitHeightScale))
  }

  return (
    <div role="toolbar">
      <button onClick={() => setScale(scale / ZOOM_STEP)}>−</button>
      <span>{Math.round(scale * 100)}%</span>
      <button onClick={() => setScale(scale * ZOOM_STEP)}>+</button>
      <button onClick={() => setScale(getFitScale().fitWidthScale)}>Fit width</button>
      <button onClick={fitPage}>Fit page</button>
    </div>
  )
}
```

### Mapping the zoom modes [#mapping-the-zoom-modes]

| Nutrient Web SDK           | @sdk-pdf/react                                                                                                                      |
| -------------------------- | ----------------------------------------------------------------------------------------------------------------------------------- |
| `ZoomMode.FIT_TO_WIDTH`    | `useViewportAutoFitScale('width')` on load, or `setScale(getFitScale().fitWidthScale)`; both fit the pages into the viewport width. |
| `ZoomMode.FIT_TO_VIEWPORT` | `setScale(Math.min(fitWidthScale, fitHeightScale))`. The fit scales measure the widest and tallest pages, not the current page.     |
| `ZoomMode.AUTO`            | No equivalent.                                                                                                                      |
| A number, `zoomIn()`       | `setScale(scale)` and `setScale(scale * 1.25)`. Check stored zoom values against a test document before you reuse them.             |

### What to know [#what-to-know-5]

* **Fits are one-time calculations.** A Nutrient zoom mode is reapplied on every resize. `useViewportAutoFitScale` applies when a document loads and doesn't re-apply on viewport resizes or manual zooming; `useViewportFitScale` reflects the viewport size at the moment of the call.
* **What the fits account for.** Both fit scales include `viewportPadding`. Scale `1` is 100%: one user-space unit per CSS pixel. `setScale(scale, pivot?)` takes a positive finite number and keeps the document still at `pivot`, a point `{ x, y }` given as fractions of the viewport area (`0` the top or left edge, `1` the bottom or right edge), the center by default. The third item of `useViewportScale()`, `animateScale(scale, pivot?)`, animates the change and resolves when it ends.
* **The scale updates after a gesture.** During a pinch or wheel zoom, `scale` keeps its value until the gesture ends; `zoomGestureStart` and `zoomGestureEnd` from [`useViewportEvent`](https://sdk.pdf.net/docs/v1/reference/useViewportEvent) mark the gesture.
* **Gestures.** Pinch and wheel zoom are built in and tuned with `zoomGestures` on [`PdfProviders`](https://sdk.pdf.net/docs/v1/reference/PdfProviders) (`minScale` `0.5` and `maxScale` `4` by default, wheel speed). Marquee zoom has no equivalent.

## Thumbnails [#thumbnails]

### Before: Nutrient Web SDK [#before-nutrient-web-sdk-6]

The built-in thumbnails sidebar is a sidebar mode; custom thumbnails come from `renderPageAsImageURL`, which renders a page at a width or height up to 5000 pixels and resolves with a Blob URL to revoke later:

```js
instance.setViewState((viewState) =>
  viewState.set('sidebarMode', NutrientViewer.SidebarMode.THUMBNAILS)
)

const src = await instance.renderPageAsImageURL(
  { width: 200 * window.devicePixelRatio },
  pageIndex
)
thumbnailImage.src = src
// Later, once the image is gone:
URL.revokeObjectURL(src)
```

### After: @sdk-pdf/react [#after-sdk-pdfreact-6]

[`PageCanvas`](https://sdk.pdf.net/docs/v1/reference/PageCanvas) renders a page lazily at the canvas's CSS size; the sidebar, its selection, and its page actions are yours. A sidebar that selects a page and shows the `PageActions` from [Page manipulation](https://sdk.pdf.net/docs/v1/migration/from-nutrient#page-manipulation):

```tsx
import { useState } from 'react'
import { PageCanvas, usePages, useScrollToPage } from '@sdk-pdf/react'

function ThumbnailsSidebar() {
  const pages = usePages()
  const scrollToPage = useScrollToPage()
  const [selectedPageId, setSelectedPageId] = useState<string>()

  return (
    <nav style={{ width: 200, overflowY: 'auto' }}>
      {pages.map((page, index) => (
        <button
          key={page.id}
          aria-current={page.id === selectedPageId}
          onClick={() => {
            setSelectedPageId(page.id)
            scrollToPage(page.id)
          }}
        >
          {/* The render resolution follows the canvas CSS size: style .thumbnail to the
             width you need. */}
          <PageCanvas
            pageId={page.id}
            className="thumbnail"
            role="img"
            aria-label={`Page ${index + 1}`}
          />
        </button>
      ))}
      {selectedPageId && <PageActions pageId={selectedPageId} />}
    </nav>
  )
}
```

### What to know [#what-to-know-6]

* **No image URLs to manage.** There is no width to compute from `devicePixelRatio` and no Blob URL to revoke: the render resolution follows the canvas's displayed CSS size, or an explicit `scale` prop.
* **Lazy by default.** An off-screen canvas does no work until it becomes visible. In a virtualized list, pass `isVisible`.
* **Annotations included.** `PageCanvas` draws annotations, form widgets included, unless `hideAnnotations` is set.
* **Thumbnails that show session edits are coming soon.** `PageCanvas` draws the page as loaded from the file; the main view shows the edits. Rotation is followed live.
* **Render only when ready.** `PageCanvas` throws `DocumentNotLoadedError` when no document is loaded.

## Forms [#forms]

### Before: Nutrient Web SDK [#before-nutrient-web-sdk-7]

Values are a record keyed by the fully qualified field name: strings for text fields and radio groups, arrays of export values for checkboxes, `null` when empty. `getFormFieldValues()` covers the fields loaded so far, so integrations call `getFormFields()` first:

```js
await instance.getFormFields()
const values = instance.getFormFieldValues()
// { 'customer.name': 'Jane Doe', newsletter: ['Yes'], plan: 'pro' }

await instance.setFormFieldValues({ 'customer.name': 'Jane Doe', newsletter: ['Yes'] })

instance.addEventListener('formFieldValues.update', () => {
  autosave(instance.getFormFieldValues())
})
```

Creating a field takes a widget annotation and a form field that point at each other, and the Form Creator component in the license:

```js
const widget = new NutrientViewer.Annotations.WidgetAnnotation({
  id: NutrientViewer.generateInstantId(),
  pageIndex: 0,
  formFieldName: 'customer.name',
  // Page coordinates: the origin is the top-left corner of the page.
  boundingBox: new NutrientViewer.Geometry.Rect({
    left: 72,
    top: 72,
    width: 180,
    height: 24,
  }),
})
const formField = new NutrientViewer.FormFields.TextFormField({
  name: 'customer.name',
  annotationIds: NutrientViewer.Immutable.List([widget.id]),
})

await instance.create([widget, formField])
```

### After: @sdk-pdf/react [#after-sdk-pdfreact-7]

Forms are widget-centric: you work with the widgets on the pages, and each value is shaped like the value of the matching HTML form control, as [Working with forms](https://sdk.pdf.net/docs/v1/guides/working-with-forms) explains. The counterpart of one entry of the values record is a widget looked up by `fieldName` and bound with [`useWidgetValue`](https://sdk.pdf.net/docs/v1/reference/useWidgetValue):

```tsx
import {
  CheckboxWidget,
  TextWidget,
  useWidgets,
  useWidgetsCreate,
  useWidgetValue,
} from '@sdk-pdf/react'

// Reads and writes one field by name, for a text field or a single checkbox.
function FieldByName({ fieldName }: { fieldName: string }) {
  const { widgets } = useWidgets()
  // Same-named widgets show and synchronize one value, so binding one of them binds the
  // field.
  const widget = widgets.find((widget) => widget.fieldName === fieldName)
  if (!widget) return null

  switch (widget.widgetType) {
    case 'text':
      return <TextInput widget={widget} />
    case 'checkbox':
      return <CheckboxInput widget={widget} />
    default:
      return null
  }
}

function TextInput({ widget }: { widget: TextWidget }) {
  // Like a useState backed by the document: filling in the main view and undo update it.
  const [value, setValue] = useWidgetValue(widget)

  return (
    <label>
      {widget.fieldName}
      <input value={value} onChange={(event) => setValue(event.currentTarget.value)} />
    </label>
  )
}

function CheckboxInput({ widget }: { widget: CheckboxWidget }) {
  // A boolean, where getFormFieldValues reports an array of export values.
  const [isChecked, setIsChecked] = useWidgetValue(widget)

  return (
    <label>
      <input
        type="checkbox"
        checked={isChecked}
        onChange={(event) => setIsChecked(event.currentTarget.checked)}
      />
      {widget.fieldName}
    </label>
  )
}

function useAddNameField() {
  const createWidgets = useWidgetsCreate()

  return async (pageId: string, pageHeight: number) => {
    // One call creates the widget and its field: there is no separate form field record.
    const [widget] = await createWidgets({
      pageId,
      widgetType: 'text',
      // The Rect above, measured from the bottom-left corner of an unrotated page.
      x: 72,
      y: pageHeight - 72 - 24,
      width: 180,
      height: 24,
      fieldName: 'customer.name',
    })
    return widget
  }
}
```

### From form fields to widgets [#from-form-fields-to-widgets]

| Concept              | Nutrient Web SDK                                                               | @sdk-pdf/react                                                                                                                                                                                        |
| -------------------- | ------------------------------------------------------------------------------ | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| Lookup               | `getFormFields()`, a field's `annotationIds`                                   | `useWidgets()` (re-renders, `isLoaded` tells when the list is complete), or `await getWidgets()` from [`useWidgetsGetter`](https://sdk.pdf.net/docs/v1/reference/useWidgetsGetter) (loads every page with widgets first) |
| Values               | A record by field name: `string`, `string[]`, or `null`                        | `useWidgetValue(widget)`: `string` for `text` and `choice`, `boolean` for `checkbox` and `radio`, option indices for `multiChoice`                                                                    |
| Writing              | `setFormFieldValues({ name: value })`, several fields at once                  | The setter from `useWidgetValue`, one widget at a time; there is no setter by field name                                                                                                              |
| Radio groups         | One value for the group: the value of the selected option                      | One `radio` widget per option with the shared field name, each with its own checked state                                                                                                             |
| Change events        | `formFieldValues.update`                                                       | `widgetInput` from [`useViewportEvent`](https://sdk.pdf.net/docs/v1/reference/useViewportEvent) for values the user changes in the viewport; `useWidgetValue` re-renders on any change, undo and redo included           |
| Creation             | `WidgetAnnotation` and `FormField` with `generateInstantId()`, `create()`      | `createWidgets(props)` from [`useWidgetsCreate`](https://sdk.pdf.net/docs/v1/reference/useWidgetsCreate); required: `pageId`, `widgetType`, `x`, `y`, `width`, `height`; a unique `fieldName` is generated when omitted  |
| Interactive creation | `TEXT_WIDGET`, `CHECKBOX_WIDGET`, and the other widget modes, `formDesignMode` | The `{ type: 'addWidget', widgetProps }` interaction tool, used with `PDF_EDITOR`; the `select` tool moves and resizes widgets                                                                        |
| Deletion             | `instance.delete(formField)` also removes its widgets                          | [`useBlockDelete`](https://sdk.pdf.net/docs/v1/reference/useBlockDelete) with widget ids; same-named widgets act as one field, so delete them together                                                                   |
| Properties           | `instance.update()` on the widget or the field                                 | `setWidgetProps(ids, props, options?)` from [`useSetWidgetProps`](https://sdk.pdf.net/docs/v1/reference/useSetWidgetProps): `isReadOnly`, `borderColor`, `fillColor`, `fontSize`, `isMultiline`, `options`, and more     |
| Form data            | Instant JSON `formFieldValues`, XFDF                                           | No equivalent                                                                                                                                                                                         |

### What to know [#what-to-know-7]

* **Coordinates differ.** A Nutrient `boundingBox` is measured from the top-left corner of the page with y pointing down; widget positions here are user-space units from the bottom-left corner. On an unrotated page, `y` is the page height minus `top` minus `height`. Check placement on a test document before converting stored positions, especially for rotated or cropped pages.
* **Values are read and written per widget.** `useWidgetValue` binds one widget per hook call. To prefill a form from a stored record, render a bound component per field, like `FieldByName` above; for a radio group, bind each option's widget.
* **Form scripts.** Nutrient runs the JavaScript in a PDF by default in standalone mode, for validation, formatting, and calculations. The SDK recognizes the date formats Acrobat generates without executing scripts and exposes them as a text widget's `format` (`FormatType.DateTime`); see [`FormatType`](https://sdk.pdf.net/docs/v1/reference/FormatType). Fields driven by hand-written scripts read as plain text fields.
* **Read-only fields.** `isReadOnly` restricts filling the field, not authoring the widget; with `{ excludeFromHistory: true }`, an undo can't unlock a submitted form.
* **Signatures.** `signature` widgets carry a read-only `isCryptoSigned`; the SDK never signs.
* **The field being filled.** [`useFocusedWidgetId`](https://sdk.pdf.net/docs/v1/reference/useFocusedWidgetId) returns the widget whose control has the focus, where Nutrient integrations track the focused form field themselves.

## Editing and interaction tools [#editing-and-interaction-tools]

### Before: Nutrient Web SDK [#before-nutrient-web-sdk-8]

The interaction mode in the view state decides what a click or drag does, `null` being text selection and scrolling. Read-only mode hides the editing UI and stops form filling, and `isEditableAnnotation` narrows editing per annotation:

```js
// The free text annotation tool; null returns to text selection.
instance.setViewState((viewState) =>
  viewState.set('interactionMode', NutrientViewer.InteractionMode.TEXT)
)
instance.setViewState((viewState) => viewState.set('interactionMode', null))

instance.setViewState((viewState) => viewState.set('readOnly', true))

NutrientViewer.load({
  // ...
  isEditableAnnotation: (annotation) =>
    !(annotation instanceof NutrientViewer.Annotations.InkAnnotation),
})

instance.addEventListener('viewState.change', (viewState) => {
  highlightActiveTool(viewState.interactionMode)
})
```

### After: @sdk-pdf/react [#after-sdk-pdfreact-8]

The **interaction mode** combines a **content policy** (what may change) and an **interaction tool** (what a gesture does); [`useSetInteractionMode`](https://sdk.pdf.net/docs/v1/reference/useSetInteractionMode) switches both at once, and [`useInteractionTool`](https://sdk.pdf.net/docs/v1/reference/useInteractionTool) reads the tool back:

```tsx
import {
  CONTENT_POLICY_PRESET,
  InteractionTool,
  stageImage,
  useInteractionTool,
  useSetInteractionMode,
} from '@sdk-pdf/react'

const textSelectTool: InteractionTool = { type: 'textSelect' }
const selectTool: InteractionTool = { type: 'select' }
const addTextTool: InteractionTool = { type: 'addText' }

function ToolPicker() {
  const interactionTool = useInteractionTool()
  const setInteractionMode = useSetInteractionMode()

  async function placeImage(file: File) {
    const image = await stageImage(file)
    // Like the image toolbar item: the next click places the image as a stamp annotation.
    setInteractionMode({ type: 'addImage', image }, CONTENT_POLICY_PRESET.PDF_EDITOR)
  }

  return (
    <div role="toolbar">
      {/* Like readOnly: true. PDF_VIEWER instead keeps widgets fillable. textSelect is
         used only here, so it identifies the mode. */}
      <button
        aria-pressed={interactionTool.type === 'textSelect'}
        onClick={() =>
          setInteractionMode(textSelectTool, CONTENT_POLICY_PRESET.PDF_PRINTER)
        }
      >
        Read only
      </button>
      {/* Select, move, and resize blocks, with editable text. */}
      <button
        aria-pressed={interactionTool.type === 'select'}
        onClick={() => setInteractionMode(selectTool, CONTENT_POLICY_PRESET.PDF_EDITOR)}
      >
        Edit
      </button>
      <button
        aria-pressed={interactionTool.type === 'addText'}
        onClick={() => setInteractionMode(addTextTool, CONTENT_POLICY_PRESET.PDF_EDITOR)}
      >
        Add text
      </button>
      <input
        type="file"
        accept="image/*"
        onChange={(event) => {
          const file = event.currentTarget.files?.[0]
          if (file) placeImage(file).catch(showError)
        }}
      />
    </div>
  )
}
```

### Mapping the tools [#mapping-the-tools]

| `NutrientViewer.InteractionMode` and settings                                                                                                       | @sdk-pdf/react                                                                                                                                                                              |
| --------------------------------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `null`                                                                                                                                              | `{ type: 'textSelect' }`: native text selection for reading and copying; needs read-only text.                                                                                              |
| `PAN`                                                                                                                                               | No tool: `Viewport` scrolls and zooms on its own.                                                                                                                                           |
| `MULTI_ANNOTATIONS_SELECTION`                                                                                                                       | `{ type: 'select' }`: select, move, and resize blocks, with drag-selection of several.                                                                                                      |
| `TEXT`, `CALLOUT`                                                                                                                                   | The nearest tool is `{ type: 'addText' }`: it creates a text paragraph at the click position and focuses it for typing.                                                                     |
| The `image` toolbar item, `STAMP_PICKER` with an image                                                                                              | `{ type: 'addImage', image, width?, height? }` with `await stageImage(blob)` places a stamp annotation; the size is in user-space units. See [`stageImage`](https://sdk.pdf.net/docs/v1/reference/stageImage). |
| `TEXT_WIDGET`, `CHECKBOX_WIDGET`, `RADIO_BUTTON_WIDGET`, `COMBO_BOX_WIDGET`, `LIST_BOX_WIDGET`, `BUTTON_WIDGET`, `SIGNATURE_WIDGET`, `FORM_CREATOR` | `{ type: 'addWidget', widgetProps }` with the matching `widgetType`.                                                                                                                        |
| `DATE_WIDGET`                                                                                                                                       | A `text` widget with a `FormatType.DateTime` format.                                                                                                                                        |
| `CONTENT_EDITOR`                                                                                                                                    | Text is editable in place when the policy's `text` level is `'editable'` (`PDF_EDITOR`). See [Content editing](https://sdk.pdf.net/docs/v1/migration/from-nutrient#content-editing).                                                           |
| `DOCUMENT_EDITOR`                                                                                                                                   | The page hooks. See [Page manipulation](https://sdk.pdf.net/docs/v1/migration/from-nutrient#page-manipulation).                                                                                                                                |
| `INK`                                                                                                                                               | No drawing tool. [`useInkAnnotationCreate`](https://sdk.pdf.net/docs/v1/reference/useInkAnnotationCreate) creates an ink annotation from strokes your code collects.                                           |
| `TEXT_HIGHLIGHTER`, `SHAPE_*`, `NOTE`, `COMMENT_MARKER`, `SIGNATURE`, `LINK`, `REDACT_*`, measurement modes                                         | No equivalent.                                                                                                                                                                              |
| `ViewState#readOnly`                                                                                                                                | `CONTENT_POLICY_PRESET.PDF_PRINTER`, or `PDF_VIEWER` to keep widgets fillable.                                                                                                              |
| `isEditableAnnotation`, `editableAnnotationTypes`                                                                                                   | A policy level as a function of the block id, such as `annotations: (blockId) => (lockedIds.has(blockId) ? 'readonly' : 'editable')`.                                                       |

### What to know [#what-to-know-8]

* **Switch the policy with the tool.** Pass `PDF_EDITOR` with the editing tools, as above. `textSelect` needs read-only text: with editable text, the combination is unsupported and reports a message through [`logMessages`](https://sdk.pdf.net/docs/v1/reference/logMessages).
* **Per-kind and per-block permissions.** A custom [content policy](https://sdk.pdf.net/docs/v1/reference/useSetInteractionMode) is a plain object with `text`, `annotations`, `links`, and `widgets` levels, finer than one read-only switch. A level can be a function of the block id, which replaces `isEditableAnnotation`, and `'hidden'` doesn't render that content at all. There is no hook to read the policy back: keep the mode you set in your own state to show or hide editing UI, where Nutrient integrations read `viewState.readOnly`.
* **Local mode.** Switching the mode affects this client only.
* **Selection toolbars.** The annotation toolbar and the content editor's text style controls become your components on top of [`useSelectionTextStyle`](https://sdk.pdf.net/docs/v1/reference/useSelectionTextStyle) (font, size, color, bold, italic, alignment, spacing), and [`useSelectionAnnotationProps`](https://sdk.pdf.net/docs/v1/reference/useSelectionAnnotationProps) (border color and width). Widget settings such as multiline and format read from the widgets in [`useSelectedBlocks`](https://sdk.pdf.net/docs/v1/reference/useSelectedBlocks) and change with `setWidgetProps(ids, props)` from [`useSetWidgetProps`](https://sdk.pdf.net/docs/v1/reference/useSetWidgetProps). [`useSelection`](https://sdk.pdf.net/docs/v1/reference/useSelection) replaces listening to `annotationSelection.change`: it returns the selected block ids, or the paragraph in text editing. The markup and comment actions of the inline text selection toolbar (highlight, underline, strikeout, squiggle, comment) have no equivalent.
* **Keyboard shortcuts are your code.** Nutrient ships its own shortcuts; here you wire undo, redo, delete, and arrow keys with [`useHotkeys`](https://sdk.pdf.net/docs/v1/reference/useHotkeys).

## Content editing [#content-editing]

### Before: Nutrient Web SDK [#before-nutrient-web-sdk-9]

Editing the text of a page needs the Content Editor component. The UI session starts with `InteractionMode.CONTENT_EDITOR` and ends with **Save & Close** or **Cancel**; the programmatic session stages changes to text blocks and commits them:

```js
// The UI session: the content editor mode.
instance.setViewState((viewState) =>
  viewState.set('interactionMode', NutrientViewer.InteractionMode.CONTENT_EDITOR)
)
// Later, from your own Done button: saves the changes and exits content editing mode.
await instance.saveContentEditingSession()

// Or, without the UI. Throws while another session is in progress.
const session = await instance.beginContentEditingSession()
const [firstBlock] = await session.getTextBlocks(0)
await session.updateTextBlocks([
  { id: firstBlock.id, text: 'Revised wording', maxWidth: 300 },
])
// Applies the staged changes and closes the session; discard() drops them.
await session.commit()
```

### After: @sdk-pdf/react [#after-sdk-pdfreact-9]

Under `PDF_EDITOR`, text is editable in `Viewport` with nothing to start or save: users click into a paragraph and type. From code, [`useProgrammaticEditor`](https://sdk.pdf.net/docs/v1/reference/useProgrammaticEditor) reads paragraphs and edits them, as [Programmatic editing](https://sdk.pdf.net/docs/v1/guides/programmatic-editing) shows:

```tsx
import { useProgrammaticEditor } from '@sdk-pdf/react'

function useReplaceFirstParagraph() {
  const { getParagraphsByPage, getParagraphEditor } = useProgrammaticEditor()

  return async (pageId: string, text: string) => {
    // Like session.getTextBlocks(pageIndex): the page's paragraphs in content order.
    const [firstParagraph] = await getParagraphsByPage(pageId)
    // The paragraph may be gone: deleted by an undo since it was read.
    const editor = firstParagraph && getParagraphEditor(firstParagraph.id)
    if (!editor) return false

    // No staging and no commit: the edit applies at once.
    editor.selectAll()
    editor.deleteAll()
    editor.insertText(text)
    return true
  }
}
```

### What to know [#what-to-know-9]

* **No session.** There is no `beginContentEditingSession()`, `saveContentEditingSession()`, `discardContentEditingSession()`, or `hasUnsavedContentEditingChanges()`. Text edits are ordinary document changes: [`useUndoRedo`](https://sdk.pdf.net/docs/v1/reference/useUndoRedo) reverts them, [`useDocumentRevision`](https://sdk.pdf.net/docs/v1/reference/useDocumentRevision) tracks them, and `prepareDocument()` saves them. To drop every edit of a session, load the original bytes again.
* **Blocks become paragraphs.** A text block's `text` becomes a paragraph's `content`, its `anchor` becomes `x` and `y` (the start of the first baseline in user space), its `maxWidth` becomes `width` (the wrap width, or `'auto'`), and its `boundingBox` becomes `bounds`, the box the paragraph covers in user space; check positions on a test document, since the origins differ. [`useSetParagraphProps`](https://sdk.pdf.net/docs/v1/reference/useSetParagraphProps) sets the position, the wrap width, and the layout (`textAlign`, `lineHeight`, `textIndent`), and the height follows the text.
* **Styling.** The content editor's font, size, and color controls become a toolbar on top of [`useSelectionTextStyle`](https://sdk.pdf.net/docs/v1/reference/useSelectionTextStyle); [`useFocusSelection`](https://sdk.pdf.net/docs/v1/reference/useFocusSelection) returns the focus to the paragraph after a toolbar click.
* **New and deleted text.** The `addText` tool and [`useParagraphCreate`](https://sdk.pdf.net/docs/v1/reference/useParagraphCreate) create paragraphs. Paragraph ids are block ids, so [`useBlockDelete`](https://sdk.pdf.net/docs/v1/reference/useBlockDelete) deletes them by id, and [`useDeleteSelection`](https://sdk.pdf.net/docs/v1/reference/useDeleteSelection) deletes the selection, where Nutrient integrations call `contentEditor.deleteBlock()`.
* **Events.** `contentEditor.stateChange` becomes state: [`useSelection`](https://sdk.pdf.net/docs/v1/reference/useSelection) returns `{ type: 'text', blockId }` while a paragraph is in text editing. From [`useViewportEvent`](https://sdk.pdf.net/docs/v1/reference/useViewportEvent), `textEditorInput` reports typing, and `addToolApplied` reports a paragraph created with the `addText` tool. Paragraphs your code creates or deletes emit no events: track them where you call the hooks.

## Annotations [#annotations]

### Before: Nutrient Web SDK [#before-nutrient-web-sdk-10]

Annotations are immutable records created, updated, and deleted through the instance, with events for every change. Integrations often store them apart from the PDF, as Instant JSON or XFDF, and apply them again on the next load:

```js
const annotation = new NutrientViewer.Annotations.RectangleAnnotation({
  pageIndex: 0,
  boundingBox: new NutrientViewer.Geometry.Rect({
    left: 200,
    top: 150,
    width: 250,
    height: 75,
  }),
})
const [created] = await instance.create(annotation)
await instance.update(created.set('noPrint', true))
await instance.delete(created.id)

instance.addEventListener('annotations.change', () => refreshAnnotationsPanel())

// A diff on top of the original PDF.
const instantJSON = await instance.exportInstantJSON()
NutrientViewer.load({ ...configuration, instantJSON })
```

### After: @sdk-pdf/react [#after-sdk-pdfreact-10]

The SDK has no general annotation object API. Existing annotations in the PDF display in `Viewport` and in `PageCanvas`, and the selection hooks restyle them: [`useSelectionAnnotationProps`](https://sdk.pdf.net/docs/v1/reference/useSelectionAnnotationProps) changes the border color and width of selected shapes, lines, ink, and widgets:

```tsx
import { useSelectionAnnotationProps } from '@sdk-pdf/react'

function AnnotationStyleBar() {
  const { isApplicable, borderColor, borderWidth } = useSelectionAnnotationProps()

  // Nothing with a border is selected.
  if (!isApplicable) return null

  return (
    <div role="toolbar">
      {/* An empty value means the selected annotations disagree. */}
      <input
        type="color"
        value={borderColor.value ?? '#000000'}
        onChange={(event) => borderColor.set(event.currentTarget.value)}
      />
      <input
        type="number"
        min={0}
        value={borderWidth.value ?? ''}
        onChange={(event) => borderWidth.set(Number(event.currentTarget.value))}
      />
    </div>
  )
}
```

### What to know [#what-to-know-10]

* **Creation.** Two types can be created. Image stamps come from the `addImage` tool or, from code, `createStamp({ pageId, image, x, y, width?, height? })` from [`useStampAnnotationCreate`](https://sdk.pdf.net/docs/v1/reference/useStampAnnotationCreate). Ink comes from [`useInkAnnotationCreate`](https://sdk.pdf.net/docs/v1/reference/useInkAnnotationCreate) with strokes your code collects; there is no drawing tool. Creating highlights, underlines, shapes, notes, free text, callouts, links, and measurements has no equivalent.
* **No diff formats.** `exportInstantJSON()`, `exportXFDF()`, `NutrientViewer.Annotations.toSerializableObject()`, and the `instantJSON` and `XFDF` load options have no equivalent. Every change lives in the document, and `prepareDocument()` produces the PDF that carries it: store that file instead of a diff.
* **Events.** `annotations.create`, `annotations.update`, and `annotations.delete` have no counterpart. [`useDocumentRevision`](https://sdk.pdf.net/docs/v1/reference/useDocumentRevision) changes on any edit, without the changed objects, and `blocksDragEnd` and `blockResizeDragEnd` from [`useViewportEvent`](https://sdk.pdf.net/docs/v1/reference/useViewportEvent) report the user moving and resizing blocks.
* **Comments and authors.** Comments, replies, mentions, and `setAnnotationCreatorName()` have no equivalent. For programmatic edits, `setEditorAwarenessProperties` from `useProgrammaticEditor` gives the editor a name and a color for real-time collaboration, which is coming soon.
* **Hiding annotations.** `PageCanvas` takes `hideAnnotations` for thumbnails and previews.

## Undo, redo, and change tracking [#undo-redo-and-change-tracking]

### Before: Nutrient Web SDK [#before-nutrient-web-sdk-11]

The History API undoes and redoes annotation changes. It's off until `enableHistory` is set, and the toolbar's undo and redo items are hidden until you add them:

```js
NutrientViewer.load({
  // ...
  enableHistory: true,
  toolbarItems: [
    ...NutrientViewer.defaultToolbarItems,
    { type: 'undo' },
    { type: 'redo' },
  ],
})

undoButton.onclick = () => {
  if (instance.history.canUndo()) instance.history.undo()
}
redoButton.onclick = () => {
  if (instance.history.canRedo()) instance.history.redo()
}

instance.addEventListener('history.undo', ({ before, after }) => trackUndo(before, after))
```

### After: @sdk-pdf/react [#after-sdk-pdfreact-11]

[`useUndoRedo`](https://sdk.pdf.net/docs/v1/reference/useUndoRedo) returns the commands and their availability, which re-renders the component as the history changes:

```tsx
import { useMemo } from 'react'
import { useHotkeys, useUndoRedo } from '@sdk-pdf/react'

function HistoryButtons() {
  const { undo: undoChange, redo: redoChange, canUndo, canRedo } = useUndoRedo()

  // Like history.undo and history.redo listeners: undo and redo emit no events, so track
  // them where you call them.
  const { undo, redo } = useMemo(
    () => ({
      undo: () => {
        undoChange()
        trackHistory('undo')
      },
      redo: () => {
        redoChange()
        trackHistory('redo')
      },
    }),
    [undoChange, redoChange]
  )

  // Nutrient handles Ctrl/Cmd+Z itself; here you map the keys. The mapping must be
  // memoized.
  useHotkeys(useMemo(() => ({ 'Mod-z': undo, 'Shift-Mod-z': redo }), [undo, redo]))

  return (
    <div role="toolbar">
      <button disabled={!canUndo} onClick={undo}>
        Undo
      </button>
      <button disabled={!canRedo} onClick={redo}>
        Redo
      </button>
    </div>
  )
}
```

`hasUnsavedChanges()` and `document.saveStateChange` move to document revisions: [`useDocumentRevision`](https://sdk.pdf.net/docs/v1/reference/useDocumentRevision) re-renders with a new revision on every edit, and undo and redo bring back the revision of that point in the history. Keep the revision you last saved and compare: a different revision means unsaved changes. A save records the revision it saved, so the indicator lives with the save button in [Saving and downloading](https://sdk.pdf.net/docs/v1/migration/from-nutrient#saving-and-downloading).

### What to know [#what-to-know-11]

* **One history for the document.** Nutrient's History API covers creating, updating, and deleting annotations. `useUndoRedo` covers the document: deleting, moving, or rotating pages are undo steps, widget property changes are undo steps unless excluded, and form values follow undo and redo.
* **Nothing to enable.** There is no `enableHistory`, `history.enable()`, or `history.disable()`: `useUndoRedo` works whenever a document is loaded.
* **Local and merged.** As with Nutrient, undo reverts the local user's changes. Edits within 500 ms merge into one step, even across hook calls; to keep an operation a separate step, call `markUndoBoundary()` from [`useUndoBoundary`](https://sdk.pdf.net/docs/v1/reference/useUndoBoundary) before it starts and after its edits finish.
* **Opting out.** `history.willChange` with `preventDefault()` keeps an annotation change out of the history; here, only widget property changes made with `{ excludeFromHistory: true }` from [`useSetWidgetProps`](https://sdk.pdf.net/docs/v1/reference/useSetWidgetProps) stay out of it.
* **No history reset.** `history.clear()` has no equivalent; recording a new saved revision resets only the unsaved-changes baseline.

## Saving and downloading [#saving-and-downloading]

### Before: Nutrient Web SDK [#before-nutrient-web-sdk-12]

Nutrient tells saved changes from unsaved ones. With `AutoSaveMode.DISABLED`, nothing is saved until `instance.save()`, `hasUnsavedChanges()` reports the difference, and `exportPDF()` leaves out unsaved annotations, so a save comes first:

```js
NutrientViewer.load({
  ...configuration,
  autoSaveMode: NutrientViewer.AutoSaveMode.DISABLED,
})

instance.addEventListener('document.saveStateChange', ({ hasUnsavedChanges }) => {
  unsavedBadge.hidden = !hasUnsavedChanges
})

async function upload() {
  await instance.save()
  const arrayBuffer = await instance.exportPDF()

  const body = new FormData()
  body.append('file', new Blob([arrayBuffer], { type: 'application/pdf' }))
  await fetch('/upload', { method: 'POST', body })
}
```

### After: @sdk-pdf/react [#after-sdk-pdfreact-12]

[`useDocumentSaving`](https://sdk.pdf.net/docs/v1/reference/useDocumentSaving) applies the recorded changes to the original document; every change is included, with no save step before the export. Unsaved changes are relative to the [`useDocumentRevision`](https://sdk.pdf.net/docs/v1/reference/useDocumentRevision) value that you record after each upload:

```tsx
import { useEffect, useRef, useState } from 'react'
import {
  useDocumentRevision,
  useDocumentRevisionGetter,
  useDocumentSaving,
} from '@sdk-pdf/react'

function SaveControls() {
  const revision = useDocumentRevision()
  const getRevision = useDocumentRevisionGetter()
  // The revision on the server: the loaded document's revision until the first save.
  const [savedRevision, setSavedRevision] = useState(revision)
  // One save at a time, so an overlapping save can't mark a failed upload as saved.
  const isSavingRef = useRef(false)
  const { prepareDocument, downloadDocument } = useDocumentSaving()

  // Replaces document.saveStateChange; undoing back to the saved state clears it too.
  const hasUnsavedChanges = revision !== savedRevision

  // Warn before the tab closes with changes that aren't uploaded.
  useEffect(() => {
    if (!hasUnsavedChanges) return
    const warn = (event: BeforeUnloadEvent) => event.preventDefault()
    window.addEventListener('beforeunload', warn)
    return () => window.removeEventListener('beforeunload', warn)
  }, [hasUnsavedChanges])

  async function upload() {
    if (isSavingRef.current) return
    isSavingRef.current = true
    try {
      // Edits made after the save starts may be missing from the saved bytes, so only
      // this revision becomes saved.
      const startRevision = getRevision()
      // The bytes go to fetch, so don't hand out the loaded document's own buffer.
      const bytes = await prepareDocument({ returnOriginalIfUnchanged: false })

      const body = new FormData()
      body.append('file', new Blob([bytes as BlobPart], { type: 'application/pdf' }))
      const response = await fetch('/upload', { method: 'POST', body })
      if (!response.ok) throw new Error(`Upload failed: ${response.status}`)

      // Like a completed save(): later checks report changes relative to this revision.
      setSavedRevision(startRevision)
    } finally {
      isSavingRef.current = false
    }
  }

  return (
    <div role="toolbar">
      <button onClick={() => upload().catch(showError)}>Save</button>
      {/* Replaces the export-pdf toolbar item. */}
      <button onClick={() => void downloadDocument()}>Download</button>
      {hasUnsavedChanges && <span>Unsaved changes</span>}
    </div>
  )
}
```

One component owns the saved revision, so the save and the indicator compare against the same value. Render it only once the document is ready, so its first revision is the loaded document's.

### What to know [#what-to-know-12]

* **No save step.** `autoSaveMode`, `instance.save()`, `ensureChangesSaved()`, and `SaveError` have no counterpart: there is no pending state between an edit and the export.
* **One artifact.** `prepareDocument()` resolves with the complete PDF as a `Uint8Array`, where `exportPDF()` resolves with an `ArrayBuffer`, and `downloadDocument()` saves the same bytes on the user's device. Storing Instant JSON or XFDF next to the original file becomes storing the PDF.
* **returnOriginalIfUnchanged** defaults to `true`, returning the buffer passed to `loadPdf` when nothing changed; set it to `false` when the bytes will be transferred or mutated.
* **Save options.** Besides `returnOriginalIfUnchanged`, `pageIds` saves only the listed pages, in the listed order. `exportPDF()` flags such as `flatten`, `incremental`, and `permissions` (passwords and document permissions), `exportOffice()`, and a download file name have no equivalent.

## Collaboration [#collaboration]

Nutrient Web SDK syncs changes between users through Instant, which needs a server: Document Engine, or DWS Viewer API for the documents it manages. With Document Engine, the viewer loads with `documentId`, `authPayload: { jwt }`, and `instant: true`, `instance.connectedClients` lists the connected clients, and Collaboration Permissions come from the JWT. In standalone mode, changes stay in the browser until you export them.

Real-time collaboration in `@sdk-pdf/react` is coming soon. Your Document Engine client settings and Instant JSON sync code don't carry over: edits are CRDT operations, not annotation records.

## Coordinates [#coordinates]

| Topic          | Nutrient Web SDK                                                                            | @sdk-pdf/react                                                          |
| -------------- | ------------------------------------------------------------------------------------------- | ----------------------------------------------------------------------- |
| Page reference | Zero-based page indexes                                                                     | Session-scoped page ids; the position is the index in `usePages()`      |
| Origin         | Page coordinates: the top-left corner of the visible page area, y pointing down             | User space with the origin at the bottom-left corner of the page        |
| Units          | PDF points                                                                                  | User-space units, one unit is 1/72 of an inch                           |
| Page size      | `pageInfoForIndex(pageIndex).width` and `height`                                            | `width` and `height` of the `usePages()` entries, with rotation applied |
| Conversion     | `transformClientToPageSpace()`, `transformPageToClientSpace()`, `transformRawToPageSpace()` | No conversion helpers                                                   |

## Errors and logging [#errors-and-logging]

### Before: Nutrient Web SDK [#before-nutrient-web-sdk-13]

A failed load rejects the `load()` promise, and integrations tell the causes apart by the error message. `instance.save()` rejects with a `SaveError`, and `logLevel` turns on initialization logs in the console:

```js
try {
  instance = await NutrientViewer.load({
    ...configuration,
    // Tagged console logs for each initialization stage.
    logLevel: NutrientViewer.LogLevel.DEBUG,
  })
} catch (error) {
  if (error.message.includes('password')) askForPassword()
  else showError(error.message)
}

try {
  await instance.save()
} catch (error) {
  if (error instanceof NutrientViewer.SaveError) reportFailedChanges(error.reason)
}
```

### After: @sdk-pdf/react [#after-sdk-pdfreact-13]

Failures are typed errors from the call that failed, and problems the SDK works around are log messages you subscribe to with [`logMessages`](https://sdk.pdf.net/docs/v1/reference/logMessages). [Error handling and logging](https://sdk.pdf.net/docs/v1/guides/error-handling-and-logging) has the full rules:

```tsx
import { useEffect } from 'react'
import { InvalidPasswordError, logMessages, LogLevel, useLoadPdf } from '@sdk-pdf/react'

function useSdkLogging() {
  useEffect(() => {
    // The SDK never prints to the console: subscribe, and unsubscribe on unmount.
    const unsubscribe = logMessages.on('message', (logMessage) => {
      if (logMessage.level >= LogLevel.Warning) {
        reportToMonitoring(logMessage.reason, logMessage.message, logMessage.data)
      }
    })
    return () => {
      unsubscribe()
    }
  }, [])
}

function useOpenBytes() {
  const [, loadPdf] = useLoadPdf()

  return async (data: Uint8Array, password?: string) => {
    try {
      // Pass a copy and keep `data` for a password retry.
      await loadPdf(data.slice(), { password })
    } catch (error) {
      // instanceof replaces matching on error.message.
      if (error instanceof InvalidPasswordError) askForPassword()
      else showError(error)
    }
  }
}
```

### What to know [#what-to-know-13]

| Situation                         | Nutrient Web SDK                                      | @sdk-pdf/react                                           |
| --------------------------------- | ----------------------------------------------------- | -------------------------------------------------------- |
| Missing or wrong password         | `onPasswordRequired`, then the built-in prompt        | `InvalidPasswordError` from `loadPdf`                    |
| Malformed file                    | The `load()` promise rejects                          | `DocumentFormatError`, when the SDK can't work around it |
| Unsupported PDF feature           | No equivalent                                         | `NotSupportedError`                                      |
| Invalid input, such as a bad page | `NutrientViewer.Error`, such as from `setViewState()` | `InvalidInputError`                                      |
| No document                       | No equivalent                                         | `DocumentNotLoadedError`                                 |
| Network or HTTP failure           | The `load()` promise rejects                          | Your own `fetch` fails before `loadPdf` runs             |

* **Error classes.** Every SDK error extends `GenericError` with a static `message` and an optional `data` object; catch and narrow them with `instanceof`.
* **Nothing half-applied.** Commands validate first and throw before touching anything; readers return empty values instead of throwing; hooks never throw during render.
* **Logs replace console output.** Where `logLevel` prints initialization stages to the console, `@sdk-pdf/react` delivers `level`, `reason`, a static `message`, `data`, and `stack` to your subscriber; filter by `level` instead of setting a verbosity. `page.renderError` has no counterpart.

## Cleanup and lifecycle [#cleanup-and-lifecycle]

### Before: Nutrient Web SDK [#before-nutrient-web-sdk-14]

`NutrientViewer.unload()` takes the container, a selector, or the instance. Listeners come off with `removeEventListener` and the same function reference, and the `signal` option cancels a load that is still in progress:

```jsx
useEffect(() => {
  const container = containerRef.current
  const controller = new AbortController()
  let instance

  const handleZoom = () => persistZoom(instance.currentZoomLevel)

  NutrientViewer.load({ ...configuration, container, signal: controller.signal })
    .then((loaded) => {
      instance = loaded
      instance.addEventListener('viewState.zoom.change', handleZoom)
    })
    .catch((error) => {
      // An aborted load rejects with an AbortError.
      if (error.name !== 'AbortError') showError(error)
    })

  return () => {
    controller.abort()
    instance?.removeEventListener('viewState.zoom.change', handleZoom)
    NutrientViewer.unload(container)
  }
}, [])
```

### After: @sdk-pdf/react [#after-sdk-pdfreact-14]

* **Subscriptions follow components.** State hooks and [`useViewportEvent`](https://sdk.pdf.net/docs/v1/reference/useViewportEvent) subscribe for the component's lifetime, with callbacks that don't need memoizing or a stable reference. [`useHotkeys`](https://sdk.pdf.net/docs/v1/reference/useHotkeys) installs window-level shortcuts; memoize the mapping, or the listener re-installs on every render, and pass a falsy value instead of the mapping to disable them, such as while a modal is open.
* **Log subscriptions** return an unsubscribe function; call it in the effect cleanup, as in [Errors and logging](https://sdk.pdf.net/docs/v1/migration/from-nutrient#errors-and-logging).
* **Closing a document** is `resetPdf()` from [`useResetPdf`](https://sdk.pdf.net/docs/v1/reference/useResetPdf), which returns the load state to `'initial'`. There is no `unload()` for the SDK itself and no container to clear.
* **Cancelling a load.** `loadPdf` takes no `signal`; cancel the `fetch` that produces the bytes.

## Features without an equivalent [#features-without-an-equivalent]

Plan around these before you migrate:

| Nutrient Web SDK feature                                                                                  | In @sdk-pdf/react                                                                                                                                                 |
| --------------------------------------------------------------------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| Built-in toolbar, sidebars, Document Editor UI, themes, and localization (`toolbarItems`, `ui`, `locale`) | The UI is yours                                                                                                                                                   |
| Office documents and images                                                                               | PDF only                                                                                                                                                          |
| Creating highlight, shape, note, free text, link, and measurement annotations, and drawing ink            | No equivalent; existing annotations display, selected shapes, lines, ink, and widgets take a new border color and width, and code can create ink and image stamps |
| Comments, replies, mentions, and annotation authors                                                       | No equivalent                                                                                                                                                     |
| Instant JSON and XFDF import and export                                                                   | No equivalent; save the PDF                                                                                                                                       |
| Reading the selected text (`getTextSelection`)                                                            | No equivalent; `getParagraphsByPage` reads paragraph text, and `useDocumentSearch` replaces `search`                                                              |
| Printing (`print`)                                                                                        | No print API; `PageCanvas` can render pages for your own print layout, as loaded from the file, without session edits                                             |
| Redaction, electronic signatures, and digital signatures                                                  | No equivalent; the SDK never signs                                                                                                                                |
| Document comparison, OCR, Office export, and AI Assistant                                                 | No equivalent                                                                                                                                                     |
| PDF JavaScript (validation, formatting, calculations)                                                     | No equivalent; Acrobat date formats are recognized without running scripts                                                                                        |
| Layout and scroll modes, view-only rotation                                                               | One scrollable layout; rotation is a document edit                                                                                                                |
| Page labels, outline, bookmarks, layers, and attachments                                                  | No equivalent                                                                                                                                                     |
| `ZoomMode.AUTO`, zoom modes that stay applied on resize, marquee zoom                                     | Fits are one-time calculations; see [Zoom and fit modes](https://sdk.pdf.net/docs/v1/migration/from-nutrient#zoom-and-fit-modes)                                                                                     |
| Importing, cropping, and adding margins to pages; `exportPDFWithOperations`                               | No equivalent; `prepareDocument({ pageIds })` saves a subset of the pages                                                                                         |
| Export options: flattening, incremental saving, passwords, and permissions                                | No equivalent                                                                                                                                                     |
| Coordinate conversion (`transformClientToPageSpace` and the other transforms)                             | No equivalent                                                                                                                                                     |
| Scrolling to a rectangle (`jumpToRect`, `jumpAndZoomToRect`)                                              | No equivalent; `scrollToBlock` scrolls to a block                                                                                                                 |
| Clearing the history (`history.clear()`)                                                                  | No equivalent; recording a new saved revision resets only the unsaved-changes baseline                                                                            |
| `httpHeaders`, linearized loading, and `signal` on load                                                   | Use your own `fetch`; no linearized loading                                                                                                                       |
| Instant with Document Engine, connected clients, and Collaboration Permissions                            | Coming soon                                                                                                                                                       |

