A framework-agnostic, universal document renderer with optional chunked loading polyrender.wisp.place/
at main 168 lines 4.9 kB view raw
1import { useRef, useEffect, useState, useCallback } from 'react' 2import type { 3 PolyRenderOptions, 4 PolyRenderState, 5 DocumentInfo, 6 PolyRenderError, 7 DocumentSource, 8} from '@polyrender/core' 9import { PolyRender } from '@polyrender/core' 10 11export interface UseDocumentRendererOptions 12 extends Omit<PolyRenderOptions, 'source' | 'onReady' | 'onPageChange' | 'onZoomChange' | 'onError' | 'onLoadingChange'> { 13 source: DocumentSource | null | undefined 14 onReady?: (info: DocumentInfo) => void 15 onPageChange?: (page: number, totalPages: number) => void 16 onZoomChange?: (zoom: number) => void 17 onError?: (error: PolyRenderError) => void 18 onLoadingChange?: (loading: boolean) => void 19} 20 21export interface UseDocumentRendererReturn { 22 /** Ref to attach to the container div. */ 23 containerRef: React.RefObject<HTMLDivElement | null> 24 /** Current viewer state. */ 25 state: PolyRenderState 26 /** Navigate to a page. */ 27 goToPage: (page: number) => void 28 /** Set zoom level. */ 29 setZoom: (zoom: number | 'fit-width' | 'fit-page') => void 30 /** Whether the viewer is mounted and ready. */ 31 ready: boolean 32 /** Current error, if any. */ 33 error: PolyRenderError | null 34} 35 36/** 37 * React hook for the PolyRender document renderer. 38 * 39 * Manages the lifecycle of a PolyRender instance, bridging its imperative API 40 * to React's declarative model. Handles mounting, updating, and cleanup. 41 * 42 * @example 43 * ```tsx 44 * function MyViewer({ url }: { url: string }) { 45 * const { containerRef, state, goToPage } = useDocumentRenderer({ 46 * source: { type: 'url', url }, 47 * theme: 'dark', 48 * }) 49 * 50 * return ( 51 * <div> 52 * <div ref={containerRef} style={{ width: '100%', height: '600px' }} /> 53 * <p>Page {state.currentPage} of {state.totalPages}</p> 54 * </div> 55 * ) 56 * } 57 * ``` 58 */ 59export function useDocumentRenderer( 60 options: UseDocumentRendererOptions, 61): UseDocumentRendererReturn { 62 const containerRef = useRef<HTMLDivElement | null>(null) 63 const instanceRef = useRef<PolyRender | null>(null) 64 const optionsRef = useRef(options) 65 66 const [state, setState] = useState<PolyRenderState>({ 67 loading: true, 68 error: null, 69 currentPage: 1, 70 totalPages: 0, 71 zoom: 1, 72 documentInfo: null, 73 }) 74 75 const [ready, setReady] = useState(false) 76 const [error, setError] = useState<PolyRenderError | null>(null) 77 78 // Keep options ref current 79 optionsRef.current = options 80 81 // Mount / unmount effect 82 useEffect(() => { 83 const container = containerRef.current 84 if (!container || !options.source) return 85 86 // Clear any previous instance 87 if (instanceRef.current) { 88 instanceRef.current.destroy() 89 instanceRef.current = null 90 } 91 92 setReady(false) 93 setError(null) 94 setState((s) => ({ ...s, loading: true, error: null })) 95 96 const instance = new PolyRender(container, { 97 ...options, 98 source: options.source, 99 onReady: (info) => { 100 setReady(true) 101 setState((s) => ({ 102 ...s, 103 loading: false, 104 totalPages: info.pageCount, 105 documentInfo: info, 106 })) 107 optionsRef.current.onReady?.(info) 108 }, 109 onPageChange: (page, totalPages) => { 110 setState((s) => ({ ...s, currentPage: page, totalPages })) 111 optionsRef.current.onPageChange?.(page, totalPages) 112 }, 113 onZoomChange: (zoom) => { 114 setState((s) => ({ ...s, zoom })) 115 optionsRef.current.onZoomChange?.(zoom) 116 }, 117 onError: (err) => { 118 setError(err) 119 setState((s) => ({ ...s, loading: false, error: err })) 120 optionsRef.current.onError?.(err) 121 }, 122 onLoadingChange: (loading) => { 123 setState((s) => ({ ...s, loading })) 124 optionsRef.current.onLoadingChange?.(loading) 125 }, 126 }) 127 128 instanceRef.current = instance 129 130 return () => { 131 instance.destroy() 132 instanceRef.current = null 133 } 134 // Re-mount when source identity changes 135 // eslint-disable-next-line react-hooks/exhaustive-deps 136 }, [options.source, options.format, options.theme]) 137 138 // Update non-source options without re-mounting 139 useEffect(() => { 140 if (!instanceRef.current) return 141 142 const changed: Partial<PolyRenderOptions> = {} 143 if (options.theme) changed.theme = options.theme 144 if (options.className !== undefined) changed.className = options.className 145 if (options.zoom !== undefined) changed.zoom = options.zoom 146 147 if (Object.keys(changed).length > 0) { 148 instanceRef.current.update(changed) 149 } 150 }, [options.theme, options.className, options.zoom]) 151 152 const goToPage = useCallback((page: number) => { 153 instanceRef.current?.goToPage(page) 154 }, []) 155 156 const setZoom = useCallback((zoom: number | 'fit-width' | 'fit-page') => { 157 instanceRef.current?.setZoom(zoom) 158 }, []) 159 160 return { 161 containerRef, 162 state, 163 goToPage, 164 setZoom, 165 ready, 166 error, 167 } 168}