@edclub/print — PDF Preview (hooks)
This package provides lightweight host‑side hooks for previewing generated PDFs. In practice we use two hooks:
- usePDFIframePreview
- usePDFCanvasPreview
Both accept printData (a serialized PDF document) and return preview component(s) and a download link.
Where does printData come from?
printData is a serialized document tree (typically produced by the client that serializes PDF components). If you already receive printData from your integration, just pass it into the hook.
usePDFIframePreview(printData)
The hook returns:
- IframePreview(props)
- DownloadLink(props)
IframePreview props
- className?: string
- width?: number
- height?: number
- onRender?: (iframe: HTMLIFrameElement) => void
DownloadLink props
- filename?: string (default: "doc.pdf")
- className?: string
- disabled?: boolean (automatically set while generating)
- children: React.ReactNode (button content)
Example
import {
Document,
Page,
Text,
View,
serializeToJSON,
type SerializeToJSONReturnValue,
} from "@edclub/print/client";
import { usePDFIframePreview } from "@edclub/print/host";
import {
renderPageHeader,
renderSectionHeader,
renderTable,
} from "@edclub/print/templates";
type Props = {
data: Record<string, any>;
};
function PrintTemplate(props: Props) {
return (
<Document>
<Page size="A4" className="p-8">
<View className="mb-6">
{renderPageHeader("Title")} // this header template generates current
date
<Text className="text-red-500">{props.data.title}</Text> //
non-template way
<View>
{" "}
// iteration can be done within View
{props.data.list.map((item) => (
<Text key={item.id}>{item.label}</Text>
))}
</View>
<View>
{renderSectionHeader("Summary")}
{renderTable({
columnWidths: [25, 25, 30, 20],
classNames: {
tableClassName: "mb-2",
rowClassName: "p-0",
headerCellClassName: "px-0",
cellClassName: "px-0",
},
headers: [
{ content: "Category", className: "font-semibold" }, // with className config,
"Actual", // Simple without custom className,
"Goal",
"Status",
],
rows: [
[
"Test Category",
"Test Actual",
"Test Goal",
renderCustomTextOrNode(randomProps),
],
],
})}
</View>
</View>
</Page>
</Document>
);
}
export function IframePreviewExample() {
const data = useSomeData();
const printData = useMemo(() => serializeToJSON(PrintTemplate(data)), [data]);
const { IframePreview, DownloadLink } = usePDFIframePreview(printData);
return (
<div className="flex flex-col gap-4">
<div className="aspect-[8.5/11] w-full overflow-hidden rounded-lg border">
<IframePreview width={200} height={100} />
</div>
<DownloadLink className="btn" filename="document.pdf">
Download PDF
</DownloadLink>
</div>
);
}
usePDFCanvasPreview(printData)
The hook returns:
- CanvasPreview(props)
- DownloadLink(props)
CanvasPreview renders a PDF page onto a <canvas> and scales it to fit the container size provided via parentRef.
CanvasPreview props
- parentRef: React.RefObject<HTMLElement | null> (required)
- className?: string (default: "size-full text-center")
- page?: number (default: 1)
- onRender?: (canvas: HTMLCanvasElement) => void
- onPDFInfo?: ({ pageCount, currentPage, goToPage, nextPage, previousPage }) => void
- pageSize?: string (default: "A4")
- pageOrientation?: "portrait" | "landscape" (default: "portrait")
Example
import { useRef } from "react";
import type { SerializeToJSONReturnValue } from "@edclub/print/client";
import { usePDFCanvasPreview } from "@edclub/print/host";
export function CanvasPreviewExample(props: Props) {
const data = useSomeData();
const printData = useMemo(() => serializeToJSON(PrintTemplate(data)), [data]);
const containerRef = useRef<HTMLDivElement | null>(null);
const { CanvasPreview, DownloadLink } = usePDFCanvasPreview(printData);
return (
<div className="flex flex-col gap-4">
<div ref={containerRef} className="relative h-[720px] w-full">
<CanvasPreview
parentRef={containerRef}
onPDFInfo={(info) => {
// e.g. update pagination UI
console.log(info.pageCount, info.currentPage);
}}
/>
</div>
<DownloadLink className="btn" filename="document.pdf">
Download PDF
</DownloadLink>
</div>
);
}
Wrapping and page breaks, and iterating lists
- To prevent a block from breaking across pages, set
wrap={false} on View. This keeps the block together on a single page.
- Perform iteration (e.g.,
.map) inside a View — not directly at the Page level.
Example
<Page style={styles.page}>
<Text style={styles.pageTitle}>Quiz Results - Manual Test</Text>
<View>
{[...questions, ...questions].map((q) => (
<View wrap={false} style={styles.questionCard}>
<Text style={styles.questionNumber}>Question {q.id}</Text>
<Text style={styles.questionText}>{q.text}</Text>
</View>
))}
</View>
</Page>
Differences between previews
- IframePreview: uses the built‑in browser PDF viewer (toolbar hidden), simple and quick to integrate.
- CanvasPreview: renders a page onto
<canvas> sized to the container (parentRef) and exposes PDF info (page count, navigation).
Generating print artifacts
- To generate print artifact that can be stored in the db we can generate a json version of a print component
const reportData = getReportData();
const printData = serialize;