A framework-agnostic, universal document renderer with optional chunked loading
polyrender.wisp.place/
1import { forwardRef, useImperativeHandle, useMemo } from 'react'
2import type {
3 DocumentSource,
4 DocumentFormat,
5 DocumentInfo,
6 PolyRenderError,
7 ToolbarConfig,
8 PdfOptions,
9 CodeOptions,
10 CsvOptions,
11 EpubOptions,
12} from '@polyrender/core'
13import { useDocumentRenderer } from './useDocumentRenderer.js'
14
15export interface DocumentViewerProps {
16 /** The document to render. Pass null/undefined to show nothing. */
17 source: DocumentSource | null | undefined
18
19 /** Explicit format override. If omitted, auto-detected from source. */
20 format?: DocumentFormat
21
22 /** Color theme. Defaults to 'dark'. */
23 theme?: 'light' | 'dark' | 'system'
24
25 /** Additional CSS class(es) for the root container. */
26 className?: string
27
28 /** Inline styles for the outer wrapper div (set width/height here). */
29 style?: React.CSSProperties
30
31 /** Page to display initially (1-indexed). Defaults to 1. */
32 initialPage?: number
33
34 /** Initial zoom level. */
35 zoom?: number | 'fit-width' | 'fit-page' | 'auto'
36
37 /** Toolbar configuration. true = default, false = hidden. */
38 toolbar?: boolean | ToolbarConfig
39
40 /** Show page numbers. */
41 showPageNumbers?: boolean
42
43 // --- Callbacks ---
44 onReady?: (info: DocumentInfo) => void
45 onPageChange?: (page: number, totalPages: number) => void
46 onZoomChange?: (zoom: number) => void
47 onError?: (error: PolyRenderError) => void
48 onLoadingChange?: (loading: boolean) => void
49
50 // --- Format-specific options ---
51 pdf?: PdfOptions
52 code?: CodeOptions
53 csv?: CsvOptions
54 epub?: EpubOptions
55}
56
57export interface DocumentViewerRef {
58 /** Navigate to a specific page (1-indexed). */
59 goToPage: (page: number) => void
60 /** Set zoom level. */
61 setZoom: (zoom: number | 'fit-width' | 'fit-page') => void
62 /** Get current page number. */
63 getCurrentPage: () => number
64 /** Get total page count. */
65 getPageCount: () => number
66 /** Get current zoom level. */
67 getZoom: () => number
68}
69
70/**
71 * React component for rendering documents of any supported format.
72 *
73 * Wraps the framework-agnostic `@polyrender/core` library with React lifecycle
74 * management, ref-based imperative API, and automatic cleanup.
75 *
76 * @example
77 * ```tsx
78 * import { DocumentViewer } from '@polyrender/react'
79 * import '@polyrender/core/styles.css'
80 *
81 * function App() {
82 * return (
83 * <DocumentViewer
84 * source={{ type: 'url', url: '/report.pdf' }}
85 * theme="dark"
86 * style={{ width: '100%', height: '80vh' }}
87 * onReady={(info) => console.log(`Loaded ${info.pageCount} pages`)}
88 * />
89 * )
90 * }
91 * ```
92 *
93 * @example Chunked / pre-rendered pages
94 * ```tsx
95 * <DocumentViewer
96 * source={{
97 * type: 'pages',
98 * pages: {
99 * totalPages: 200,
100 * fetchPage: async (n) => ({
101 * pageNumber: n,
102 * imageUrl: `/api/pages/${n}.webp`,
103 * width: 1654,
104 * height: 2339,
105 * }),
106 * },
107 * }}
108 * />
109 * ```
110 *
111 * @example Imperative control via ref
112 * ```tsx
113 * const viewerRef = useRef<DocumentViewerRef>(null)
114 *
115 * <DocumentViewer ref={viewerRef} source={source} />
116 * <button onClick={() => viewerRef.current?.goToPage(10)}>Go to page 10</button>
117 * ```
118 */
119export const DocumentViewer = forwardRef<DocumentViewerRef, DocumentViewerProps>(
120 function DocumentViewer(props, ref) {
121 const {
122 source,
123 format,
124 theme,
125 className,
126 style,
127 initialPage,
128 zoom,
129 toolbar,
130 showPageNumbers,
131 onReady,
132 onPageChange,
133 onZoomChange,
134 onError,
135 onLoadingChange,
136 pdf,
137 code,
138 csv,
139 epub,
140 } = props
141
142 const {
143 containerRef,
144 state,
145 goToPage,
146 setZoom,
147 } = useDocumentRenderer({
148 source: source ?? undefined,
149 format,
150 theme,
151 className,
152 initialPage,
153 zoom,
154 toolbar,
155 showPageNumbers,
156 onReady,
157 onPageChange,
158 onZoomChange,
159 onError,
160 onLoadingChange,
161 pdf,
162 code,
163 csv,
164 epub,
165 })
166
167 // Expose imperative API via ref
168 useImperativeHandle(ref, () => ({
169 goToPage,
170 setZoom,
171 getCurrentPage: () => state.currentPage,
172 getPageCount: () => state.totalPages,
173 getZoom: () => state.zoom,
174 }), [goToPage, setZoom, state.currentPage, state.totalPages, state.zoom])
175
176 const containerStyle = useMemo<React.CSSProperties>(() => ({
177 width: '100%',
178 height: '100%',
179 minHeight: 200,
180 ...style,
181 }), [style])
182
183 return (
184 <div
185 ref={containerRef as any}
186 style={containerStyle}
187 data-polyrender-wrapper=""
188 />
189 )
190 },
191)