Creating PDFs with React

Creating PDFs with React

Table of Contents

Motivation

Creating PDFs using Puppeteer and some templating engine is rather simple and useful. Template engines like handlebars also support if else statements & custom functions defined by the developer but I wanted to use a different approach and have a more in-depth control over the data and use code splitting and have fun.

Tools & Packages

I'll be using yarn as package manager in this article, if you're using npm check out this link

Setting up the project

With project-chef cli ( no configuration time )

If you're using project-chef cli you can follow the two quick steps and skip to Rendering React Components to HTML String

  • First run project-chef cli using project-chef command then select

    backend > react-for-server-side

  • Then install puppeteer

    yarn add puppeteer
    

Normal way to setup your project

  • First init a new nodejs project by creating a new folder and running

    yarn init
    

    or

    npm init
    
  • Install babel and other packages

    yarn add react react-dom puppeteer
    
    yarn add -D @babel/cli @babel/core @babel/node @babel/plugin-proposal-class-properties @babel/plugin-transform-async-to-generator @babel/plugin-transform-runtime @babel/preset-env @babel/preset-react @babel/polyfill
    
  • Create babel.config.json in the root of your project and paste the following

    {
    "presets": ["@babel/preset-env", "@babel/preset-react"],
    "plugins": [
      "@babel/plugin-transform-async-to-generator",
      "@babel/plugin-proposal-class-properties",
      "@babel/plugin-transform-runtime",
    ]
    }
    

With that done, let's dive into coding!

Rendering React to HTML

We are only using react to create a simple html page to print it to pdf using puppeteer ( which I'll explain in the following section ), so we don't need to render the react components into a webpage.

Let's create a new folder under src named renderer

First creating our components in App.js in renderer

renderer/App.js

import React from "react";

const data = [
    { name: "Luke Skywalker", img: "https://upload.wikimedia.org/wikipedia/en/9/9b/Luke_Skywalker.png" },
    { name: "Obi-Wan (Ben) Kenobi", img: "https://upload.wikimedia.org/wikipedia/en/3/32/Ben_Kenobi.png" },
    { name: "Darth Vader", img: "https://upload.wikimedia.org/wikipedia/tr/8/83/DarthVader.JPG" },
];

function Character({ name, img }) {
    return (
        <div>
            <img src={img} alt={name} height={250} />
            <h3>{name}</h3>
        </div>
    );
}

function App() {
    return (
        <html>
            <h1>Some Star Wars Characters</h1>
            {data.map((char, i) => (
                <Character {...char} key={i} />
            ))}
        </html>
    );
}

export default App;

Here we've created two basic react components and some data to show.

Then we create an index.js inside renderer

In renderer/index.js

import React from "react";
import { renderToStaticMarkup } from "react-dom/server";
import App from "./App";

export function render() {
    // rendering react component to html string
    return renderToStaticMarkup(<App />);
}

This is the most important part, because we're not rendering the react component into an html page, instead of that we're rendering our react component into a html string

import { renderToStaticMarkup } from "react-dom/server";

Here we import the renderToStaticMarkup function from react-dom/server to render our component to html string.

renderToStaticMarkup(<App />);

In the render function we call the renderToStaticMarkup function and call our App component using JSX. (like we normally do in renderDOM)

Output of render function will be:

<html>
  <h1>Some Star Wars Characters</h1>
  <div>
    <img
      src="https://upload.wikimedia.org/wikipedia/en/9/9b/Luke_Skywalker.png"
      alt="Luke Skywalker"
      height="250"
    />
    <h3>Luke Skywalker</h3>
  </div>
  <div>
    <img
      src="https://upload.wikimedia.org/wikipedia/en/3/32/Ben_Kenobi.png"
      alt="Obi-Wan (Ben) Kenobi"
      height="250"
    />
    <h3>Obi-Wan (Ben) Kenobi</h3>
  </div>
  <div>
    <img
      src="https://upload.wikimedia.org/wikipedia/tr/8/83/DarthVader.JPG"
      alt="Darth Vader"
      height="250"
    />
    <h3>Darth Vader</h3>
  </div>
</html>

You might've been thinking where's the css of the page, that's an another topic I'll write about in my next article.

What is Puppeteer ?

Puppeteer is package that helps us control a headless chromium instance.

Creating PDFs using puppeteer

We know that puppeteer uses a chromium instance, so we just need to open our html page and print it to pdf like we are in a normal browser.

Create a createPdf.js in src:

import puppeteer from "puppeteer";

// Creates a pdf document from htmlContent and saves it to outputPath
export async function createPdf(outputPath, htmlContent) {
    // launchs a puppeteer browser instance and opens a new page
    const browser = await puppeteer.launch();
    const page = await browser.newPage();

    // sets the html of the page to htmlContent argument
    await page.setContent(htmlContent);

    // Prints the html page to pdf document and saves it to given outputPath
    await page.emulateMediaType("print");
    await page.pdf({ path: outputPath, format: "A4" });

    // Closing the puppeteer browser instance
    await browser.close();
}

The important points to catch here:

  • We're setting the html content of the empty browser page, using the html string rendered by our react component
// sets the html of the page by htmlContent argument
await page.setContent(htmlContent);
  • And we're calling the page.pdf method to print the current browser page to pdf.
await page.pdf({ path: outputPath, format: "A4" });

Let's put everything together

Create an index.js in src:

import { render } from "./renderer";
import { createPdf } from "./createPdf";

async function main() {
    // creating the html string using react
    const html = render();

    // printing page to pdf using puppeteer
    await createPdf("./output.pdf", html);
}

main();

Here we've just called the two functions, we've declared in

  • renderer/index.js - render function: Renders react to html string
  • createPdf.js - createPdf function: Creates the pdf with given html

Running the code

  • First add a start script in package.json
    {
      // ... package.json
      "scripts": {
          "start": "babel-node src/index.js"
      }
    // ... package.json
    }
    
  • Then run the code with
    yarn start
    
  • If no errors occurred, you should see an output.pdf in the root of you project

What about styling the pdf then ???

I'll be writing about it in my next article! I'll put the link here when it is ready!

Finished Code

You can find the full code for this article here

I hope this article gave you a different approach creating pdf files using javascript

Credits

Photo by NASA on Unsplash