React:Examples:ExportPdfForHiddenComponents
lastmod 2025-04-01created 2025-03-267 edits
Example of saving a hidden React component as a PDF.
Why not use media queries
@media print only works when the browser's print feature (Ctrl+P) is used.
So if you want to download a PDF directly without a separate print popup, you need to do it as below.
Why not use an iframe
- An iframe is a completely separate document.
- React components run inside the Virtual DOM, so inserting them directly into another DOM (DocumentContext) requires a separate rendering approach.
- An iframe isolates CSS, so the app's global styles and themes are not applied to the iframe.
- CSS has to be loaded separately or passed inline.
- Passing props, context, state, etc. to an iframe requires extra work such as postMessage and JSON serialization.
- You cannot simply pass a component over.
Creating a hidden HTML element
<div
ref={componentRef}
style={{
position: "fixed",
top: 0,
left: 0,
width: "210mm",
height: "297mm",
zIndex: -9999,
opacity: 0,
pointerEvents: "none",
overflow: "hidden",
}}
>
<OtherPageComponent />
</div>
| Style property | Reason |
| | Fixed on screen regardless of scrolling |
| | All the way to the back of the screen (so it isn't visible) |
| | Invisible to the eye |
| | Blocks clicks and other events |
| | Prevents any internal overflow, just in case |
| | Exact size matching the PDF output |
Downloading an Export PDF icon
Download a suitable icon from https://icones.js.org/.
import {SVGProps} from 'react';
export function BiFileEarmarkPdf(props: SVGProps<SVGSVGElement>) {
return (
<svg
xmlns="http://www.w3.org/2000/svg"
width="1em"
height="1em"
viewBox="0 0 16 16"
{...props}
>
<g fill="currentColor">
<path d="M14 14V4.5L9.5 0H4a2 2 0 0 0-2 2v12a2 2 0 0 0 2 2h8a2 2 0 0 0 2-2M9.5 3A1.5 1.5 0 0 0 11 4.5h2V14a1 1 0 0 1-1 1H4a1 1 0 0 1-1-1V2a1 1 0 0 1 1-1h5.5z"></path>
<path d="M4.603 14.087a.8.8 0 0 1-.438-.42c-.195-.388-.13-.776.08-1.102c.198-.307.526-.568.897-.787a7.7 7.7 0 0 1 1.482-.645a20 20 0 0 0 1.062-2.227a7.3 7.3 0 0 1-.43-1.295c-.086-.4-.119-.796-.046-1.136c.075-.354.274-.672.65-.823c.192-.077.4-.12.602-.077a.7.7 0 0 1 .477.365c.088.164.12.356.127.538c.007.188-.012.396-.047.614c-.084.51-.27 1.134-.52 1.794a11 11 0 0 0 .98 1.686a5.8 5.8 0 0 1 1.334.05c.364.066.734.195.96.465c.12.144.193.32.2.518c.007.192-.047.382-.138.563a1.04 1.04 0 0 1-.354.416a.86.86 0 0 1-.51.138c-.331-.014-.654-.196-.933-.417a5.7 5.7 0 0 1-.911-.95a11.7 11.7 0 0 0-1.997.406a11.3 11.3 0 0 1-1.02 1.51c-.292.35-.609.656-.927.787a.8.8 0 0 1-.58.029m1.379-1.901q-.25.115-.459.238c-.328.194-.541.383-.647.547c-.094.145-.096.25-.04.361q.016.032.026.044l.035-.012c.137-.056.355-.235.635-.572a8 8 0 0 0 .45-.606m1.64-1.33a13 13 0 0 1 1.01-.193a12 12 0 0 1-.51-.858a21 21 0 0 1-.5 1.05zm2.446.45q.226.245.435.41c.24.19.407.253.498.256a.1.1 0 0 0 .07-.015a.3.3 0 0 0 .094-.125a.44.44 0 0 0 .059-.2a.1.1 0 0 0-.026-.063c-.052-.062-.2-.152-.518-.209a4 4 0 0 0-.612-.053zM8.078 7.8a7 7 0 0 0 .2-.828q.046-.282.038-.465a.6.6 0 0 0-.032-.198a.5.5 0 0 0-.145.04c-.087.035-.158.106-.196.283c-.04.192-.03.469.046.822q.036.167.09.346z"></path>
</g>
</svg>
);
}
export default BiFileEarmarkPdf;
Applying an icon button with a tooltip:
<div
className="tw-group tw-relative tw-flex tw-justify-center tw-items-center"
onClick={e => onClickExportPdf(e, d)}
>
<BiFileEarmarkPdf className="tw-text-center tw-text-lg tw-m-1" />
<span className="group-hover:tw-opacity-70 tw-transition-opacity tw-bg-gray-500 tw-py-1 tw-px-2 tw-text-xs tw-text-gray-100 tw-rounded-md tw-absolute tw-left-1/2 -tw-translate-x-1/2 tw-translate-y-full tw-opacity-0 tw-m-4 tw-mx-auto">
PDF
</span>
</div>
Adding a button and event handler in a suitable place
import BiFileEarmarkPdf from '../icons/BiFileEarmarkPdf';
import styles from './DailyWorkStatus.module.scss';
// ...
export default function DailyWorkStatus() {
// ...
const pdfLayoutRef = useRef<HTMLDivElement>(null);
const onClickExportPdf = (
e: React.MouseEvent<SVGSVGElement, MouseEvent>,
data: DailyInfoData
) => {
e.stopPropagation();
};
// ...
<BiFileEarmarkPdf onClick={e => onClickExportPdf(e, d)} />
// ...
<div ref={pdfLayoutRef} className={styles.pdfLayout}></div>
// ...
}
Actual code
const pdfLayoutRef = useRef<HTMLDivElement>(null);
const [selectedDailyInfo, setSelectedDailyInfo] = useState<DailyInfoData>();
const [grapples, setGrapples] = useState<DailyDetailGrapplesData[]>();
const [workBrief, setWorkBrief] = useState<DailyInfoBrief>();
const [raw, setRaw] = useState<DailyInfoRaw>();
const [grappleImageSrcs, setGrappleImageSrcs] = useState<string[]>([]);
const onClickExportPdf = async (
e: React.MouseEvent<HTMLDivElement, MouseEvent>,
data: DailyInfoData
) => {
e.stopPropagation();
if (selectedDailyInfo) {
return;
}
setSelectedDailyInfo(data);
try {
const vehicleIdx = data.vehicleIdx;
const briefResp = await getData('work/daily/detail/info/work', {vehicleIdx});
const workBrief = briefResp as DailyInfoBrief;
const grapplesResp = await getData('work/daily/detail/grapples', {vehicleIdx});
const grapples = grapplesResp as DailyDetailGrapplesData[];
const imagePromises = grapples?.map(async grapple => {
const imageData = await getImage(`${grapple.originImage}`, `true`);
const blob = await imageData.blob();
return URL.createObjectURL(blob);
});
const imageSrcs = await Promise.all(imagePromises);
const rawResp = await getData(`statistics/scraps`, {vehicleIdx});
const raw = rawResp as DailyInfoRaw;
setGrapples(grapples);
setWorkBrief(workBrief);
setRaw(raw);
setGrappleImageSrcs(imageSrcs);
} catch (e) {
console.error(e);
setGrapples(undefined);
setWorkBrief(undefined);
setRaw(undefined);
setGrappleImageSrcs([]);
} finally {
setSelectedDailyInfo(undefined);
}
};
const generatePDF = async () => {
if (!pdfLayoutRef.current) {
return;
}
try {
const canvas = await html2canvas(pdfLayoutRef.current, {
scale: 2.0,
useCORS: true,
logging: false,
});
const imgData = canvas.toDataURL('image/png');
const pdf = new jspdf({
orientation: 'p',
unit: 'px',
format: [canvas.width, canvas.height],
});
pdf.addImage(imgData, 'PNG', 0, 0, canvas.width, canvas.height);
pdf.save('document.pdf');
} catch (error) {
console.error('PDF 생성 중 오류:', error);
}
};
useEffect(() => {
if (!pdfLayoutRef.current) {
return;
}
if (!(grapples && workBrief && raw && grappleImageSrcs)) {
return;
}
const timer = setTimeout(async () => {
await generatePDF();
}, 100);
return () => clearTimeout(timer);
}, [grapples, workBrief, raw, grappleImageSrcs]);
const DownloadPdfIcon = ({cursor}: {cursor: DailyInfoData}) => {
if (!selectedDailyInfo) {
return (
<div
className="tw-group tw-relative tw-flex tw-justify-center tw-items-center"
onClick={e => onClickExportPdf(e, cursor)}
>
<BiFileEarmarkPdf className="tw-text-center tw-text-lg tw-m-1" />
<span className="group-hover:tw-opacity-70 tw-transition-opacity tw-bg-gray-500 tw-py-1 tw-px-2 tw-text-xs tw-text-gray-100 tw-rounded-md tw-absolute tw-left-1/2 -tw-translate-x-1/2 tw-translate-y-full tw-opacity-0 tw-m-4 tw-mx-auto">
PDF
</span>
</div>
);
} else if (selectedDailyInfo === cursor) {
return (
<div
className="tw-group tw-relative tw-flex tw-justify-center tw-items-center"
onClick={e => e.stopPropagation()}
>
<SvgSpinnersRingResize className="tw-text-center tw-text-lg tw-m-1" />
<span className="group-hover:tw-opacity-70 tw-transition-opacity tw-bg-gray-500 tw-py-1 tw-px-2 tw-text-xs tw-text-gray-100 tw-rounded-md tw-absolute tw-left-1/2 -tw-translate-x-1/2 tw-translate-y-full tw-opacity-0 tw-m-4 tw-mx-auto">
Loading
</span>
</div>
);
} else {
return (
<div
className="tw-relative tw-flex tw-justify-center tw-items-center"
onClick={e => e.stopPropagation()}
>
<BiFileEarmarkPdf className="tw-text-center tw-text-gray-500/50 tw-text-lg tw-m-1" />
</div>
);
}
};
Changing the code to split into pages
const generatePDF = async () => {
if (!pdfLayoutRef.current) {
return;
}
try {
const pdf = new jspdf({orientation: 'p', unit: 'mm', format: 'a4'});
const pageWidth = 210;
const pageHeight = 297;
const pages = pdfLayoutRef.current.children;
for (let i = 0; i < pages.length; i++) {
const page = pages[i] as HTMLElement;
const canvas = await html2canvas(page, {scale: 2.0});
const imgData = canvas.toDataURL('image/png');
if (i > 0) {
pdf.addPage('a4', 'p');
}
pdf.addImage(imgData, 'PNG', 0, 0, pageWidth, pageHeight);
}
pdf.save('Document.pdf');
} catch (error) {
console.error('PDF 생성 중 오류:', error);
}
};
See also
- React
- react-pdf - React renderer for creating PDF files on the browser and server.
- react-to-pdf
- html2canvas - Copies the HTML rendering result to a Canvas.
- jsPDF