Display dynamic data in a PDF using @react-pdf

Viewed 1537

I am building an Electron App that allows user to auto populate a PDF based on fetched data from an internal server. I'm trying to use @react-pdf/renderer to display and create the PDF.

I've been using these two reference pages, official documentation, regarding this topic:

Here's my code:

Home.tsx (abridged)

import PDFDoc from "/components/PDFDoc";

export default function Home(){
  // I'm not sure how to use this as the official docs are limited.
  const [pdfInstance, setPdfInstance] = ReactPDF.usePDF({ document: PDFDoc });

  // I'd display code, here, to display data but it isn't relevant to this case.
  return(
     <PDFViewer
       width="100%"
       height="100%"
       children={PDFDoc}
     />
  )
}

PDFDoc.tsx

const PDFDoc = () => (
  <Document>
    <Page>
      <Text>Test text</Text>
      <Text
        render={({pageNumber, totalPages}) => `${pageNumber} / ${totalPages}`}
        fixed
      />
    </Page>
  </Document>
)
export default PDFDoc;

If I create the PDF in a normal component style similar to the home page, it displays properly within the PDFViewer.

The issue:
Without passing props to the component, how can I send data to the document so as to display dynamic data fetched via REST API.

I'm hoping that someone more experienced with the library can assist me with understanding how to dynamically display data within the PDF.

1 Answers

Yes, the react-pdf documentation about dynamic content.

So having faced the same challenge, here's what I discovered. It should help you ...

If you look at the codebase, under @react-pdf/renderer/index.d.ts (v2.1.1), you'll find two major optional props - render and children - that <Text/> can accept (at least it vital for you to understand them for this use case of passing dynamic content/data)

declare namespace ReactPDF {
  // ...

  interface TextProps extends NodeProps {
    render?: (props: {
      pageNumber: number;
      totalPages: number;
      subPageNumber: number;
      subPageTotalPages: number;
    }) => React.ReactNode;
    children?: React.ReactNode;
    // ...
  }

  /**
   * A React component for displaying text...
   */
  class Text extends React.Component<TextProps | SVGTextProps> {}
  
  // ...
}

declare const Text: typeof ReactPDF.Text;
// ...

export { Text, // ... }

Note this:

  • render prop expects specific props to be passed through it.
    So it cannot accept any kind of data fetched via the API calls unless you're obviously passing it stuff related to no. of pages and total pages etc.

  • On the hand, the children prop is more open versatile in this case.
    The type of "children" expected are based off the React.ReactNode.

type React.ReactNode = boolean | React.ReactChild | React.ReactFragment | React.ReactPortal | null | undefined

So your answer lies in utilizing the children prop. How?

Here's a step-by-step process:

  1. In the Parent component, do this...
const Parent = props => {

  React.useEffect(() => {
    // make API call
    apiCall(...).then(res => {
       if (res) {
         // store your data in a session or local storage
         // Why don't we use "state" and pass it down as props?
         sessionStorage.setItem('data', JSON.stringify(res));
       };
    }).catch(error => ... )
  }, []); // eslint-disable-line react-hooks/exhaustive-deps

  return (...);
};

If you try to pass your fetched data (after the API call) by updating the Parent component state with an aim to pass it to the Child component as props, it does not work!

Why? The behavior is that the passed props are often undefined (I'm yet to understand why!).

Therefore storing the fetched data using those storage methods helps (or if you're using redux you might want to keep it in the store)

  1. In the Child component, do this...
import { Page, Text, View, Document, StyleSheet } from '@react-pdf/renderer';

// Create styles
const styles = StyleSheet.create({
  page: { ... },
  section: { ... }
});

// Create Document Component
export const Child = () => {
  const fetchedData = JSON.parse(sessionStorage.getItem("data"));

  // let's destructure our data
  const { email, gender, username, lastname, firstname, ... } = fetchedData;

  // we define block-scope React Components to be passed into <Text>
  // for example ...
  const LastName = () => `${lastname}`;

  return (
    <Document>
      <Page size="A4" style={styles.page}>
        <View style={styles.section}>
          <Text>Last name:</Text>
          <Text children={<LastName/>} />
        </View>
        <View style={styles.section}>
          <Text>Section #2</Text>
        </View>
      </Page>
    </Document>
  );
};

If you change this line:

<Text children={<LastName/>} />

and replace it with this:

<Text>{lastname}</Text>

It will not work (to your dismay) ... see the "Why?" under Parent component.
Otherwise, here's a screenshot where this approach worked - look towards the bottom-left to see what the Child component renders (that's dynamic).

Related