I'm trying to show a PDF in my page by using "react-pdf". I am now struggling with making it responsive. I am now using this custom hook to get the size of the contentRect:
import { useEffect, useState, useCallback } from "react";
interface Size {
width: number;
height: number;
}
const useResizeObserver = (targetRef: HTMLElement | null): Size => {
const [size, setSize] = useState<Size>({ width: 0, height: 0 });
const handleResize = useCallback((entries: ResizeObserverEntry[]) => {
const [entry] = entries;
if (entry) {
setSize({
width: entry.contentRect.width,
height: entry.contentRect.height,
});
}
}, []);
useEffect(() => {
if (!targetRef) return;
const resizeObserver = new ResizeObserver(handleResize);
resizeObserver.observe(targetRef);
return () => {
resizeObserver.disconnect();
};
}, [targetRef, handleResize]);
return size;
};
export default useResizeObserver;
This is my PDFViewer.tsx that is used in the page.tsx:
"use client";
import React, { useRef, useState } from "react";
import { Document, Page } from "react-pdf";
import "react-pdf/dist/esm/Page/AnnotationLayer.css";
import { pdfjs } from "react-pdf";
import "react-pdf/dist/Page/TextLayer.css";
import type { PDFDocumentProxy } from 'pdfjs-dist';
import useResizeObserver from "@/hooks/useResizeObserver";
pdfjs.GlobalWorkerOptions.workerSrc = new URL(
"pdfjs-dist/build/pdf.worker.min.mjs",
import.meta.url
).toString();
const PDFViewer: React.FC = () => {
const [numPages, setNumPages] = useState<number>(0);
const [pageNumber, setPageNumber] = useState<number>(1);
const containerRef = useRef<HTMLDivElement | null>(null);
const { width: pdfWidth, height: pdfHeight } = useResizeObserver(containerRef.current);
const pdfUrl = "/docs/menu.pdf";
function onDocumentLoadSuccess({ numPages: nextNumPages }: PDFDocumentProxy): void {
setNumPages(nextNumPages);
}
return (
<div className="flex flex-row left-2 mt-4 justify-center items-center w-full h-auto px-4">
<div className="flex flex-1 font-generalRegular justify-right text-right text-primary text-6xl px-4">
{" < "}
</div>
<div id="containerRef" ref={containerRef} className="flex flex-8 flex-col font-generalRegular justify-center text-center text-primary text-sm px-4">
<p>Menu</p>
<p>Page {pageNumber} of {numPages}</p>
<p>Width: {pdfWidth}</p>
<Document file={pdfUrl} onLoadSuccess={onDocumentLoadSuccess}>
<Page pageNumber={pageNumber} width={pdfWidth}/>
</Document>
</div>
<div className="flex flex-1 font-generalRegular justify-left text-left text-primary text-6xl px-4">
{" > "}
</div>
</div>
);
};
export default PDFViewer;
As you might notice, I've added a text element to print the width. Yet, it never changes. Even when I constantly resize the browser window. Can anybody tell me what went wrong here?
Thank you.
I'm trying to show a PDF in my page by using "react-pdf". I am now struggling with making it responsive. I am now using this custom hook to get the size of the contentRect:
import { useEffect, useState, useCallback } from "react";
interface Size {
width: number;
height: number;
}
const useResizeObserver = (targetRef: HTMLElement | null): Size => {
const [size, setSize] = useState<Size>({ width: 0, height: 0 });
const handleResize = useCallback((entries: ResizeObserverEntry[]) => {
const [entry] = entries;
if (entry) {
setSize({
width: entry.contentRect.width,
height: entry.contentRect.height,
});
}
}, []);
useEffect(() => {
if (!targetRef) return;
const resizeObserver = new ResizeObserver(handleResize);
resizeObserver.observe(targetRef);
return () => {
resizeObserver.disconnect();
};
}, [targetRef, handleResize]);
return size;
};
export default useResizeObserver;
This is my PDFViewer.tsx that is used in the page.tsx:
"use client";
import React, { useRef, useState } from "react";
import { Document, Page } from "react-pdf";
import "react-pdf/dist/esm/Page/AnnotationLayer.css";
import { pdfjs } from "react-pdf";
import "react-pdf/dist/Page/TextLayer.css";
import type { PDFDocumentProxy } from 'pdfjs-dist';
import useResizeObserver from "@/hooks/useResizeObserver";
pdfjs.GlobalWorkerOptions.workerSrc = new URL(
"pdfjs-dist/build/pdf.worker.min.mjs",
import.meta.url
).toString();
const PDFViewer: React.FC = () => {
const [numPages, setNumPages] = useState<number>(0);
const [pageNumber, setPageNumber] = useState<number>(1);
const containerRef = useRef<HTMLDivElement | null>(null);
const { width: pdfWidth, height: pdfHeight } = useResizeObserver(containerRef.current);
const pdfUrl = "/docs/menu.pdf";
function onDocumentLoadSuccess({ numPages: nextNumPages }: PDFDocumentProxy): void {
setNumPages(nextNumPages);
}
return (
<div className="flex flex-row left-2 mt-4 justify-center items-center w-full h-auto px-4">
<div className="flex flex-1 font-generalRegular justify-right text-right text-primary text-6xl px-4">
{" < "}
</div>
<div id="containerRef" ref={containerRef} className="flex flex-8 flex-col font-generalRegular justify-center text-center text-primary text-sm px-4">
<p>Menu</p>
<p>Page {pageNumber} of {numPages}</p>
<p>Width: {pdfWidth}</p>
<Document file={pdfUrl} onLoadSuccess={onDocumentLoadSuccess}>
<Page pageNumber={pageNumber} width={pdfWidth}/>
</Document>
</div>
<div className="flex flex-1 font-generalRegular justify-left text-left text-primary text-6xl px-4">
{" > "}
</div>
</div>
);
};
export default PDFViewer;
As you might notice, I've added a text element to print the width. Yet, it never changes. Even when I constantly resize the browser window. Can anybody tell me what went wrong here?
Thank you.
When you call useResizeObserver
use pass it the value of containerRef.current
, which is undefined
. After the component renders, and calls useEffect
, the value is still null
, which skips the observer's init due to if (!targetRef) return;
.
const { width: pdfWidth, height: pdfHeight } = useResizeObserver(containerRef.current);
Instead change the hook to accept the entire ref object, and after the render the current
value would update:
const useResizeObserver = (targetRef: RefObject<HTMLElement | null>): Size => {
const [size, setSize] = useState<Size>({ width: 0, height: 0 });
useEffect(() => {
const handleResize = (entries: ResizeObserverEntry[]) => {
const [entry] = entries;
if (entry) {
setSize({
width: entry.contentRect.width,
height: entry.contentRect.height,
});
}
};
const resizeObserver = new ResizeObserver(handleResize);
resizeObserver.observe(targetRef.current);
return () => {
resizeObserver.disconnect();
};
}, []);
return size;
};
Calling it:
const { width: pdfWidth, height: pdfHeight } = useResizeObserver(containerRef);
Running example (without TS):
const { useState, useEffect, useRef } = React;
const useResizeObserver = targetRef => {
const [size, setSize] = useState({ width: 0, height: 0 });
useEffect(() => {
const handleResize = entries => {
const [entry] = entries;
if (entry) {
setSize({
width: entry.contentRect.width,
height: entry.contentRect.height,
});
}
};
const resizeObserver = new ResizeObserver(handleResize);
resizeObserver.observe(targetRef.current);
return () => {
resizeObserver.disconnect();
};
}, []);
return size;
};
const Demo = () => {
const ref = useRef();
const size = useResizeObserver(ref);
console.log(size);
return <div className="target" ref={ref} />
}
ReactDOM
.createRoot(root)
.render(<Demo />);
.target {
height: 50vh;
}
<script src="https://cdnjs.cloudflare/ajax/libs/react/18.3.1/umd/react.production.min.js"></script>
<script src="https://cdnjs.cloudflare/ajax/libs/react-dom/18.3.1/umd/react-dom.production.min.js"></script>
<div id="root"></div>
However, in certain cases, for example components that are added / removed dynamically, the ref might not be ready in time. In those cases the hook can have a state that holds the ref. The hook returns a function to set the ref, and when the ref updates, the useEffect
is called with the updated ref:
const useResizeObserver = () => {
const [targetRef, setTargetRef] = useState<HTMLElement | null>(null);
const [size, setSize] = useState<Size>({ width: 0, height: 0 });
const handleResize = useCallback((entries: ResizeObserverEntry[]) => {
const [entry] = entries;
if (entry) {
setSize({
width: entry.contentRect.width,
height: entry.contentRect.height,
});
}
}, []);
useEffect(() => {
if (!targetRef) return;
const resizeObserver = new ResizeObserver(handleResize);
resizeObserver.observe(targetRef);
return () => {
resizeObserver.disconnect();
};
}, [targetRef, handleResize]);
return {
setTargetRef,
size
};
};
Usage:
const PDFViewer: React.FC = () => {
const [numPages, setNumPages] = useState<number>(0);
const [pageNumber, setPageNumber] = useState<number>(1);
const { setTargetRef, size: { width: pdfWidth, height: pdfHeight }} = useResizeObserver();
/* unrelated code **/
return (
/* unrelated code **/
<div id="containerRef" ref={setTargetRef} className="classed">
/* unrelated code **/
);
};
Running example (without TS):
const { useState, useEffect, useCallback } = React;
const useResizeObserver = () => {
const [targetRef, setTargetRef] = useState(null);
const [size, setSize] = useState({ width: 0, height: 0 });
const handleResize = useCallback(entries => {
const [entry] = entries;
if (entry) {
setSize({
width: entry.contentRect.width,
height: entry.contentRect.height,
});
}
}, []);
useEffect(() => {
if (!targetRef) return;
const resizeObserver = new ResizeObserver(handleResize);
resizeObserver.observe(targetRef);
return () => {
resizeObserver.disconnect();
};
}, [targetRef, handleResize]);
return {
setTargetRef,
size
};
};
const Demo = () => {
const { setTargetRef, size } = useResizeObserver();
console.log(size);
return <div className="target" ref={setTargetRef} />
}
ReactDOM
.createRoot(root)
.render(<Demo />);
.target {
height: 50vh;
}
<script src="https://cdnjs.cloudflare/ajax/libs/react/18.3.1/umd/react.production.min.js"></script>
<script src="https://cdnjs.cloudflare/ajax/libs/react-dom/18.3.1/umd/react-dom.production.min.js"></script>
<div id="root"></div>