React-PDF w Next.js
Kilka miesięcy temu poproszono mnie w pracy o stworzenie PDF-a w React/Next. Dlaczego tak? Bo projekt był w Next.js, a PDF miał się tworzyć dynamicznie po dokonaniu zakupu, przyjmując unikalny numer pochodzący z backendu (dowód zakupu). Zadanie wydawało się proste, po krótkim researchu wybrałem bibliotekę React-PDF. Szybko okazało się jednak, że pojawiły się problemy. Gdybyś przypadkiem natknął się na podobne, ten post powinien ci pomóc się z nimi uporać.
Instalacja i pierwsze kroki
Ze względu na to, że React-PDF jest niekompatybilny z nowszymi wersjami React.js, przy instalacji konieczne jest dodanie --legacy-peer-deps:
npm install @react-pdf/renderer --save --legacy-peer-deps
Elementy, z których tworzymy nasz dokument PDF to Document, Page, View (odpowiednik diva), Text, Image. Importujemy je:
import { Document, Page, View, Text, Image, PDFViewer, StyleSheet, Font } from '@react-pdf/renderer';
To jedyne klocki, z jakich budujemy naszego PDF-a.
Fonty, z których chcemy skorzystać, musimy „zarejestrować” w następujący sposób:
Font.register({
family: 'Roboto',
fonts: [
{ src: '/assets/fonts/Roboto-Regular.ttf', fontWeight: 400 },
{ src: '/assets/fonts/Roboto-Medium.ttf', fontWeight: 500 },
{ src: '/assets/fonts/Roboto-Bold.ttf', fontWeight: 700 },
],
});
Struktura dokumentu
Struktura naszego dokumentu jest następująca:
const PDF = () => {
return (
<Document>
<Page>
<View>
{/* Your content here */}
</View>
</Page>
</Document>
);
};
Oczywiście, tak jak już wspominałem, <code>View</code> jest tu odpowiednikiem diva i tak jak z diva tu z niego korzystasz – tworzysz tych <code>View</code> tyle, ile potrzebujesz, zagnieżdżasz jedne w drugich lub nie.
CSS
CSS piszemy na dwa sposoby.
Po pierwsze: „inline”:
<View style={{ display: 'flex', flexDirection: 'row', justifyContent: 'center' }}></View>
Pewnie dziwi cię zapis <code>flexDirection: 'row'</code>. Tak, wiem, że <code>row</code> to domyślna właściwość dla flex-direction, jednak z mojego doświadczenia wynika, że gdy pominiemy ten zapis, React-PDF sprawia problemy.
Drugi sposób, pewnie nieco czystszy:
const styles = StyleSheet.create({
imageWrapper: {
marginLeft: 20
}
});
<View style={styles.imageWrapper}></View>
PDF na stronie
Aby móc renderować PDF na jakiejś stronie, z komponentu, w którym PDF-a tworzymy, eksportujemy następującą funkcję:
const PDFView = () => {
return (
<PDFViewer>
<PDF />
</PDFViewer>
);
};
export default PDFView;
Następnie importujemy tę funkcję w pliku strony, na której PDF ma się wyświetlać, przy czym – WAŻNE, jeśli korzystamy z Next.js – musimy to zrobić w następujący sposób:
const InvoicePDF = dynamic(() => import('./pdf'), {
ssr: false,
});
Nie zapominając przy tym o dodaniu dyrektywy 'use client' w pierwszej linii.
Chodzi o to, że Next.js domyślnie renderuje strony po stronie serwera, a nasz PDF ma się renderować po stronie klienta i musimy o to zadbać.
Następnie korzystacie z <code>InvoicePDF</code>, jak z każdego innego komponentu React:
export default function PDFPage() {
return (
<div className={styles.pdfWrapper}>
<InvoicePDF locale={locale} />
</div>
);
}
Przyciski do pobierania PDF-a
Gdybyś zaś chciał – zamiast lub obok wyświetlania całego PDF-a – wstawić przycisk do jego ściągnięcia, to w Next.js będziesz musiał posłużyć się analogicznym zapisem, by zaimportować PDF-a:
const PDFDownloadLink = dynamic(
() => import('@react-pdf/renderer').then(mod => mod.PDFDownloadLink),
{ ssr: false }
);
A następnie owinąć nim np. przycisk w następujący sposób:
<PDFDownloadLink
document={<PDF locale={locale} />}
fileName={`DominikFrackowiak_CV_${locale}.pdf`}
>
<button className={className}>
{handleTranslation(locale)}
</button>
</PDFDownloadLink>
Voila!