Skip to content

Framework and bundler integration

All recipes use the same framework-agnostic runtime. npm run test:pack installs the generated tarball into a clean directory and verifies strict TypeScript, plain ESM/SSR import, esbuild, Vite, webpack and Next.js production builds.

Vite and React

Import package CSS once and create/destroy the client inside the component lifecycle. The complete runnable version in examples/react contains three tabs showing distinct integration levels:

  • ui: true for the complete built-in interface;
  • ui: false for a managed viewport with navigation, search, copy, download, state, and events rendered by React;
  • no container for headless page/sheet canvases, thumbnails, metadata, and text extraction owned entirely by React.
tsx
import { useEffect, useRef } from "react";
import { ViewerClient } from "@zrimo/viewer";
import "@zrimo/viewer/styles.css";

export function DocumentViewer({ file }: { file?: File }) {
  const host = useRef<HTMLDivElement>(null);
  useEffect(() => {
    if (!host.current) return;
    const client = ViewerClient.create();
    const viewer = client.createViewer({ container: host.current, ui: true });
    if (file) void viewer.load(file, { fileName: file.name });
    return () => void viewer.destroy().finally(() => client.destroy());
  }, [file]);
  return <div ref={host} />;
}

webpack and Next.js

The root import is SSR-safe. Create the viewer only in a client component/effect because Canvas, Worker and the host element are browser resources.

tsx
"use client";
import { useEffect, useRef } from "react";
import { ViewerClient } from "@zrimo/viewer";
import "@zrimo/viewer/styles.css";

export default function Viewer() {
  const host = useRef<HTMLDivElement>(null);
  useEffect(() => {
    const client = ViewerClient.create();
    const viewer = client.createViewer({ container: host.current!, ui: true });
    return () => void viewer.destroy().finally(() => client.destroy());
  }, []);
  return <div ref={host} />;
}

webpack 5 handles new URL(..., import.meta.url) and async WASM assets. If deployment tooling copies package assets rather than emitting them, copy dist/assets, dist/workers, and dist/fonts to one public directory and pass that directory as assetBaseUrl.

Angular with the esbuild builder

Create the client in ngAfterViewInit and dispose it in ngOnDestroy. Angular's current application builder uses esbuild; the packed consumer test bundles the same public imports with strict browser resolution.

ts
export class PreviewComponent implements AfterViewInit, OnDestroy {
  @ViewChild("host", { static: true }) host!: ElementRef<HTMLElement>;
  private client?: ViewerClient;
  private viewer?: ViewerApi;

  ngAfterViewInit() {
    this.client = ViewerClient.create();
    this.viewer = this.client.createViewer({
      container: this.host.nativeElement,
      ui: true,
    });
  }

  async ngOnDestroy() {
    await this.viewer?.destroy();
    await this.client?.destroy();
  }
}

Add the package stylesheet to angular.json or import it from the application stylesheet.

Plain ESM and self-hosted assets

Serve the package from a CDN/import map, or let a bundler resolve it. For an explicit self-host directory:

js
import { ViewerClient } from "@zrimo/viewer";

const client = ViewerClient.create({
  assetBaseUrl: new URL("/static/zrimo/", location.href),
});

Copy assets/, workers/, and fonts/ from package dist/ below that URL. Serve .wasm as application/wasm, .js as JavaScript modules, .woff2 as font/woff2, and enable Brotli/gzip with immutable hashed deployment URLs. Workers are ESM and CSP-compatible without eval; allow the self-host origin in worker-src, script-src, font-src and connect-src as applicable.

Released under the MIT or Apache-2.0 license.