Generating Pdf documents in React

Written by Lenvin Gonsalves.
on April 21, 2021

Initially, I thought, it would be easy to generate pdf from React, as there are libraries available for each and everything in npm (Even fart noises). The problem isn't the lack of libraries, but the problem was they weren't meant for my use-case. Let us first go through the existing solutions.

If you are here only for the final code, skip to the result.

React-pdf

This is probably the best library to use if you are printing something similar to a newspaper, resume, or magazine, It has many components which can be put together, and then exported as a pdf, but if you plan on re-using your existing react components or even something as trivial as a table, then you are out of luck. You can find the library here

Advantages

  • Easy to use API.
  • Works even in react-native.
  • Has a pdf viewer as well.
  • Exports the pdf in multiple ways.

Disadvantages

  • Can't use existing React components (Like tables).
  • You can only use the components provided by the library.

React to PDF

This library is more suited to my use case, as it supported using custom components, which meant no rewriting components (Yay!). But the drawback was, it would generate a pdf with all the components on a single page (no page breaking) and the pdf just contains a screenshot of the components. You can find the library here

Advantages

  • You can use any existing components.
  • You just need to pass the ref of the component to be printed.

Disadvantages

  • PDF is not vectorized ( You can't select the text in a PDF viewer).
  • IF you have a lot to put in the PDF, then the output will have a very long single page.

url-to-pdf-api

Using this library, you can easily set up a microservice which will take the required URL as a query parameter, along with page size & various other customization options. You can find the library here

Advantages

  • A single microservice can be used across all the applications in an organization.
  • The code isn't at the mercy of browsers.

Disadvantages

  • Setting up a microservice for just generating a PDF can be a deal-breaker for some cases.

Finding an easier approach

Well, as sad I was, I still had to implement this save as pdf functionality. After some brainstorming, I came up with the following approach.

  • Create a new window (which will contain only the components to be printed).
  • Get the components required (Passing refs as props or by using getElementById).
  • Clone the components to the new window (with styles).
  • Trigger print from the new window.

In short, we are creating a new HTML document, with only the components we want to print.

Implementation

We will iterate the implementation by writing pseudocode, and then converting it step by step to real code.

function PrintButton({ refsToPrint }) {

  const printFunction = () => {
      //TODO : Add functionality to print passed nodes (refs)
  };

  return <button onClick={printFunction}> Print </button>;
}

As you can see, we will be taking only one prop, which is the refs to the components that need to be printed (refsToPrint), the input will be in a form of an array [ref1, ref2 ...]. You can assign a ref to a component in the following way

function App(){
    const tableRef = React.useRef()

    return(
        <table ref={tableRef}>
          // TODO - complete code for table
        </table>
    )
}

And pass the refs to the PrintButton component (The component which will generate the PDF) as follows, For brevity, in this tutorial, we will be passing only one ref (only printing one table/component).

    <PrintButton refsToPrint={[tableRef]}>

Now, let's fill the PrintButton component's printFunction function. We will be creating a new window, write the basic HTML tags like body, title, head. Then we will get the body node via getElementById and use appendChild to add the clone of the component. Then we will use print() to call the browser's print option (which will have the Save as PDF option).

  const printFunction = () => {
    const printWindow = window.open("", "", "height=400,width=800");
    printWindow.document.write(
      "<html><head><title>Page Title</title></head><body id='print-body'>"
    );
    const body = printWindow.document.getElementById("print-body");
    refsToPrint.map((ref) => {
      const clone = ref.current.cloneNode(true);
      return body.appendChild(clone);
    });
    printWindow.document.write("</body></html>");
    printWindow.document.close();
    printWindow.print();
  };

But the problem is, using appendChild() only the markup is cloned into the new document. For getting the styles, we can use the getComputedStyles DOM method.

    refsToPrint.map((ref) => {
      const clone = ref.current.cloneNode(true);
      clone.styles.cssText = document.defaultView.getComputedStyle(ref.current, null);
      return body.appendChild(clone);
    });

Here again, the problem is that only styles of the topmost node will be copied, the child nodes will not get their styles. To overcome this problem, we will have to iterate over each child node and copy the styles, for this we will introduce a new function deepCloneWithStyles.

  const deepCloneWithStyles = (node) => {
    const style = document.defaultView.getComputedStyle(node, null);
    const clone = node.cloneNode(false);
    if (clone.style && style.cssText) clone.style.cssText = style.cssText;
    for (let child of node.childNodes) {
      if (child.nodeType === 1) clone.appendChild(deepCloneWithStyles(child));
      else clone.appendChild(child.cloneNode(false));
    }
    return clone;
  };

  const printFunction = () => {
    const printWindow = window.open("", "", "height=400,width=800");
    printWindow.document.write(
      "<html><head><title>Page Title</title></head><body id='print-body'>"
    );
    const body = printWindow.document.getElementById("print-body");
    refsToPrint.map((ref) => {
      const clone = deepCloneWithStyles(ref.current);
      return body.appendChild(clone);
    });
    printWindow.document.write("</body></html>");
    printWindow.document.close();
    printWindow.print();
  };

Now, we have implemented a way to print as PDF without using any library. The user is now free to get the PDF in A4, A3, Letter, Portrait, Landscape, or in whatever way he/she requires.

Result

You can copy the code from the sandbox below, If you have any improvements, suggestions, or doubts, Hit me up at lenvingonsalves@gmail.com