# Migrating from Syncfusion PDF Viewer

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



This guide moves a React application built on the Syncfusion React PDF Viewer (`@syncfusion/ej2-react-pdfviewer`) to `@sdk-pdf/react`. It covers both Syncfusion deployment modes: standalone, which renders in the browser with PDFium assets from `resourceUrl`, and server-backed, which sends the work to a web service at `serviceUrl`. `PdfViewerComponent` is a complete viewer with toolbars, panels, and dialogs, extended by the feature modules you inject; `@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 built-in UI you rely on as React components, then moving your calls through the viewer `ref` to hooks. 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-syncfusion#overview): how the two integrations differ, and the API map for every viewer property, method, module call, and event you use.
* [ ] [List the built-in UI your users rely on](https://sdk.pdf.net/docs/v1/migration/from-syncfusion#display-the-document) (primary toolbar items, the annotation and form designer toolbars, the thumbnails and bookmarks panels, the Organize Pages dialog, the context menu, and the dialogs): each becomes a React component.
* [ ] [Install the package and replace `PdfViewerComponent` and its modules with `PdfProviders`](https://sdk.pdf.net/docs/v1/migration/from-syncfusion#install-and-set-up).
* [ ] [Load and close documents](https://sdk.pdf.net/docs/v1/migration/from-syncfusion#load-and-reset-a-document) with `loadPdf` and `resetPdf`, including the password flow.
* [ ] [Lay out `Viewport` with your own toolbar and sidebars](https://sdk.pdf.net/docs/v1/migration/from-syncfusion#display-the-document).
* [ ] [Move page navigation from numbers to page ids](https://sdk.pdf.net/docs/v1/migration/from-syncfusion#pages-and-navigation).
* [ ] [Move zoom percentages and fit modes](https://sdk.pdf.net/docs/v1/migration/from-syncfusion#zoom-and-fit-modes).
* [ ] [Rebuild the thumbnails panel with `PageCanvas`](https://sdk.pdf.net/docs/v1/migration/from-syncfusion#thumbnails) and [the Organize Pages dialog with the page hooks](https://sdk.pdf.net/docs/v1/migration/from-syncfusion#page-organizer).
* [ ] [Move form filling to widgets](https://sdk.pdf.net/docs/v1/migration/from-syncfusion#forms) and [form designing to the widget hooks](https://sdk.pdf.net/docs/v1/migration/from-syncfusion#form-designer).
* [ ] [Map interaction modes and read-only switches](https://sdk.pdf.net/docs/v1/migration/from-syncfusion#editing-and-interaction-tools), and [plan for the annotation tools](https://sdk.pdf.net/docs/v1/migration/from-syncfusion#annotations).
* [ ] [Replace `undo()`, `redo()`, and `isDocumentEdited`](https://sdk.pdf.net/docs/v1/migration/from-syncfusion#undo-redo-and-change-tracking).
* [ ] [Replace `saveAsBlob()` and `download()`](https://sdk.pdf.net/docs/v1/migration/from-syncfusion#saving-and-downloading), and check [collaboration](https://sdk.pdf.net/docs/v1/migration/from-syncfusion#collaboration).
* [ ] [Move error handling and logging](https://sdk.pdf.net/docs/v1/migration/from-syncfusion#errors-and-logging) and [cleanup](https://sdk.pdf.net/docs/v1/migration/from-syncfusion#cleanup-and-lifecycle).
* [ ] [Move text extraction](https://sdk.pdf.net/docs/v1/migration/from-syncfusion#text-extraction-and-search) and [plan for features without an equivalent](https://sdk.pdf.net/docs/v1/migration/from-syncfusion#features-without-an-equivalent).
* [ ] Remove `@syncfusion/ej2-react-pdfviewer`, the theme package and its CSS import, the `registerLicense` call, and the PDFium assets or the PDF Viewer web service. 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>

  **The Syncfusion PDF Viewer** is a packaged viewer component:

  * `PdfViewerComponent` renders the whole viewer: the primary toolbar, the navigation panel with thumbnails and bookmarks, the annotation and form designer toolbars, the Organize Pages dialog, the context menu, and dialogs such as the password prompt and error messages.
  * Features are modules. Each one is registered with `<Inject services={[...]} />` and switched with an `enable*` prop (`enableToolbar`, `enableAnnotation`, `enableFormDesigner`, `enablePageOrganizer`, and more), most of them on by default. A module that isn't injected isn't available: without `ThumbnailView`, `thumbnailView` is `undefined`.
  * You reach the API through a `ref` to the component: instance members (`load`, `saveAsBlob`, `download`, `undo`, `pageCount`) and module objects (`navigation`, `magnification`, `annotation`, `formDesigner`, `thumbnailView`, `pageOrganizer`). Changes arrive as component events such as `documentLoad`, `pageChange`, and `zoomChange`.
  * There are two deployment modes. Standalone mode renders in the browser with PDFium (`pdfium.js` and `pdfium.wasm`) loaded from `resourceUrl`, from the CDN or from your public folder. Server-backed mode renders and processes on a .NET web service at `serviceUrl`; since the 2026 Vol 2 release it gets no new features.
  * The app registers a license key with `registerLicense` from `@syncfusion/ej2-base` and imports its styles from a theme package, such as `@syncfusion/ej2-tailwind3-theme`.
  * Pages are 1-based numbers, zoom is a percentage (10 to 400 by default), and annotations and form fields are objects in collections (`annotationCollection`, `formFieldCollections`) that you edit through module methods and can export as JSON, XFDF, or FDF.

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

  * There are no prebuilt toolbars, panels, 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. There are no modules to inject and no instance to hold: components under it reach the document through hooks.
  * Rendering and editing run in the browser from the installed package, with no asset folder to host and no server mode. 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, the zoom is a scale factor, and positions are user-space units with the origin at the bottom-left corner of the page.

  | Concern      | Syncfusion PDF Viewer                                                                              | @sdk-pdf/react                                                                  |
  | ------------ | -------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------- |
  | UI           | Prebuilt toolbars, panels, and dialogs, configured with props (`toolbarSettings`, `enable*`)       | `Viewport` and `PageCanvas` only; you build toolbars, panels, and dialogs       |
  | Features     | Modules registered with `Inject` and switched with `enable*` props                                 | Hooks, all available under `PdfProviders`                                       |
  | Rendering    | Standalone: PDFium in the browser from `resourceUrl`; server-backed: a web service at `serviceUrl` | In the browser, from the package                                                |
  | Deployment   | License key, theme package CSS, PDFium assets or a web service                                     | The package from a private registry                                             |
  | State access | Instance members and module methods through a `ref`, component events                              | State hooks that re-render, command hooks, and event hooks                      |
  | Pages        | 1-based numbers                                                                                    | Session-scoped page ids                                                         |
  | Zoom         | Percentages, 10 to 400 by default                                                                  | A scale factor, where `1` is 100%                                               |
  | Forms        | Fields in `formFieldCollections`, updated with `updateFormFieldsValue`                             | Widgets with a `fieldName`, values bound with `useWidgetValue`                  |
  | Annotations  | Markup, shape, measurement, free text, ink, stamp, and sticky note tools                           | Existing annotations display; image stamps, ink from code, and border restyling |
  | Text         | Free text annotations on top of the page                                                           | Page text editable in place under the `PDF_EDITOR` content policy               |
  | Persistence  | `saveAsBlob()` and `download()`; JSON, XFDF, and FDF for annotations and form data                 | `prepareDocument()` and `downloadDocument()` for the PDF                        |

  What this means for your code:

  1. **Build the UI.** Every toolbar item, panel, 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 ref.** Calls through `viewerRef.current` become hook calls in the components that need them, and event props become state hooks or event hooks.
  3. **Drop the modules and the deployment pieces:** `Inject`, the `enable*` props, the PDFium assets or the web service, the license registration, and the theme CSS.
  4. **Replace numbers and percentages** with page ids and scale factors; convert at the edges, such as deep links and stored preferences.
  5. **Rethink forms as widgets.** Field objects become widgets, and the form designer becomes widget hooks.
  6. **Plan for the annotation tools.** Markup, shape, measurement, and sticky note tools have no equivalent; image stamps become image placement, and ink can be created from code.
  7. **Replace exported annotation and form data** with storing the PDF produced by `prepareDocument()`.
</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-syncfusion#features-without-an-equivalent).

  | Syncfusion PDF Viewer                                                                   | @sdk-pdf/react                                                                                                                                    | Notes                                                                                                                                                                                                                                                                                                                                                  |
  | --------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
  | `<PdfViewerComponent>` with `<Inject services={[...]} />`                               | `<PdfProviders>` and [`Viewport`](https://sdk.pdf.net/docs/v1/reference/Viewport)                                                                                    | No modules to register.                                                                                                                                                                                                                                                                                                                                |
  | `resourceUrl`, `resourcesLoaded`, `registerLicense`                                     | Nothing to configure                                                                                                                              | No asset URL or license key.                                                                                                                                                                                                                                                                                                                           |
  | `serviceUrl`, `serverActionSettings`, `ajaxRequestSettings`, `retryCount`               | **No equivalent**                                                                                                                                 | Everything runs in the browser.                                                                                                                                                                                                                                                                                                                        |
  | `documentPath`, `load(document, password)`                                              | `loadPdf(Uint8Array, { password })` from [`useLoadPdf`](https://sdk.pdf.net/docs/v1/reference/useLoadPdf)                                                            | Fetch the URL or decode the Base64 into bytes first.                                                                                                                                                                                                                                                                                                   |
  | Built-in password popup                                                                 | Catch `InvalidPasswordError` and ask yourself                                                                                                     |                                                                                                                                                                                                                                                                                                                                                        |
  | `documentLoad` event                                                                    | `loadState.status === 'ready'`                                                                                                                    |                                                                                                                                                                                                                                                                                                                                                        |
  | `documentLoadFailed` event                                                              | The rejected `loadPdf` promise                                                                                                                    | See [Errors and logging](https://sdk.pdf.net/docs/v1/migration/from-syncfusion#errors-and-logging).                                                                                                                                                                                                                                                                                                         |
  | `unload()`, `documentUnload` event                                                      | `resetPdf()` from [`useResetPdf`](https://sdk.pdf.net/docs/v1/reference/useResetPdf)                                                                                 |                                                                                                                                                                                                                                                                                                                                                        |
  | `pageCount`                                                                             | [`usePageCount`](https://sdk.pdf.net/docs/v1/reference/usePageCount)                                                                                                 |                                                                                                                                                                                                                                                                                                                                                        |
  | `currentPageNumber`, `pageChange` event                                                 | [`useCurrentPageId`](https://sdk.pdf.net/docs/v1/reference/useCurrentPageId)                                                                                         | Re-renders with the id of the page in the middle of the viewport.                                                                                                                                                                                                                                                                                      |
  | `navigation.goToPage(pageNumber)` and the first, previous, next, and last page methods  | `scrollToPage(pageId)` from [`useScrollToPage`](https://sdk.pdf.net/docs/v1/reference/useScrollToPage)                                                               | Resolve numbers through [`usePages`](https://sdk.pdf.net/docs/v1/reference/usePages).                                                                                                                                                                                                                                                                                     |
  | `getPageInfo(pageIndex)`                                                                | The `width`, `height`, and `rotation` of the `usePages()` entries                                                                                 | Same unit as points; rotation applied.                                                                                                                                                                                                                                                                                                                 |
  | `magnification.zoomTo(percent)`, `zoomIn()`, `zoomOut()`, `zoomValue`, `zoomPercentage` | [`useViewportScale`](https://sdk.pdf.net/docs/v1/reference/useViewportScale)                                                                                         | A factor, not a percentage.                                                                                                                                                                                                                                                                                                                            |
  | `magnification.fitToWidth()`, `fitToPage()`, `zoomMode`                                 | [`useViewportAutoFitScale`](https://sdk.pdf.net/docs/v1/reference/useViewportAutoFitScale), [`useViewportFitScale`](https://sdk.pdf.net/docs/v1/reference/useViewportFitScale)          | One-time fits.                                                                                                                                                                                                                                                                                                                                         |
  | `minZoom`, `maxZoom`                                                                    | `zoomGestures` on [`PdfProviders`](https://sdk.pdf.net/docs/v1/reference/PdfProviders)                                                                               | Bounds the pinch and wheel zoom.                                                                                                                                                                                                                                                                                                                       |
  | `zoomChange` event                                                                      | `scale` from `useViewportScale` in a `useEffect`                                                                                                  | `zoomGestureEnd` from [`useViewportEvent`](https://sdk.pdf.net/docs/v1/reference/useViewportEvent) reports pinch and wheel zoom.                                                                                                                                                                                                                                          |
  | `ThumbnailView`, `thumbnailView.openThumbnailPane()`, `thumbnailClick` event            | [`PageCanvas`](https://sdk.pdf.net/docs/v1/reference/PageCanvas) in your own sidebar                                                                                 |                                                                                                                                                                                                                                                                                                                                                        |
  | Organize Pages dialog, `pageOrganizerSettings`, `pageOrganizer.openPageOrganizer()`     | The page hooks in your own panel                                                                                                                  | See [Page organizer](https://sdk.pdf.net/docs/v1/migration/from-syncfusion#page-organizer).                                                                                                                                                                                                                                                                                                                 |
  | `extractPages(pages)`                                                                   | `prepareDocument({ pageIds })`                                                                                                                    | Saves only the listed pages, in the listed order.                                                                                                                                                                                                                                                                                                      |
  | Importing pages from another PDF                                                        | **No equivalent**                                                                                                                                 |                                                                                                                                                                                                                                                                                                                                                        |
  | `formFieldCollections`, `retrieveFormFields()`                                          | [`useWidgets`](https://sdk.pdf.net/docs/v1/reference/useWidgets), [`useWidgetsGetter`](https://sdk.pdf.net/docs/v1/reference/useWidgetsGetter)                                          | Widgets, not fields.                                                                                                                                                                                                                                                                                                                                   |
  | `updateFormFieldsValue(field)`, `clearFormFields()`                                     | [`useWidgetValue`](https://sdk.pdf.net/docs/v1/reference/useWidgetValue)                                                                                             | One widget per hook call.                                                                                                                                                                                                                                                                                                                              |
  | `resetFormFields()`                                                                     | **No equivalent**                                                                                                                                 |                                                                                                                                                                                                                                                                                                                                                        |
  | `focusFormField(name)`                                                                  | `scrollToBlock(widgetId)` from [`useScrollToBlock`](https://sdk.pdf.net/docs/v1/reference/useScrollToBlock)                                                          | Scrolls to the widget.                                                                                                                                                                                                                                                                                                                                 |
  | `formFieldAdd`, `formFieldRemove`, `formFieldPropertiesChange`, and other field events  | `useWidgets` re-renders on changes; `widgetInput` from [`useViewportEvent`](https://sdk.pdf.net/docs/v1/reference/useViewportEvent) reports a value the user changed | [`useDocumentRevision`](https://sdk.pdf.net/docs/v1/reference/useDocumentRevision) changes on any edit.                                                                                                                                                                                                                                                                   |
  | `importFormFields()`, `exportFormFields()`, `exportFormFieldsAsObject()`                | **No equivalent**                                                                                                                                 | `prepareDocument()` produces the filled PDF.                                                                                                                                                                                                                                                                                                           |
  | `isRequired`, `enableFormFieldsValidation`, `validateFormFields` event                  | **No equivalent**                                                                                                                                 | Validate in your own submit flow.                                                                                                                                                                                                                                                                                                                      |
  | `formDesigner.addFormField(type, settings)`                                             | `createWidgets(props)` from [`useWidgetsCreate`](https://sdk.pdf.net/docs/v1/reference/useWidgetsCreate)                                                             |                                                                                                                                                                                                                                                                                                                                                        |
  | `formDesigner.setFormFieldMode(type)`                                                   | The `{ type: 'addWidget', widgetProps }` interaction tool                                                                                         | Pass `PDF_EDITOR` with the tool.                                                                                                                                                                                                                                                                                                                       |
  | `formDesigner.updateFormField(field, settings)`                                         | `setWidgetProps(ids, props)` from [`useSetWidgetProps`](https://sdk.pdf.net/docs/v1/reference/useSetWidgetProps)                                                     |                                                                                                                                                                                                                                                                                                                                                        |
  | `formDesigner.deleteFormField(field)`                                                   | `deleteBlocks(ids)` from [`useBlockDelete`](https://sdk.pdf.net/docs/v1/reference/useBlockDelete)                                                                    |                                                                                                                                                                                                                                                                                                                                                        |
  | `interactionMode`, `annotation.setAnnotationMode(type)`                                 | [`useSetInteractionMode`](https://sdk.pdf.net/docs/v1/reference/useSetInteractionMode)                                                                               | See [Editing and interaction tools](https://sdk.pdf.net/docs/v1/migration/from-syncfusion#editing-and-interaction-tools).                                                                                                                                                                                                                                                                                   |
  | `enableAnnotation`, `enableFormDesigner`, `enableFormFields`, `designerMode`            | A content policy from [`CONTENT_POLICY_PRESET`](https://sdk.pdf.net/docs/v1/reference/CONTENT_POLICY_PRESET), or a custom one                                        |                                                                                                                                                                                                                                                                                                                                                        |
  | `annotation.addAnnotation()`, `editAnnotation()`, `annotationCollection`                | **No equivalent**                                                                                                                                 | Ink: [`useInkAnnotationCreate`](https://sdk.pdf.net/docs/v1/reference/useInkAnnotationCreate); image stamps: [`useStampAnnotationCreate`](https://sdk.pdf.net/docs/v1/reference/useStampAnnotationCreate); selected shapes, lines, and ink: border color and width through [`useSelectionAnnotationProps`](https://sdk.pdf.net/docs/v1/reference/useSelectionAnnotationProps). See [Annotations](https://sdk.pdf.net/docs/v1/migration/from-syncfusion#annotations). |
  | `exportAnnotation()`, `exportAnnotationsAsObject()`, `importAnnotation()`               | **No equivalent**                                                                                                                                 | No JSON or XFDF.                                                                                                                                                                                                                                                                                                                                       |
  | `annotationAdd`, `annotationRemove`, `annotationPropertiesChange` events                | `addToolApplied`, `blocksDragEnd`, and `blockResizeDragEnd` from `useViewportEvent`; `useDocumentRevision` for any change                         | Only for what the user does in `Viewport`.                                                                                                                                                                                                                                                                                                             |
  | `toolbarSettings`, `enableToolbar`, `toolbar.showToolbarItem()`, `toolbarClick` event   | Your own components                                                                                                                               | See [Display the document](https://sdk.pdf.net/docs/v1/migration/from-syncfusion#display-the-document).                                                                                                                                                                                                                                                                                                     |
  | Theme packages, dark themes                                                             | The `theme` configuration, see [Theming and customization](https://sdk.pdf.net/docs/v1/guides/theming-and-customization)                                             |                                                                                                                                                                                                                                                                                                                                                        |
  | `undo()`, `redo()`                                                                      | [`useUndoRedo`](https://sdk.pdf.net/docs/v1/reference/useUndoRedo)                                                                                                   | With `canUndo` and `canRedo`.                                                                                                                                                                                                                                                                                                                          |
  | `isDocumentEdited`                                                                      | [`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.                                                                                                                                                                                                                                                                                                              |
  | `saveAsBlob()`                                                                          | `prepareDocument()` from [`useDocumentSaving`](https://sdk.pdf.net/docs/v1/reference/useDocumentSaving)                                                              | Resolves with a `Uint8Array`, not a `Blob`.                                                                                                                                                                                                                                                                                                            |
  | `download()`, `downloadFileName`                                                        | `downloadDocument()`                                                                                                                              | No file name option.                                                                                                                                                                                                                                                                                                                                   |
  | `downloadStart`, `downloadEnd` events, `annotationSettings.skipDownload`                | **No equivalent**                                                                                                                                 |                                                                                                                                                                                                                                                                                                                                                        |
  | `showNotificationDialog`, `showNotificationPopup()`                                     | Your own error UI                                                                                                                                 | The SDK shows no dialogs.                                                                                                                                                                                                                                                                                                                              |
  | `destroy()`, `enableLocalStorage`                                                       | No dispose call; unsubscribe log listeners yourself                                                                                               | See [Cleanup and lifecycle](https://sdk.pdf.net/docs/v1/migration/from-syncfusion#cleanup-and-lifecycle).                                                                                                                                                                                                                                                                                                   |
  | `extractText(pageIndex, options)`                                                       | `getParagraphsByPage(pageId)` from [`useProgrammaticEditor`](https://sdk.pdf.net/docs/v1/reference/useProgrammaticEditor)                                            | Paragraph text and `bounds`.                                                                                                                                                                                                                                                                                                                           |
  | `textSearch.searchText()`, `searchNext()`, `searchPrevious()`                           | [`useDocumentSearch`](https://sdk.pdf.net/docs/v1/reference/useDocumentSearch)                                                                                       | Highlights the matches and scrolls to the active one.                                                                                                                                                                                                                                                                                                  |
  | `Print` module, `BookmarkView` module                                                   | **No equivalent**                                                                                                                                 |                                                                                                                                                                                                                                                                                                                                                        |
  | `convertClientPointToPagePoint()`, `convertPagePointToClientPoint()`                    | **No equivalent**                                                                                                                                 |                                                                                                                                                                                                                                                                                                                                                        |
</details>

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

### Before: Syncfusion PDF Viewer [#before-syncfusion-pdf-viewer]

The Syncfusion PDF Viewer installs as `@syncfusion/ej2-react-pdfviewer`, takes its styles from a theme package, and, in standalone mode, loads PDFium from a CDN or from assets you copy into your app:

```sh
npm install @syncfusion/ej2-react-pdfviewer @syncfusion/ej2-tailwind3-theme
# Only to host PDFium yourself instead of using the CDN.
cp -R ./node_modules/@syncfusion/ej2-pdfviewer/dist/ej2-pdfviewer-lib \
  public/assets/ej2-pdfviewer-lib
```

```css
@import '../node_modules/@syncfusion/ej2-tailwind3-theme/styles/pdfviewer/index.css';
```

The license key is registered once, and the component lists the modules it uses:

```jsx
import { registerLicense } from '@syncfusion/ej2-base'
import {
  Annotation,
  FormDesigner,
  FormFields,
  Inject,
  Magnification,
  Navigation,
  PageOrganizer,
  PdfViewerComponent,
  TextSelection,
  ThumbnailView,
  Toolbar,
} from '@syncfusion/ej2-react-pdfviewer'

registerLicense('YOUR_LICENSE_KEY')

// The PDFium assets must match the installed version.
export const RESOURCE_URL =
  'https://cdn.syncfusion.com/ej2/35.1.37/dist/ej2-pdfviewer-lib'

export function App() {
  return (
    <PdfViewerComponent
      id="container"
      documentPath="https://example.com/files/contract.pdf"
      // Standalone mode; server-backed integrations set serviceUrl instead.
      resourceUrl={RESOURCE_URL}
      style={{ height: '100vh' }}
    >
      <Inject
        services={[
          Toolbar,
          Magnification,
          Navigation,
          ThumbnailView,
          TextSelection,
          Annotation,
          FormFields,
          FormDesigner,
          PageOrganizer,
        ]}
      />
    </PdfViewerComponent>
  )
}
```

The other Before snippets in this guide reuse `RESOURCE_URL`.

### 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) where `PdfViewerComponent` was. Its props must keep their identity between renders, so define tools 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="TextSelection": native text selection for reading and copying.
const textSelectTool: InteractionTool = { type: 'textSelect' }
// Select, move, and resize blocks.
const selectTool: InteractionTool = { type: 'select' }

export function App({ canEdit }: { canEdit: boolean }) {
  // The PDF application re-renders whenever a PdfProviders prop changes identity.
  const config = useMemo(
    () => ({
      theme: { viewportPadding: 16 },
      // Both apply when the viewport first mounts; switch later with
      // useSetInteractionMode.
      initialInteractionTool: canEdit ? selectTool : textSelectTool,
      initialContentPolicy: canEdit
        ? CONTENT_POLICY_PRESET.PDF_EDITOR
        : CONTENT_POLICY_PRESET.PDF_VIEWER,
    }),
    [canEdit]
  )

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

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

* **No assets, no service, no license key in code.** The install is the registry scope and the package. `resourceUrl`, the copied `ej2-pdfviewer-lib` folder, `serviceUrl` and its web service, and `registerLicense` have no counterpart. A server-backed integration keeps only the endpoints that serve and store the PDF bytes.
* **No modules.** `Inject` and the `enable*` props go away. Every hook works under `PdfProviders`, and a feature your users shouldn't see is a control you don't render; the content policy decides what users may change inside `Viewport`.
* **No ref.** The `ref` to `PdfViewerComponent`, or the `ej2_instances` lookup, goes away: any component under `PdfProviders` reaches the document through hooks.
* **Both default to editing.** The Syncfusion viewer enables annotations, the form designer, form fields, and the page organizer by default, and `PdfProviders` defaults to the `select` tool and [`CONTENT_POLICY_PRESET.PDF_EDITOR`](https://sdk.pdf.net/docs/v1/reference/CONTENT_POLICY_PRESET), where text, annotations, links, and widgets are all editable. Where your app turned those modules off for read-only users, start with `PDF_VIEWER` (text and annotations read-only, links and widgets interactive) and the `textSelect` tool, as above.

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

### Before: Syncfusion PDF Viewer [#before-syncfusion-pdf-viewer-1]

`documentPath` loads the first document, and `load(document, password)` loads another one at runtime from a URL, a Base64 data URI, or a `Uint8Array`. Encrypted files get the built-in password popup. Loading reports through `documentLoad` and `documentLoadFailed`, whose arguments carry `documentName`, `isPasswordRequired`, and the incorrect `password`, and `unload()` closes the document:

```jsx
import { useRef } from 'react'
import {
  Inject,
  Navigation,
  PdfViewerComponent,
  Toolbar,
} from '@syncfusion/ej2-react-pdfviewer'

function Viewer() {
  const viewerRef = useRef(null)

  async function openFromServer() {
    const response = await fetch('/api/documents/contract.pdf', {
      headers: { Authorization: `Bearer ${token}` },
    })
    const blob = await response.blob()
    // The second argument is the password; the popup asks for it when it's missing or
    // wrong.
    viewerRef.current?.load(URL.createObjectURL(blob), null)
  }

  return (
    <>
      <button onClick={openFromServer}>Open</button>
      <button onClick={() => viewerRef.current?.unload()}>Close</button>
      <PdfViewerComponent
        ref={viewerRef}
        resourceUrl={RESOURCE_URL}
        documentLoad={(args) => showDocumentUi(args.documentName)}
        documentLoadFailed={(args) => showError(args.documentName)}
        style={{ height: '100vh' }}
      >
        <Inject services={[Toolbar, Navigation]} />
      </PdfViewerComponent>
    </>
  )
}
```

In standalone mode, a `load` call made before the PDFium assets are ready can fail, so integrations that load right away wait for the `resourcesLoaded` event.

### 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. Fetching the file is your own code, with whatever headers your server needs:

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

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

  async function open(password?: string) {
    setLoadError(undefined)

    try {
      // Replaces documentPath: fetch the file once and keep the bytes for a password
      // retry.
      if (!bytesRef.current) {
        const response = await fetch(url)
        if (!response.ok)
          throw new Error(`The document request failed with ${response.status}`)
        bytesRef.current = new Uint8Array(await response.arrayBuffer())
      }

      // loadPdf reads from the buffer it receives for the document's lifetime, so pass a
      // copy.
      await loadPdf(bytesRef.current.slice(), { password })
      setNeedsPassword(false)
    } catch (error) {
      if (error instanceof InvalidPasswordError) {
        // Replaces the built-in password popup: ask, then call open(password) again.
        setNeedsPassword(true)
      } else {
        // Replaces documentLoadFailed and the built-in error message box.
        setLoadError(error)
      }
    }
  }

  if (needsPassword)
    return <PasswordPrompt onSubmit={(password) => void open(password)} />
  if (loadError) return <LoadError error={loadError} onRetry={() => void open()} />
  if (loadState.status === 'initial')
    return <button onClick={() => void open()}>Open</button>
  if (loadState.status === 'loading') return <p>Loading the document…</p>

  return <PdfApplication />
}
```

[`useResetPdf`](https://sdk.pdf.net/docs/v1/reference/useResetPdf) replaces `unload()`:

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

function CloseDocumentButton() {
  const resetPdf = useResetPdf()

  // The load state returns to 'initial', and DocumentLoader shows its Open button again.
  return <button onClick={() => resetPdf()}>Close</button>
}
```

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

* **Bytes, not paths.** `loadPdf` takes a `Uint8Array`. A URL becomes a `fetch`, a `Blob` or `File` becomes `new Uint8Array(await blob.arrayBuffer())`, and a Base64 string is decoded to bytes first. A server-backed `documentPath` that names a file on the web service becomes a request to your own endpoint.
* **Keep a copy for retries.** The document reads from the given buffer for its whole lifetime, and another `loadPdf` call can detach it. To reuse the bytes for a password retry, pass a copy: `loadPdf(data.slice(), { password })`.
* **Passwords are your UI.** Without a password, the empty password is tried, as the PDF specification prescribes; a missing or wrong password rejects with `InvalidPasswordError`. There is no popup: catch the rejection, ask, and call `loadPdf` again.
* **No events to wire.** `documentLoad` becomes the awaited `loadPdf` or `loadState.status === 'ready'`, and `documentLoadFailed` becomes the rejection. With no `resourceUrl`, there is no `resourcesLoaded` step before the first load.
* **Closing invalidates ids.** After `resetPdf()`, page and block ids, widget snapshots, and document revisions from the old document are no longer valid. For an open-another-document action, call `resetPdf()` and then `loadPdf`.

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

### Before: Syncfusion PDF Viewer [#before-syncfusion-pdf-viewer-2]

The component renders the document with its toolbars and panels. Integrations pick the primary toolbar items, handle custom items in `toolbarClick`, hide toolbars with props or with `toolbar.showToolbar(false)`, and style everything with a theme package:

```jsx
<PdfViewerComponent
  ref={viewerRef}
  resourceUrl={RESOURCE_URL}
  // Only these items, in this order; a custom item is an object with an id.
  toolbarSettings={{
    toolbarItems: [
      'OpenOption',
      'PageNavigationTool',
      'MagnificationTool',
      { id: 'fit_width', text: 'Fit to width', align: 'Center' },
      'DownloadOption',
    ],
  }}
  toolbarClick={(args) => {
    if (args.item.id === 'fit_width') viewerRef.current?.magnification.fitToWidth()
  }}
  enableNavigationToolbar={false}
  style={{ height: '640px' }}
>
  <Inject services={[Toolbar, Magnification, Navigation]} />
</PdfViewerComponent>
```

For a fully custom toolbar, integrations set `enableToolbar={false}` and call the viewer API from their own buttons, which is the shape every toolbar takes with `@sdk-pdf/react`.

### 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. A typical shell puts a toolbar above the viewport and a sidebar beside it, as [Building a PDF application](https://sdk.pdf.net/docs/v1/guides/building-a-pdf-application) walks through:

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

// A fuller theme than the one in App: pass it as PdfProviders' `theme` prop.
// Defined outside components, so its identity is stable.
export const theme: Config['theme'] = {
  viewportPadding: [24, 16],
  classNames: { scrollViewport: 'pdf-viewport', page: 'pdf-page' },
  styles: { scrollViewport: { '--selection-color': '#0b5cad' } as React.CSSProperties },
}

function PdfApplication() {
  // Replaces zoomMode="FitToWidth": fits once per loaded document.
  useViewportAutoFitScale('width')

  return (
    <div style={{ display: 'grid', gridTemplateRows: 'auto 1fr', height: '100vh' }}>
      {/* Replaces the primary toolbar: the controls from the sections below. */}
      <header style={{ display: 'flex', gap: 8 }}>
        <PageNavigation />
        <ZoomControls />
        <HistoryControls />
        <SaveControls />
      </header>
      <div style={{ display: 'grid', gridTemplateColumns: 'auto 1fr', minHeight: 0 }}>
        {/* Replaces the navigation panel. */}
        <ThumbnailsSidebar />
        {/* The viewport fills its parent, so the parent needs an explicit size. */}
        <div style={{ minHeight: 0 }}>
          <Viewport />
        </div>
      </div>
    </div>
  )
}
```

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

* **There is nothing to hide.** `toolbarItems`, `enableToolbar`, `enableNavigationToolbar`, and the show and hide methods of the `toolbar` module have no counterpart: every control exists only if you build it. Selection, text editing, form filling, links, and zoom gestures work inside `Viewport`, limited by the content policy.
* **Toolbar clicks become handlers.** Custom toolbar items and the `toolbarClick` branches on `args.item.id` become `onClick` handlers on your own buttons, which call the hooks shown in this guide.
* **Theming is CSS you own.** The Material 3, Fluent 2, Bootstrap 5, and Tailwind theme packages and their dark variants have no counterpart. `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.
* **Size the parent.** Where `PdfViewerComponent` takes a `height` or a `style`, `Viewport` fills its parent element, so give the parent an explicit size.
* **Render only when ready.** `Viewport` throws `DocumentNotLoadedError` when no document is loaded.

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

### Before: Syncfusion PDF Viewer [#before-syncfusion-pdf-viewer-3]

The `navigation` module moves between 1-based page numbers, `pageCount` and `currentPageNumber` read the position, and `pageChange` reports changes:

```jsx
import { useRef, useState } from 'react'
import { Inject, Navigation, PdfViewerComponent } from '@syncfusion/ej2-react-pdfviewer'

function Viewer() {
  const viewerRef = useRef(null)
  const [pageNumber, setPageNumber] = useState(1)
  const [pageCount, setPageCount] = useState(0)

  return (
    <>
      <button onClick={() => viewerRef.current?.navigation.goToPreviousPage()}>
        Previous
      </button>
      <span>
        {pageNumber} of {pageCount}
      </span>
      <button onClick={() => viewerRef.current?.navigation.goToNextPage()}>Next</button>
      <PdfViewerComponent
        ref={viewerRef}
        resourceUrl={RESOURCE_URL}
        documentLoad={() => setPageCount(viewerRef.current.pageCount)}
        pageChange={(args) => setPageNumber(args.currentPageNumber)}
        style={{ height: '640px' }}
      >
        <Inject services={[Navigation]} />
      </PdfViewerComponent>
    </>
  )
}
```

### 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 PageNavigation() {
  const pages = usePages()
  // Replaces currentPageNumber and pageChange.
  const currentPageId = useCurrentPageId()
  const scrollToPage = useScrollToPage()

  // Until there is a current page, treat the first page as current.
  const index =
    currentPageId === undefined ? 0 : pages.findIndex((page) => page.id === currentPageId)

  // Replaces navigation.goToPage(pageNumber): resolve the 1-based number to an id first.
  function scrollToPageNumber(pageNumber: number) {
    const page = pages[pageNumber - 1]
    if (page) scrollToPage(page.id, { behavior: 'smooth' })
  }

  return (
    <div role="toolbar">
      <button disabled={index <= 0} onClick={() => scrollToPageNumber(1)}>
        First
      </button>
      {/* The current page number is index + 1, so the previous one is index. */}
      <button disabled={index <= 0} onClick={() => scrollToPageNumber(index)}>
        Previous
      </button>
      <span>
        {index === -1 ? '–' : index + 1} of {pages.length}
      </span>
      <button
        disabled={index === -1 || index >= pages.length - 1}
        onClick={() => scrollToPageNumber(index + 2)}
      >
        Next
      </button>
      <button
        disabled={index === -1 || index >= pages.length - 1}
        onClick={() => scrollToPageNumber(pages.length)}
      >
        Last
      </button>
    </div>
  )
}
```

When you need a page's number 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; [`usePageCount`](https://sdk.pdf.net/docs/v1/reference/usePageCount) replaces `pageCount`.

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

* **Ids survive reordering; numbers don't.** An id keeps pointing at the same page while pages are added, removed, and rearranged by the user, where a number would drift to another page. Ids are session-scoped: persist page numbers, not ids, and resolve them once the document is ready.
* **Invalid targets throw.** `goToPage` with a number out of range keeps the current page. `scrollToPage` with an unknown or deleted id throws `InvalidInputError`, so resolve the number first, as `scrollToPageNumber` above does.
* **Scroll options.** `scrollToPage(pageId, options?)` takes `behavior` (`'auto'`, `'smooth'`, or `'instant'`), `vertical` (`'top'` by default), and `horizontal` (`'nearest'` by default).
* **Page size.** `getPageInfo(pageIndex)` becomes the `width`, `height`, and `rotation` of a `usePages()` entry. The sizes are user-space units, one unit being 1/72 of an inch like a point, with the rotation applied.
* **Links and bookmarks.** Links work inside `Viewport` when the content policy's `links` level is `'interactive'`. The bookmarks panel (`BookmarkView`), `hyperlinkClick`, and `hyperlinkOpenState` have no equivalent.

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

### Before: Syncfusion PDF Viewer [#before-syncfusion-pdf-viewer-4]

Zoom is a percentage, 10 to 400 by default and clamped to `minZoom` and `maxZoom`. `zoomMode` sets the initial fit, and the `magnification` module zooms and fits from your controls:

```jsx
<PdfViewerComponent
  ref={viewerRef}
  resourceUrl={RESOURCE_URL}
  // 'Default', 'FitToWidth', or 'FitToPage'.
  zoomMode="FitToWidth"
  minZoom={50}
  maxZoom={300}
  zoomChange={(args) => persistZoom(args.zoomValue)}
  style={{ height: '640px' }}
>
  <Inject services={[Toolbar, Magnification, Navigation]} />
</PdfViewerComponent>
```

```js
viewerRef.current?.magnification.zoomTo(150)
viewerRef.current?.magnification.zoomIn()
viewerRef.current?.magnification.fitToPage()
viewerRef.current?.magnification.fitToWidth()
```

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

[`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'

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

  // Replaces zoomChange: every scale change, whatever the source.
  useEffect(() => persistZoom(scale), [scale])

  function fitPage() {
    const { fitWidthScale, fitHeightScale } = getFitScale()
    // The smaller scale fits the pages in both directions, like fitToPage().
    setScale(Math.min(fitWidthScale, fitHeightScale))
  }

  return (
    <div role="toolbar">
      <button onClick={() => setScale(scale / 1.25)}>Zoom out</button>
      {/* The scale is a factor: 1 is 100%. */}
      <span>{Math.round(scale * 100)}%</span>
      <button onClick={() => setScale(scale * 1.25)}>Zoom in</button>
      {/* Call the getter inside the handler: the result reflects the viewport size at
         that moment. */}
      <button onClick={() => setScale(getFitScale().fitWidthScale)}>Fit width</button>
      <button onClick={fitPage}>Fit page</button>
    </div>
  )
}
```

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

* **Percentages become factors.** `setScale` takes a positive finite number, not a percentage, and throws `InvalidInputError` otherwise; scale `1` is one user-space unit per CSS pixel. Compare the page size on screen with a test document before converting zoom levels you stored as Syncfusion percentages.
* **Fits are one-time calculations.** [`useViewportAutoFitScale`](https://sdk.pdf.net/docs/v1/reference/useViewportAutoFitScale) takes `'width'` or `'height'` and applies when a document loads; it doesn't re-apply on viewport resizes or manual zooming. `zoomMode="FitToPage"` has no automatic counterpart: fit the page from a control, as `fitPage` above does.
* **What the fits measure.** `fitWidthScale` fits the widest page of the document into the viewport width and `fitHeightScale` the tallest page into its height, both accounting for `viewportPadding`.
* **Zoom range and gestures.** Pinch and wheel zoom are built in and tuned with `zoomGestures` on `PdfProviders`: `minScale` (`0.5` by default) and `maxScale` (`4`) take the place of `minZoom` and `maxZoom` for the gestures, and the wheel speed is configurable too.
* **Zoom around a point.** `setScale(scale, pivot?)` 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.

## Thumbnails [#thumbnails]

### Before: Syncfusion PDF Viewer [#before-syncfusion-pdf-viewer-5]

The `ThumbnailView` module adds a thumbnails panel, opened from the navigation toolbar on the left of the viewer. `isThumbnailViewOpen` opens it on load, the `thumbnailView` module opens and closes it, and `thumbnailClick` reports the clicked page:

```jsx
;<>
  <button onClick={() => viewerRef.current?.thumbnailView.openThumbnailPane()}>
    Thumbnails
  </button>
  <PdfViewerComponent
    ref={viewerRef}
    resourceUrl={RESOURCE_URL}
    isThumbnailViewOpen={true}
    thumbnailClick={(args) => trackThumbnailClick(args.pageNumber)}
    style={{ height: '640px' }}
  >
    <Inject services={[Toolbar, Navigation, ThumbnailView]} />
  </PdfViewerComponent>
</>
```

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

[`PageCanvas`](https://sdk.pdf.net/docs/v1/reference/PageCanvas) renders a page lazily at the canvas's CSS size; the panel, its open state, and its highlight of the current page are yours:

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

function ThumbnailsSidebar() {
  const pages = usePages()
  const currentPageId = useCurrentPageId()
  const scrollToPage = useScrollToPage()
  // Replaces isThumbnailViewOpen, openThumbnailPane(), and closeThumbnailPane().
  const [isOpen, setIsOpen] = useState(true)

  return (
    <aside>
      <button aria-expanded={isOpen} onClick={() => setIsOpen(!isOpen)}>
        Thumbnails
      </button>
      {isOpen && (
        <nav>
          {pages.map((page, index) => (
            <button
              key={page.id}
              aria-current={page.id === currentPageId ? 'page' : undefined}
              // Replaces thumbnailClick: run your own code before or after scrolling.
              onClick={() => scrollToPage(page.id)}
            >
              <PageCanvas
                pageId={page.id}
                className="thumbnail"
                role="img"
                aria-label={`Page ${index + 1}`}
              />
            </button>
          ))}
        </nav>
      )}
    </aside>
  )
}
```

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

* **Lazy and sized with CSS.** An off-screen canvas does no work until it becomes visible, and the render resolution follows the displayed CSS size or an explicit `scale`. 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.

## Page organizer [#page-organizer]

### Before: Syncfusion PDF Viewer [#before-syncfusion-pdf-viewer-6]

The `PageOrganizer` module adds the Organize Pages dialog: users rotate, rearrange, insert, delete, and copy pages, import pages from another PDF, and then save over the document or save a copy. `pageOrganizerSettings` chooses the actions, `pageOrganizer.openPageOrganizer()` opens the dialog, and `pageOrganizerSaveAs` receives the result as Base64. `extractPages` returns selected pages as a new PDF:

```jsx
const pageOrganizerSettings = {
  canDelete: true,
  canInsert: true,
  canRotate: true,
  canCopy: true,
  canRearrange: true,
  canImport: false,
}

function Viewer() {
  const viewerRef = useRef(null)

  return (
    <>
      <button onClick={() => viewerRef.current?.pageOrganizer.openPageOrganizer()}>
        Organize pages
      </button>
      {/* A new PDF with pages 1 and 2, as a Uint8Array. */}
      <button onClick={() => uploadExtract(viewerRef.current?.extractPages('1,2'))}>
        Extract
      </button>
      <PdfViewerComponent
        ref={viewerRef}
        resourceUrl={RESOURCE_URL}
        pageOrganizerSettings={pageOrganizerSettings}
        pageOrganizerSaveAs={(args) => {
          // Upload the result instead of the default Save As download.
          args.cancel = true
          uploadBase64(args.fileName, args.downloadDocument)
        }}
        style={{ height: '640px' }}
      >
        <Inject services={[Toolbar, Navigation, ThumbnailView, PageOrganizer]} />
      </PdfViewerComponent>
    </>
  )
}
```

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

Each page operation is a hook that returns a command taking page ids: [`usePageCreate`](https://sdk.pdf.net/docs/v1/reference/usePageCreate), [`usePageDelete`](https://sdk.pdf.net/docs/v1/reference/usePageDelete), [`usePageMove`](https://sdk.pdf.net/docs/v1/reference/usePageMove), [`useSetPageRotation`](https://sdk.pdf.net/docs/v1/reference/useSetPageRotation), and [`usePageDuplicate`](https://sdk.pdf.net/docs/v1/reference/usePageDuplicate). The organizer panel is yours; it offers only the actions you render:

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

function OrganizePagesPanel() {
  const pages = usePages()
  const createPage = usePageCreate()
  const deletePages = usePageDelete()
  const duplicatePage = usePageDuplicate()
  const { moveBefore, moveAfter } = usePageMove()
  const setPageRotation = useSetPageRotation()

  return (
    <ol>
      {pages.map((page, index) => (
        <li key={page.id}>
          <PageCanvas
            pageId={page.id}
            className="organizer-thumbnail"
            role="img"
            aria-label={`Page ${index + 1}`}
          />
          {/* Rotation is absolute: add to the current angle. -90 is normalized to
             270. */}
          <button onClick={() => setPageRotation(page.id, page.rotation - 90)}>
            Rotate left
          </button>
          <button onClick={() => setPageRotation(page.id, page.rotation + 90)}>
            Rotate right
          </button>
          <button
            disabled={index === 0}
            onClick={() => moveBefore(page.id, pages[index - 1].id)}
          >
            Move up
          </button>
          <button
            disabled={index === pages.length - 1}
            onClick={() => moveAfter(page.id, pages[index + 1].id)}
          >
            Move down
          </button>
          {/* Copy pages: the duplicate goes right after the source. */}
          <button onClick={() => void duplicatePage(page.id)}>Copy</button>
          {/* A blank page the size of this one as displayed, inserted after it. */}
          <button onClick={() => createPage(page.width, page.height, page.id)}>
            Insert blank page
          </button>
          <button disabled={pages.length <= 1} onClick={() => deletePages(page.id)}>
            Delete
          </button>
        </li>
      ))}
    </ol>
  )
}
```

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

| Organize Pages action    | @sdk-pdf/react                                                                                                                                                                                   |
| ------------------------ | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
| Rotate                   | `setPageRotation(pageIdOrIds, rotation)` sets an absolute angle; values outside `0..360` are normalized, so `-90` and `450` work, and angles that aren't quarter turns throw `InvalidInputError` |
| Rearrange                | `moveBefore(ids, beforePageId?)` (start when omitted) or `moveAfter(ids, afterPageId?)` (end when omitted); drag and drop is your UI                                                             |
| Insert blank pages       | `createPage(width, height, insertAfterPageId?)`: after the given page, or appended; the size in user-space units, with no defaults                                                               |
| Delete                   | `deletePages(pageIdOrIds)`                                                                                                                                                                       |
| Copy                     | `duplicatePage(sourcePageId, insertAfterPageId?)` is asynchronous and inserts after the source by default                                                                                        |
| Select all, bulk actions | Pass several ids in one call                                                                                                                                                                     |
| Import a PDF             | No equivalent                                                                                                                                                                                    |
| `extractPages`           | `prepareDocument({ pageIds })` from [`useDocumentSaving`](https://sdk.pdf.net/docs/v1/reference/useDocumentSaving) resolves with a new PDF of the listed pages, in the listed order                                 |
| Save, Save As            | Changes are part of the document; save with `prepareDocument()` or `downloadDocument()`                                                                                                          |

* **Undo.** Deleting, moving, or rotating several pages in one call is a single undo step, and an undo can restore a deleted page with its content; separate edits within 500 ms merge into one step.
* **No page events.** Page operations your code calls emit no events: run your follow-up where you call them, and watch [`useDocumentRevision`](https://sdk.pdf.net/docs/v1/reference/useDocumentRevision) for any change.
* **No dialog to open.** `openPageOrganizer()`, `isPageOrganizerOpen`, and the zoom slider settings become your own panel state and thumbnail size.

## Forms [#forms]

### Before: Syncfusion PDF Viewer [#before-syncfusion-pdf-viewer-7]

The `FormFields` module makes fields fillable. Field objects come from `formFieldCollections` or `retrieveFormFields()`, with `name`, `type`, `value`, and `isChecked`, and a changed field goes back through `updateFormFieldsValue`. With `enableFormFieldsValidation`, required fields are checked before printing and downloading:

```jsx
function Viewer() {
  const viewerRef = useRef(null)

  function prefill() {
    const viewer = viewerRef.current
    const field = viewer
      .retrieveFormFields()
      .find((field) => field.name === 'customer.name')
    if (field) {
      field.value = 'Jane Doe'
      viewer.updateFormFieldsValue(field)
    }
  }

  return (
    <PdfViewerComponent
      ref={viewerRef}
      resourceUrl={RESOURCE_URL}
      documentLoad={prefill}
      formFieldFocusOut={(args) => autosave(args.fieldName, args.value)}
      enableFormFieldsValidation={true}
      validateFormFields={(args) => showMissingFields(args.nonFillableFields)}
      style={{ height: '100vh' }}
    >
      <Inject services={[Toolbar, Navigation, FormFields]} />
    </PdfViewerComponent>
  )
}
```

### 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. Users fill fields in `Viewport` with nothing to build when the content policy allows interacting with forms. For form UI outside the main view, [`useWidgets`](https://sdk.pdf.net/docs/v1/reference/useWidgets) lists the widgets in reading order and [`useWidgetValue`](https://sdk.pdf.net/docs/v1/reference/useWidgetValue) binds one widget's value like a `useState` backed by the document:

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

// The counterpart of formFieldCollections, re-rendered as the widgets change.
function FormPanel({ savedValues }: { savedValues: Record<string, string> }) {
  const { widgets, isLoaded } = useWidgets()

  // The list is partial until every page containing widgets has been loaded.
  if (!isLoaded) return <p>Loading the form…</p>

  return (
    <form>
      {widgets.map((widget) => (
        <FormField
          key={widget.id}
          widget={widget}
          savedValue={savedValues[widget.fieldName]}
        />
      ))}
    </form>
  )
}

function FormField({ widget, savedValue }: { widget: Widget; savedValue?: string }) {
  switch (widget.widgetType) {
    case 'text':
      return <TextField widget={widget} savedValue={savedValue} />
    case 'checkbox':
      return <CheckboxField widget={widget} />
    default:
      return null
  }
}

function TextField({ widget, savedValue }: { widget: TextWidget; savedValue?: string }) {
  // Filling in the main view and undo update this value.
  const [value, setValue] = useWidgetValue(widget)

  return (
    <label>
      {widget.fieldName}
      <input value={value} onChange={(event) => setValue(event.currentTarget.value)} />
      {/* Replaces setting field.value and calling updateFormFieldsValue. */}
      {savedValue !== undefined && (
        <button type="button" onClick={() => setValue(savedValue)}>
          Fill in
        </button>
      )}
    </label>
  )
}

function CheckboxField({ widget }: { widget: CheckboxWidget }) {
  // isChecked becomes the boolean value.
  const [isChecked, setIsChecked] = useWidgetValue(widget)

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

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

| Concept       | Syncfusion PDF Viewer                                             | @sdk-pdf/react                                                                                                                                                                                                                                |
| ------------- | ----------------------------------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| Lookup        | `formFieldCollections`, `retrieveFormFields()`                    | `useWidgets()` (re-renders), or `await getWidgets()` from [`useWidgetsGetter`](https://sdk.pdf.net/docs/v1/reference/useWidgetsGetter) (one-time, loads every page with widgets first)                                                                           |
| Shared values | Radio buttons grouped by the same `name`                          | Every widget with the same field name shows and synchronizes the same value, radio groups included                                                                                                                                            |
| Values        | `field.value`, `field.isChecked`, then `updateFormFieldsValue`    | `useWidgetValue(widget)`: `string` for `text` and `choice`, `boolean` for `checkbox` and `radio`, option indices for `multiChoice`                                                                                                            |
| Clearing      | `clearFormFields()`, `resetFormFields()`                          | Set the empty value per widget with `useWidgetValue`; no reset to defaults                                                                                                                                                                    |
| Focus         | `focusFormField(name)`                                            | `scrollToBlock(widget.id)` from [`useScrollToBlock`](https://sdk.pdf.net/docs/v1/reference/useScrollToBlock) scrolls to the widget                                                                                                                               |
| Events        | `formFieldFocusOut`, `formFieldClick`, and the other field events | `widgetInput` from [`useViewportEvent`](https://sdk.pdf.net/docs/v1/reference/useViewportEvent) reports a value the user changed, and [`useFocusedWidgetId`](https://sdk.pdf.net/docs/v1/reference/useFocusedWidgetId) returns the widget being filled; `useWidgetValue` re-renders |
| Validation    | `isRequired`, `enableFormFieldsValidation`, `validateFormFields`  | No equivalent; validate in your own submit flow                                                                                                                                                                                               |
| Form data     | `importFormFields()`, `exportFormFields()` as JSON, FDF, or XFDF  | No equivalent; `prepareDocument()` produces the filled PDF                                                                                                                                                                                    |

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

* **Values are read and written per widget.** `useWidgetValue` binds one widget per hook call, and there is no setter by field name: to prefill a form, render a bound component per widget, like `TextField` above.
* **Field kinds.** The widget kinds are `text`, `choice`, `multiChoice`, `checkbox`, `radio`, `pushButton`, and `signature`. Syncfusion's password fields and initial fields have no matching kind.
* **Read-only forms.** `enableFormFields={false}` maps to a policy where `widgets` is `'readonly'`, such as `PDF_PRINTER`, or `'hidden'` to leave the widgets out of the view. To lock a submitted form instead, set `isReadOnly` on the widgets, as [Form designer](https://sdk.pdf.net/docs/v1/migration/from-syncfusion#form-designer) shows.
* **Date fields.** A text widget's `format` exposes Acrobat date formats as `FormatType.DateTime`, recognized without running scripts; see [`FormatType`](https://sdk.pdf.net/docs/v1/reference/FormatType).
* **Signatures.** `signature` widgets carry a read-only `isCryptoSigned`; the SDK never signs.

## Form designer [#form-designer]

### Before: Syncfusion PDF Viewer [#before-syncfusion-pdf-viewer-8]

The `FormDesigner` module adds the form designer toolbar and the `formDesigner` module. Fields are added at given bounds or placed by the user after `setFormFieldMode`, updated with settings objects, and deleted by object or id:

```jsx
function Designer() {
  const viewerRef = useRef(null)

  function addNameField() {
    viewerRef.current?.formDesigner.addFormField('Textbox', {
      name: 'customer.name',
      pageNumber: 1,
      bounds: { X: 100, Y: 150, Width: 200, Height: 24 },
      maxLength: 40,
      isRequired: true,
    })
  }

  function lockForm() {
    const viewer = viewerRef.current
    for (const field of viewer.retrieveFormFields()) {
      viewer.formDesigner.updateFormField(field, { isReadOnly: true })
    }
  }

  function deleteField(name) {
    const viewer = viewerRef.current
    const field = viewer.retrieveFormFields().find((field) => field.name === name)
    if (field) viewer.formDesigner.deleteFormField(field)
  }

  return (
    <>
      <button onClick={addNameField}>Add name field</button>
      {/* The next click on a page places a text box. */}
      <button onClick={() => viewerRef.current?.formDesigner.setFormFieldMode('Textbox')}>
        Place text box
      </button>
      <button onClick={lockForm}>Lock form</button>
      <button onClick={() => deleteField('customer.name')}>Delete name field</button>
      <PdfViewerComponent
        ref={viewerRef}
        resourceUrl={RESOURCE_URL}
        designerMode={true}
        formFieldAdd={(args) => trackFieldAdded(args.pageIndex)}
        style={{ height: '100vh' }}
      >
        <Inject services={[Toolbar, Navigation, FormFields, FormDesigner]} />
      </PdfViewerComponent>
    </>
  )
}
```

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

[`useWidgetsCreate`](https://sdk.pdf.net/docs/v1/reference/useWidgetsCreate) creates widgets, [`useSetWidgetProps`](https://sdk.pdf.net/docs/v1/reference/useSetWidgetProps) updates them by id, and [`useBlockDelete`](https://sdk.pdf.net/docs/v1/reference/useBlockDelete) deletes them:

```tsx
import {
  useBlockDelete,
  useSetWidgetProps,
  useWidgetsCreate,
  useWidgetsGetter,
} from '@sdk-pdf/react'

function useFormDesigner() {
  const createWidgets = useWidgetsCreate()
  const setWidgetProps = useSetWidgetProps()
  const deleteBlocks = useBlockDelete()
  const getWidgets = useWidgetsGetter()

  // Replaces formDesigner.addFormField('Textbox', { name, bounds, maxLength }).
  async function addNameField(pageId: string) {
    const [widget] = await createWidgets({
      pageId,
      widgetType: 'text',
      // User-space units: the origin is at the bottom-left corner of the page.
      x: 72,
      y: 700,
      width: 180,
      height: 24,
      fieldName: 'customer.name',
      maxLength: 40,
    })
    return widget
  }

  // Replaces updateFormField(field, { isReadOnly: true }) for every field, in one call.
  async function lockForm() {
    const widgets = await getWidgets()
    // excludeFromHistory keeps undo from unlocking the submitted form.
    // The props apply at once; the promise resolves when the widgets show them.
    await setWidgetProps(
      widgets.map((widget) => widget.id),
      { isReadOnly: true },
      { excludeFromHistory: true }
    )
  }

  // Replaces deleteFormField(field): same-named widgets act as one field, so delete them
  // together.
  async function deleteField(fieldName: string) {
    const widgets = await getWidgets()
    const widgetIds = widgets
      .filter((widget) => widget.fieldName === fieldName)
      .map((widget) => widget.id)
    if (widgetIds.length > 0) deleteBlocks(widgetIds)
  }

  return { addNameField, lockForm, deleteField }
}
```

To let users place a widget by clicking the page, like `setFormFieldMode`, switch the interaction tool to `{ type: 'addWidget', widgetProps }` with [`useSetInteractionMode`](https://sdk.pdf.net/docs/v1/reference/useSetInteractionMode); pass a policy where widgets are editable, such as `PDF_EDITOR`, with the tool.

### From field settings to widget props [#from-field-settings-to-widget-props]

| Syncfusion field settings                                      | @sdk-pdf/react widget props                                                              |
| -------------------------------------------------------------- | ---------------------------------------------------------------------------------------- |
| `Textbox`, `CheckBox`, `RadioButton`                           | `widgetType` `'text'`, `'checkbox'`, `'radio'`                                           |
| `DropDown`, `ListBox`                                          | `'choice'` (with `isCombo` for a dropdown) or `'multiChoice'`                            |
| `SignatureField`                                               | `'signature'`                                                                            |
| `Password`, `InitialField`                                     | No equivalent                                                                            |
| `name`                                                         | `fieldName`; widgets sharing it share the value                                          |
| `pageNumber` and `bounds`                                      | `pageId` and `x`, `y`, `width`, `height` in user-space units from the bottom-left corner |
| `isReadOnly`, `maxLength`, `isMultiline`                       | `isReadOnly`, `maxLength`, `isMultiline`                                                 |
| `backgroundColor`, `borderColor`, `thickness`                  | `fillColor`, `borderColor`, `borderWidth`                                                |
| `color`, `fontSize`, `alignment`                               | `textColor`, `fontSize`, `horizontalAlign`                                               |
| `options` with `itemName` and `itemValue`                      | `options` with `label` and `value`                                                       |
| `isRequired`, `tooltip`, `visibility`, `isPrint`, `fontFamily` | No equivalent                                                                            |

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

* **Convert stored positions.** `@sdk-pdf/react` widget positions are user-space units, 1/72 of an inch, with the origin at the bottom-left corner of the page. Check placement on a test document before converting `bounds` you stored for Syncfusion.
* **Designer UI.** The form designer toolbar and its properties panel become your own controls. [`useSelectBlocks`](https://sdk.pdf.net/docs/v1/reference/useSelectBlocks) replaces `selectFormField` and `clearSelection` (`selectBlocks([])`), [`useSelectedBlocks`](https://sdk.pdf.net/docs/v1/reference/useSelectedBlocks) returns the selected widgets for a properties panel, `setWidgetProps` from `useSetWidgetProps` changes their props, such as `isMultiline` and `format`, [`useSelectionAnnotationProps`](https://sdk.pdf.net/docs/v1/reference/useSelectionAnnotationProps) changes their border color and width, and [`useDuplicateSelection`](https://sdk.pdf.net/docs/v1/reference/useDuplicateSelection) copies them under new field names.
* **Replacing a kind.** A widget's kind can't be changed through its props: create the new widget with the same field name and delete the old one, as [Working with forms](https://sdk.pdf.net/docs/v1/guides/working-with-forms#replacing-widgets) shows.
* **Undo.** Creating widgets is an undo step, and so is changing their props, unless you pass `{ excludeFromHistory: true }` to `setWidgetProps`.
* **Design and fill modes.** `designerMode` and `enableFormDesigner` map to the content policy's `widgets` level: `'interactive'` (as in `PDF_VIEWER`) lets users fill fields, and `'editable'` (as in `PDF_EDITOR`) makes widgets editable, as the `addWidget` tool needs. The level can be a function of the widget id, such as `widgets: (blockId) => (lockedIds.has(blockId) ? 'interactive' : 'editable')`. `isReadOnly` restricts filling a field, not authoring the widget.

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

### Before: Syncfusion PDF Viewer [#before-syncfusion-pdf-viewer-9]

`interactionMode` switches between text selection (the default) and panning, `annotation.setAnnotationMode` arms an annotation tool for the next interaction, and the `enable*` props remove the editing features for read-only users:

```jsx
function Viewer({ isReadOnly }) {
  const viewerRef = useRef(null)

  return (
    <>
      <button onClick={() => (viewerRef.current.interactionMode = 'TextSelection')}>
        Select text
      </button>
      <button onClick={() => (viewerRef.current.interactionMode = 'Pan')}>Pan</button>
      <button onClick={() => viewerRef.current?.annotation.setAnnotationMode('FreeText')}>
        Free text
      </button>
      <button onClick={() => viewerRef.current?.annotation.setAnnotationMode('None')}>
        Stop
      </button>
      <PdfViewerComponent
        ref={viewerRef}
        resourceUrl={RESOURCE_URL}
        interactionMode="TextSelection"
        enableAnnotation={!isReadOnly}
        enableFormDesigner={!isReadOnly}
        enablePageOrganizer={!isReadOnly}
        style={{ height: '100vh' }}
      >
        <Inject
          services={[
            Toolbar,
            Navigation,
            TextSelection,
            Annotation,
            FormFields,
            FormDesigner,
            PageOrganizer,
          ]}
        />
      </PdfViewerComponent>
    </>
  )
}
```

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

The **interaction mode** combines a **content policy** (what may change) and an **interaction tool** (what a click, tap, or drag 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,
  useInteractionTool,
  useSetInteractionMode,
} from '@sdk-pdf/react'

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

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

  return (
    <div role="toolbar">
      {/* Like interactionMode="TextSelection" with the editing modules off. */}
      <button
        aria-pressed={interactionTool.type === 'textSelect'}
        onClick={() =>
          setInteractionMode(textSelectTool, CONTENT_POLICY_PRESET.PDF_VIEWER)
        }
      >
        Read
      </button>
      {/* Select, move, and resize blocks; page text is editable in place. */}
      <button
        aria-pressed={interactionTool.type === 'select'}
        onClick={() => setInteractionMode(selectTool, CONTENT_POLICY_PRESET.PDF_EDITOR)}
      >
        Edit
      </button>
      {/* The nearest tool to free text: a new paragraph at the click position. */}
      <button
        aria-pressed={interactionTool.type === 'addText'}
        onClick={() => setInteractionMode(addTextTool, CONTENT_POLICY_PRESET.PDF_EDITOR)}
      >
        Add text
      </button>
    </div>
  )
}
```

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

| Syncfusion PDF Viewer                                                                           | @sdk-pdf/react                                                                                                                                                                                |
| ----------------------------------------------------------------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `interactionMode="TextSelection"`                                                               | `{ type: 'textSelect' }`: native text selection for reading and copying, with read-only text.                                                                                                 |
| `interactionMode="Pan"`                                                                         | No tool: `Viewport` scrolls and zooms on its own.                                                                                                                                             |
| `setAnnotationMode('None')`                                                                     | Back to `{ type: 'select' }` or `{ type: 'textSelect' }`.                                                                                                                                     |
| `setAnnotationMode('FreeText')`                                                                 | The nearest tool is `{ type: 'addText' }`, which creates a text paragraph in editable text.                                                                                                   |
| Image stamps                                                                                    | `{ type: 'addImage', image, width?, height? }` with `await stageImage(blob)` places a stamp annotation. See [Annotations](https://sdk.pdf.net/docs/v1/migration/from-syncfusion#annotations).                                                      |
| `formDesigner.setFormFieldMode(type)`                                                           | `{ type: 'addWidget', widgetProps }` with editable widgets.                                                                                                                                   |
| `Ink`                                                                                           | No drawing tool. [`useInkAnnotationCreate`](https://sdk.pdf.net/docs/v1/reference/useInkAnnotationCreate) creates an ink annotation from strokes your code collects.                                             |
| Text markup, shapes, measurement, `StickyNotes`, `HandWrittenSignature`, `Initial`, `Redaction` | No equivalent.                                                                                                                                                                                |
| `enableAnnotation`, `enableFormDesigner`, `enableFormFields`, `designerMode`                    | The `annotations` and `widgets` levels of the content policy: `PDF_EDITOR` makes everything editable, `PDF_VIEWER` lets users fill forms, `PDF_PRINTER` makes everything read-only but links. |

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

* **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 permissions.** A custom policy is a plain object with `text`, `annotations`, `links`, and `widgets` levels, finer than turning modules on and off; [`useSetInteractionMode`](https://sdk.pdf.net/docs/v1/reference/useSetInteractionMode) lists them. Each level can also be `'hidden'`, which leaves that content out of the view, or a function of the block id for per-block levels. There is no hook to read the policy back: keep the policy you set in your own state to show or hide editing UI.
* **Editing existing text is new.** Under `PDF_EDITOR`, page text is editable in place and `Viewport` handles typing. [`useSelectionTextStyle`](https://sdk.pdf.net/docs/v1/reference/useSelectionTextStyle) drives a style toolbar (font, size, color, bold, italic, alignment, spacing), and [`useFocusSelection`](https://sdk.pdf.net/docs/v1/reference/useFocusSelection) returns the focus after a toolbar click.
* **Local mode.** Switching the mode affects this client only.
* **Keyboard shortcuts.** Wire the shortcuts you need, such as delete with [`useDeleteSelection`](https://sdk.pdf.net/docs/v1/reference/useDeleteSelection) or arrow-key nudging with [`useMoveSelection`](https://sdk.pdf.net/docs/v1/reference/useMoveSelection), through [`useHotkeys`](https://sdk.pdf.net/docs/v1/reference/useHotkeys) with a memoized mapping. Wrap block shortcuts in [`createUnfocusedHandler`](https://sdk.pdf.net/docs/v1/reference/createUnfocusedHandler), so they stay inactive while the user types in a paragraph or a form field.

## Annotations [#annotations]

### Before: Syncfusion PDF Viewer [#before-syncfusion-pdf-viewer-10]

The `Annotation` module adds the annotation toolbar and the `annotation` module: text markup, shapes, measurements, free text, ink, stamps, and sticky notes, with comments, default settings such as the author, and JSON or XFDF export:

```jsx
function ReviewTools() {
  const viewerRef = useRef(null)

  function addHighlight() {
    viewerRef.current?.annotation.addAnnotation('Highlight', {
      bounds: [{ x: 97, y: 110, width: 350, height: 14 }],
      pageNumber: 1,
    })
  }

  function recolorMine() {
    const viewer = viewerRef.current
    for (const annotation of viewer.annotationCollection) {
      if (annotation.author === 'Reviewer') {
        annotation.color = '#ff0000'
        viewer.annotation.editAnnotation(annotation)
      }
    }
  }

  async function storeAnnotations() {
    // Annotations are stored apart from the PDF.
    await saveAnnotations(await viewerRef.current.exportAnnotationsAsObject())
  }

  return (
    <>
      <button onClick={addHighlight}>Highlight</button>
      <button onClick={recolorMine}>Recolor mine</button>
      <button onClick={storeAnnotations}>Store annotations</button>
      <PdfViewerComponent
        ref={viewerRef}
        resourceUrl={RESOURCE_URL}
        annotationSettings={{ author: 'Reviewer' }}
        annotationAdd={(args) => trackAnnotation(args.annotationType, args.pageIndex)}
        style={{ height: '100vh' }}
      >
        <Inject services={[Toolbar, Navigation, TextSelection, Annotation]} />
      </PdfViewerComponent>
    </>
  )
}
```

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

Existing annotations display in `Viewport` and in `PageCanvas`, and selected shapes, lines, ink, and widgets can be restyled with [`useSelectionAnnotationProps`](https://sdk.pdf.net/docs/v1/reference/useSelectionAnnotationProps). Images become stamp annotations, placed with the `addImage` tool, which takes an image staged with [`stageImage`](https://sdk.pdf.net/docs/v1/reference/stageImage):

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

function AnnotationToolbar() {
  const { isApplicable, borderColor, borderWidth } = useSelectionAnnotationProps()
  const setInteractionMode = useSetInteractionMode()

  async function placeImage(file: File) {
    // Replaces an image stamp: the next click on a page places the image as a stamp
    // annotation.
    const image = await stageImage(file)
    // Placing images is editing, so set the policy with the tool. The width is in
    // user-space units.
    setInteractionMode(
      { type: 'addImage', image, width: 150 },
      CONTENT_POLICY_PRESET.PDF_EDITOR
    )
  }

  return (
    <div role="toolbar">
      <input
        type="file"
        accept="image/*"
        onChange={(event) => {
          const file = event.currentTarget.files?.[0]
          if (file) placeImage(file).catch(showError)
        }}
      />
      {/* Like the stroke color and thickness pickers, for the selected annotations. */}
      {isApplicable && (
        <>
          <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]

* **Few annotation kinds.** Creating text markup, shapes, measurements, free text, stamps other than images, sticky notes, handwritten signatures, and redactions has no equivalent, from the UI or from code (`addAnnotation`). From code, [`useStampAnnotationCreate`](https://sdk.pdf.net/docs/v1/reference/useStampAnnotationCreate) places an image stamp at a position on a page, and [`useInkAnnotationCreate`](https://sdk.pdf.net/docs/v1/reference/useInkAnnotationCreate) creates ink from strokes your code collects; there is no ink drawing tool.
* **Restyling is the border.** `useSelectionAnnotationProps` changes the border color, with the opacity in its alpha channel, and the border width of the selected shapes, lines, ink, and widgets, and skips the rest of the selection. `editAnnotation` with other properties has no equivalent.
* **Images.** `stageImage(blob)` accepts any image format the browser decodes, and SVGs are rasterized when the PDF is saved. The size goes on the tool or on `useStampAnnotationCreate`: `width` and `height` in user-space units, a single dimension keeping the aspect ratio. Without a size, the image's natural size is used, fit down to the page.
* **No annotation data.** `annotationCollection`, the annotation events, the comments panel, `annotationSettings` (author, locking, `skipPrint`, `skipDownload`), and JSON or XFDF import and export have no equivalent. [`useDocumentRevision`](https://sdk.pdf.net/docs/v1/reference/useDocumentRevision) changes on any edit, and `addToolApplied` from [`useViewportEvent`](https://sdk.pdf.net/docs/v1/reference/useViewportEvent) reports a stamp the user placed.
* **Hiding annotations in previews.** `PageCanvas` draws annotations unless `hideAnnotations` is set.

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

### Before: Syncfusion PDF Viewer [#before-syncfusion-pdf-viewer-11]

`undo()` and `redo()` revert and reapply annotation edits, from the toolbar's undo and redo tools, the keyboard, or code. `isDocumentEdited` tells whether the document was edited, such as for a warning before leaving the page:

```jsx
function Viewer() {
  const viewerRef = useRef(null)

  useEffect(() => {
    function warnIfEdited(event) {
      if (viewerRef.current?.isDocumentEdited) event.preventDefault()
    }
    window.addEventListener('beforeunload', warnIfEdited)
    return () => window.removeEventListener('beforeunload', warnIfEdited)
  }, [])

  return (
    <>
      <button onClick={() => viewerRef.current?.undo()}>Undo</button>
      <button onClick={() => viewerRef.current?.redo()}>Redo</button>
      <PdfViewerComponent
        ref={viewerRef}
        resourceUrl={RESOURCE_URL}
        style={{ height: '100vh' }}
      >
        <Inject services={[Toolbar, Navigation, Annotation, FormFields]} />
      </PdfViewerComponent>
    </>
  )
}
```

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

[`useUndoRedo`](https://sdk.pdf.net/docs/v1/reference/useUndoRedo) returns the commands and their availability, and [`useHotkeys`](https://sdk.pdf.net/docs/v1/reference/useHotkeys) wires the shortcuts:

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

function HistoryControls() {
  // canUndo and canRedo re-render the component as the history changes.
  const { undo, redo, canUndo, canRedo } = useUndoRedo()

  // The mapping must be memoized, or the listener re-installs on every render.
  useHotkeys(useMemo(() => ({ 'Mod-z': undo, 'Shift-Mod-z': redo }), [undo, redo]))

  // Events report what the user does in the viewport, such as moving blocks by dragging.
  useViewportEvent('blocksDragEnd', ({ blockIds }) => trackMove(blockIds.length))

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

`isDocumentEdited` becomes 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 unsaved-changes indicator and the leave-page warning live with the save button in [Saving and downloading](https://sdk.pdf.net/docs/v1/migration/from-syncfusion#saving-and-downloading).

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

* **One history for the whole document,** not only for annotations: deleting, moving, or rotating pages are undo steps, widget property changes are undo steps unless excluded, page text edits are undo steps, and form values follow undo and redo.
* **Local and merged.** Undo reverts the local user's changes, and 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.
* **Shortcuts.** `Mod` is Cmd on macOS and Ctrl elsewhere; a handler that returns `false` lets the event reach the browser, and a falsy value instead of the mapping disables the shortcuts, such as while a modal is open.
* **Events for user actions only.** [`useViewportEvent`](https://sdk.pdf.net/docs/v1/reference/useViewportEvent) reports what users do in the main view, such as `textEditorInput`, `addToolApplied`, `blocksDragEnd`, `blockResizeDragEnd`, and `widgetInput`. Page operations, undo, and redo that your code calls emit no events: track them where you call them, and watch `useDocumentRevision` for any change.

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

### Before: Syncfusion PDF Viewer [#before-syncfusion-pdf-viewer-12]

`saveAsBlob()` resolves with the edited PDF as a `Blob`, with annotations, form values, and page changes applied, and `download()` saves it on the user's device under `downloadFileName`. `downloadStart` can cancel a download, and `annotationSettings.skipDownload` leaves annotations out. Server-backed integrations can save on the web service in its `Download` action instead:

```jsx
function Viewer() {
  const viewerRef = useRef(null)

  async function upload() {
    const blob = await viewerRef.current.saveAsBlob()
    const body = new FormData()
    body.append('file', blob, 'contract.pdf')
    await fetch('/api/documents', { method: 'POST', body })
  }

  return (
    <>
      <button onClick={upload}>Save</button>
      <button onClick={() => viewerRef.current?.download()}>Download</button>
      <PdfViewerComponent
        ref={viewerRef}
        resourceUrl={RESOURCE_URL}
        downloadStart={(args) => {
          if (!canDownload()) args.cancel = true
        }}
        style={{ height: '100vh' }}
      >
        <Inject services={[Toolbar, Navigation, Annotation, FormFields]} />
      </PdfViewerComponent>
    </>
  )
}
```

### 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. The component that saves also owns the saved revision, so the save, the indicator, and the leave-page warning compare against the same value. Render it only once the document is ready, so its first revision is the loaded document's:

```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 isDocumentEdited: undoing back to the saved state clears it too.
  const hasUnsavedChanges = revision !== savedRevision

  // Warn before leaving the page with unsaved changes.
  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()
      // Replaces saveAsBlob(). The bytes go to fetch, so don't hand out the loaded
      // document's buffer.
      const bytes = await prepareDocument({ returnOriginalIfUnchanged: false })

      const body = new FormData()
      body.append(
        'file',
        new Blob([bytes as BlobPart], { type: 'application/pdf' }),
        'contract.pdf'
      )
      const response = await fetch('/api/documents', { method: 'POST', body })
      // A failed upload keeps the unsaved-changes indicator.
      if (!response.ok) return

      setSavedRevision(startRevision)
    } finally {
      isSavingRef.current = false
    }
  }

  return (
    <div role="toolbar">
      <button onClick={() => void upload()}>Save</button>
      <button onClick={() => void downloadDocument()}>Download</button>
      {hasUnsavedChanges && <span>Unsaved changes</span>}
    </div>
  )
}
```

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

* **One artifact.** Where Syncfusion lets you store annotations and form data apart from the PDF (JSON, XFDF, or FDF), `@sdk-pdf/react` saves the complete PDF: `prepareDocument()` resolves with its bytes as a `Uint8Array`, and `downloadDocument()` saves the same bytes on the user's device.
* **A Uint8Array, not a Blob.** Wrap the bytes in a `Blob` for `FormData`, as above, or send them as the request body.
* **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. The other option, `pageIds`, saves only the listed pages, as `extractPages` does. There is no download file name, flattening, or option to leave annotations out.
* **No download events.** Instead of cancelling in `downloadStart` or reacting in `downloadEnd`, run your checks before calling `downloadDocument()` and your follow-up after awaiting it. Both functions throw `DocumentNotLoadedError` when no document is loaded.
* **Server-backed saving.** The web service's `Download` action has no counterpart: post the bytes from `prepareDocument()` to your own endpoint, which stores them as it did before.

## Collaboration [#collaboration]

Syncfusion's collaborative editing connects the viewer to a Collaboration Server through `@syncfusion/ej2-collaborator` and a `PdfViewerAdapter`, with Redis behind the server, and shares annotations, form field changes, and Organize Pages operations between users.

Real-time collaboration in `@sdk-pdf/react` is coming soon. The adapter and the Collaboration Server setup don't carry over.

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

### Before: Syncfusion PDF Viewer [#before-syncfusion-pdf-viewer-13]

Load failures arrive through `documentLoadFailed`, and the viewer shows errors in its own message box, which `showNotificationDialog={false}` turns off; `showNotificationPopup(message)` shows a message in it:

```jsx
<PdfViewerComponent
  ref={viewerRef}
  resourceUrl={RESOURCE_URL}
  // Show your own error UI instead of the built-in message box.
  showNotificationDialog={false}
  documentLoadFailed={(args) => showError(`Couldn't open ${args.documentName}`)}
  style={{ height: '100vh' }}
>
  <Inject services={[Toolbar, Navigation]} />
</PdfViewerComponent>
```

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

Failures are typed errors from the call that failed, and recoverable problems 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 useReportSdkWarnings() {
  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.message, logMessage.data)
      }
    })
    return () => {
      unsubscribe()
    }
  }, [])
}

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

  return async (data: Uint8Array) => {
    try {
      // Pass a copy and keep `data` for a password retry.
      await loadPdf(data.slice())
    } catch (error) {
      // Replaces documentLoadFailed for this load.
      if (error instanceof InvalidPasswordError) askForPassword()
      else showError(error)
    }
  }
}
```

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

* **Error classes.** `DocumentNotLoadedError`, `InvalidInputError`, `InvalidPasswordError`, `DocumentFormatError` (malformed beyond what the SDK can work around), `NotSupportedError` (a PDF feature not supported yet), and `UninitializedError`, all extending `GenericError` with a static `message` and an optional `data` object. 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. `Viewport` and `PageCanvas` are the exception: they throw `DocumentNotLoadedError` when rendered without a document.
* **No dialogs.** The SDK shows no message box, so `showNotificationDialog` and `showNotificationPopup` have no counterpart: error UI is yours.
* **Logs are opt-in.** Each message carries `level` (`LogLevel.Debug`, `Info`, `Warning`, `Error`), `reason`, a static `message`, `data`, and `stack`. Filter by `level` in your subscriber.

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

### Before: Syncfusion PDF Viewer [#before-syncfusion-pdf-viewer-14]

`unload()` closes the document and fires `documentUnload`, and `destroy()` releases the instance's resources. By default the viewer keeps session data, such as form fields, annotations, and signatures, in the browser's `sessionStorage`; with `enableLocalStorage` it keeps them in memory, and the instance should be destroyed when it's no longer needed:

```jsx
<PdfViewerComponent
  ref={viewerRef}
  resourceUrl={RESOURCE_URL}
  enableLocalStorage={true}
  documentUnload={() => clearDocumentState()}
  style={{ height: '100vh' }}
>
  <Inject services={[Toolbar, Navigation]} />
</PdfViewerComponent>
```

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

| Concern            | @sdk-pdf/react                                                                                                                                                                             |
| ------------------ | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
| Closing a document | `resetPdf()` from [`useResetPdf`](https://sdk.pdf.net/docs/v1/reference/useResetPdf) unloads the document and invalidates its ids, snapshots, and revisions; run your `documentUnload` code after calling it. |
| Event listeners    | [`useViewportEvent`](https://sdk.pdf.net/docs/v1/reference/useViewportEvent) subscribes for the component's lifetime, with a callback that doesn't need memoizing.                                            |
| Keyboard listeners | [`useHotkeys`](https://sdk.pdf.net/docs/v1/reference/useHotkeys) installs window-level shortcuts; pass a falsy value instead of the mapping to disable them.                                                  |
| Log subscriptions  | `logMessages.on()` returns an unsubscribe function: call it in the effect cleanup, as in [Errors and logging](https://sdk.pdf.net/docs/v1/migration/from-syncfusion#errors-and-logging).                                                        |
| Buffers            | Don't reuse a buffer passed to `loadPdf`; copy it first.                                                                                                                                   |

There is no `destroy()` to call and no storage setting: the document lives in `PdfProviders` in your React tree.

## Text extraction and search [#text-extraction-and-search]

The Syncfusion viewer extracts text with `extractText(pageIndex, options)`, which resolves with `textData` and `pageText`, and searches with the `TextSearch` module (`searchText`, `findText`). In `@sdk-pdf/react`, `getParagraphsByPage(pageId)` from [`useProgrammaticEditor`](https://sdk.pdf.net/docs/v1/reference/useProgrammaticEditor) resolves with paragraph entries in content order: `id`, `content`, `pageNumber`, the box `bounds` (`{ x, y, width, height }`), and the paragraph layout, such as `textAlign` and `lineHeight`:

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

function useExtractPageText() {
  const pages = usePages()
  const { getParagraphsByPage } = useProgrammaticEditor()

  // Replaces extractText(pageIndex, ExtractTextOption.TextOnly): the index is zero-based
  // in both.
  return async (pageIndex: number) => {
    const page = pages[pageIndex]
    if (!page) return ''

    const paragraphs = await getParagraphsByPage(page.id)
    return paragraphs.map((paragraph) => paragraph.content).join('\n\n')
  }
}
```

The bounds are user-space units with the origin at the bottom-left corner of the page.

[`useDocumentSearch`](https://sdk.pdf.net/docs/v1/reference/useDocumentSearch) replaces the `TextSearch` module: set the query and the options, and the SDK finds the matches, highlights them on the pages, and scrolls to the active one:

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

function SearchBar() {
  const {
    criteria,
    matches,
    activeMatchIndex,
    status,
    setSearchCriteria,
    goToNextMatch,
    goToPreviousMatch,
  } = useDocumentSearch()

  return (
    <div role="search">
      {/* Replaces searchText(text, isMatchCase); an empty query replaces
         cancelTextSearch(). */}
      <input
        value={criteria.query}
        onChange={(event) =>
          setSearchCriteria({ ...criteria, query: event.currentTarget.value })
        }
      />
      <label>
        <input
          type="checkbox"
          checked={criteria.isCaseSensitive}
          onChange={(event) =>
            setSearchCriteria({
              ...criteria,
              isCaseSensitive: event.currentTarget.checked,
            })
          }
        />
        Match case
      </label>
      <span>
        {status === 'searching'
          ? 'Searching…'
          : `${activeMatchIndex + 1} of ${matches.length}`}
      </span>
      {/* Replace searchPrevious() and searchNext(). */}
      <button disabled={matches.length === 0} onClick={goToPreviousMatch}>
        Previous
      </button>
      <button disabled={matches.length === 0} onClick={goToNextMatch}>
        Next
      </button>
    </div>
  )
}
```

* **Options.** `isCaseSensitive` replaces `isMatchCase`, and `isWholeWord` matches whole words only.
* **Matches.** Each match carries `id`, `pageId`, `paragraphId`, and the matched `text`, in document order; `goToMatch(match.id)` activates one from a results list. A changed query is searched once typing pauses, and `status` turns `'complete'` once the whole document is searched, in place of `textSearchComplete`. Matches carry no bounds, so `findText` has no counterpart.
* **No selection API.** There is no API to read the user's text selection. To jump to a paragraph you located yourself, call `scrollToBlock(paragraphId)` from [`useScrollToBlock`](https://sdk.pdf.net/docs/v1/reference/useScrollToBlock).

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

Plan around these before you migrate:

| Syncfusion PDF Viewer feature                                                                            | In @sdk-pdf/react                                                                                                                                                   |
| -------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| Server-backed mode (`serviceUrl` and the web service)                                                    | No equivalent; the SDK runs in the browser, and your server keeps serving and storing the bytes                                                                     |
| Printing (`Print`)                                                                                       | No print API; `PageCanvas` can render pages for your own print layout, but it draws the pages as loaded from the file, without the session's edits                  |
| Bookmarks (`BookmarkView`), hyperlink events                                                             | No equivalent                                                                                                                                                       |
| Text markup, shape, measurement, free text, and sticky note annotations; ink drawing; the comments panel | No equivalent; existing shapes, lines, and ink display, their border color and width can be changed, and ink can be created from code with `useInkAnnotationCreate` |
| Dynamic, sign here, standard business, and text stamps                                                   | No equivalent; images are placed with the `addImage` tool                                                                                                           |
| Handwritten signatures, initials, digital signatures, redaction                                          | No equivalent; the SDK never signs                                                                                                                                  |
| Annotation and form data import and export (JSON, XFDF, FDF), flattening, `skipDownload`                 | No equivalent                                                                                                                                                       |
| Resetting form fields to their default values (`resetFormFields`)                                        | No equivalent; set each widget's value with `useWidgetValue`                                                                                                        |
| Form validation (`isRequired`, `validateFormFields`), password fields, initial fields, field tooltips    | No equivalent                                                                                                                                                       |
| Importing pages from another PDF                                                                         | No equivalent                                                                                                                                                       |
| Search hit bounds (`findText`)                                                                           | No equivalent; `useDocumentSearch` highlights the matches itself                                                                                                    |
| Coordinate conversion (`convertClientPointToPagePoint`, `convertPagePointToClientPoint`)                 | No equivalent                                                                                                                                                       |
| Fit to page on load (`zoomMode="FitToPage"`)                                                             | Fits are one-time calculations; see [Zoom and fit modes](https://sdk.pdf.net/docs/v1/migration/from-syncfusion#zoom-and-fit-modes)                                                                                       |
| Download events and file name (`downloadStart`, `downloadEnd`, `downloadFileName`)                       | No equivalent                                                                                                                                                       |
| Built-in toolbars, context menu, dialogs, theme packages, localization (`locale`)                        | The UI is yours                                                                                                                                                     |

