# Next.js, Nuxt, SvelteKit and Astro: mount on the client, tear down cleanly

The package imports cleanly on the server, but a viewer is browser-only. Render the component on the client alone, keep `init()` in one module, and release both the viewer and the document on unmount.

- Level: Frameworks
- Uses: `Kernstra.init()`, `dynamic(…, { ssr: false })`, `.client.vue`, `viewer.destroy()`, `doc.close()`

Nothing touches `window` until `Kernstra.init()` or a viewer runs, so importing the package on the server is fine. What must stay on the client is the module whose top level calls `init()` and the component that builds the viewer.

## What you get

- No viewer is built during server rendering.
- `Kernstra.init()` is idempotent: a second call with the same options gets the same promise back, so a remount or hot reload needs no guard.
- The viewer and the document are released when the person leaves.

## 1. One module owns init

Every component calls `initKernstra()` from this module. A second `init()` call with the same options gets the same promise back (a remount, a new `pdfUrl`, React's StrictMode); a call with other options rejects with `INVALID_ARGUMENT`, which is why one module holds them. It only defines a function, so importing it from server-rendered code runs nothing. Read the key the way your framework exposes variables to the browser: `import.meta.env.KERNSTRA_LICENSE` with Vite, as here (add `envPrefix: ['VITE_', 'KERNSTRA_']`), `process.env.KERNSTRA_LICENSE` in Next.js, and in Nuxt a composable instead of this module; the steps below show each.

`kernstra.ts`

```ts
import { Kernstra } from '@kernstra/pdf-web-sdk';

export const initKernstra = () =>
  Kernstra.init({ license: import.meta.env.KERNSTRA_LICENSE });
```

## 2. Next.js: load the component with ssr: false

Load the component with `ssr: false`, and call `initKernstra()` inside an effect, never during render. In the App Router, `ssr: false` is allowed only in a client component: put the `dynamic()` call in a file that starts with `'use client'` and render it from the page. `PdfEditor` is the React integration. In `kernstra.ts` read `process.env.KERNSTRA_LICENSE`, exposed to the browser by `env: { KERNSTRA_LICENSE: process.env.KERNSTRA_LICENSE }` in `next.config.ts`: the key ships to the browser by design. For the render worker, put `render.worker.js` under `public/` and pass `workerUrl: '/render.worker.js'`.

`app/report/PdfEditorClient.tsx`

```tsx
'use client';
import dynamic from 'next/dynamic';

// Render <PdfEditorClient pdfUrl="/report.pdf" /> from app/report/page.tsx.
export const PdfEditorClient = dynamic(() => import('./PdfEditor'), { ssr: false });
```

## 3. Nuxt: .client.vue or <ClientOnly>

Give the component the `.client.vue` suffix (or wrap a plain component in `<ClientOnly>`), and keep the Vue integration's `onMounted` / `onBeforeUnmount` pair. In Nuxt the module that owns `init` is a composable, `app/composables/useKernstra.ts`, which reads the key with `useRuntimeConfig().public.kernstraLicense`, filled from `NUXT_PUBLIC_KERNSTRA_LICENSE`. Call it in setup and await what it returns in `onMounted`.

`app/pages/report.vue`

```vue
<template>
  <PdfEditor pdf-url="/report.pdf" />
</template>
```

## 4. SvelteKit: key the component by the route

`onMount` never runs on the server, so the Svelte component needs no wrapper. Key it by the route parameter so a client navigation to another id mounts a fresh viewer. SvelteKit 3 replaced the `$lib` alias with `#lib`.

`src/routes/documents/[id]/+page.svelte`

```svelte
<script lang="ts">
  import { page } from '$app/state';
  // SvelteKit 2: '$lib/PdfEditor.svelte'
  import PdfEditor from '#lib/PdfEditor.svelte';
</script>

{#key page.params.id}
  <PdfEditor pdfUrl={`/api/documents/${page.params.id}.pdf`} />
{/key}
```

## 5. Astro: a client-only island

In Astro, a framework component rendered with `client:only` is skipped on the server and hydrated on the client alone, which is the same job `ssr: false` does in Next.js. Use the React, Vue or Svelte integration as the island, with the matching value (`client:only="react"`, `"vue"` or `"svelte"`; a `.svelte` or `.vue` import keeps its extension). Astro passes only `PUBLIC_` variables to the browser, so expose the key in `astro.config.mjs` with `vite: { envPrefix: ['PUBLIC_', 'KERNSTRA_'] }` (keep `PUBLIC_`), or declare it with `astro:env` (`envField.string({ context: 'client', access: 'public' })`) and import it from `astro:env/client`.

`src/pages/report.astro`

```astro
---
import PdfEditor from '../components/PdfEditor';
---

<PdfEditor client:only="react" pdfUrl="/report.pdf" />
```

## 6. Tear down in the right order

`viewer.destroy()` removes everything the viewer added to the page but **does not close the underlying `PDFDocument`**: call `doc.close()` yourself, before or after, or the document's memory is never released. An effect canceled while its document is still opening (StrictMode's first run, a route change) closes the document it opened, since nothing will clean it up later; a loader placed by that run is removed in the cleanup too.

`PdfEditor.tsx`

```tsx
return () => {
  canceled = true;
  removeLoader();
  viewer?.destroy();
  void doc?.close();
};
```

## Gotchas

- A viewer destroyed before it is ready rejects `whenReady()` with `ABORTED`, which is no error to report.
- The `?url` import for the render worker is Vite's. A bundler without it (webpack 5, the Angular CLI, esbuild, Rollup) serves the file as a static asset instead.
- `Kernstra.events` is kept across `Kernstra.destroy()`: a handler registered at module level hears the next `init()`'s `initialized` without subscribing again.

## Related

- [React](https://kernstra.com/docs/web-sdk/guides/integrations#s-react)
- [Next.js](https://kernstra.com/docs/web-sdk/guides/integrations#s-next-js)
- [Nuxt](https://kernstra.com/docs/web-sdk/guides/integrations#s-nuxt)
- [Kernstra.init](https://kernstra.com/docs/web-sdk/api/kernstra/init)
- [PDFViewer.destroy](https://kernstra.com/docs/web-sdk/api/pdfviewer/destroy)

Web page: https://kernstra.com/docs/web-sdk/recipes/ssr-frameworks
