DOMINIK FRACKOWIAKWeb Developer
Wszystkie wpisy

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!