Using React and styled-components to generate PDFs
Tags: react, webdev, html, css
Published: 2021-08-13 21:36:53

Anvil's [PDF generation API endpoint](/docs/api/generate-pdf) supports generating PDFs from HTML and CSS. For the sake of simplicity and interoperability, the endpoint only accepts vanilla HTML & CSS. Obviously writing vanilla HTML & CSS is painful and far out of fashion.

Modern technologies like React, Vue, LESS, SASS, styled-components, etc. allow you to write more modular, reusable code. Fortunately these technologies all _compile down to vanilla HTML and CSS_.

So, while the endpoint accepts only vanilla HTML and CSS, you can use _any technologies you'd like_ to generate the HTML and CSS then send it to the API. For example, you can use the aforementioned libraries like React and Vue, or even your crazy homegrown template language. As long as a library can generate HTML and CSS strings, you can use it with the API!

In this post, I'll show you how to use React and styled-components in Node to create several PDFs, then ultimately [an invoice PDF](https://github.com/anvilco/html-pdf-invoice-template/tree/main/react-pdf).

The gist of the post is: use the React and styled-components libraries to generate vanilla HTML + CSS strings, then send those strings to the API. Let's get started.

## Set up the scaffolding

The first thing we'll do is set up a quick node script to generate a PDF from plain HTML and CSS. We'll use vanilla HTML and CSS strings at first, then we'll incrementally layer in React and styled-components.

First thing, make sure you have an [API key](/docs/api/getting-started#api-key), then install the Anvil [Node API client](https://github.com/anvilco/node-anvil):

```sh
yarn add '@anvilco/anvil'
# or
npm install '@anvilco/anvil'
```

Then this script will generate the PDF:

```js
// generate-pdf.js script

import fs from 'fs'
import path from 'path'
import Anvil from '@anvilco/anvil'

const apiKey = 'YOUR_ANVIL_API_KEY'

// Subsequent code samples will focus on modifying
// this buildHTMLToPDFPayload() function
function buildHTMLToPDFPayload () {
  // We'll change these lines soon!
  const html = '<div>Hello World</div>'
  const css = ''
  return {
    data: {
      html,
      css,
    },
  }
}

async function main () {
  const client = new Anvil({ apiKey })
  const exampleData = buildHTMLToPDFPayload()
  const { statusCode, data, errors } = await client.generatePDF(exampleData)

  if (statusCode === 200) {
    fs.writeFileSync('output.pdf', data, { encoding: null })
  } else {
    console.log(statusCode, JSON.stringify(errors || data, null, 2))
  }
}

main()
```

Run this script, and you'll see the following output. Great job!

![Hello world HTML to PDF](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/3rr50p490f8un9bsjhra.png)
*Hello world HTML to PDF*

## Add babel

Using React `jsx` syntax in node requires the use of `babel`. I assume most readers will have `babel` setup already. If you do, skip this section!

What follows is a super minimal install to get you up and running. Your production environment will likely be much more robust. The first step is installing two core packages, then a couple of presets&mdash;the most important being `@babel/preset-react`.

```sh
yarn add -D @babel/core @babel/node @babel/preset-env @babel/preset-react
# or
npm install --save-dev @babel/core @babel/node @babel/preset-env @babel/preset-react
```

The last step is adding a `.babelrc` file to the root of your project that uses the installed presets:

```js
{
  "presets": [
    "@babel/preset-env",
    "@babel/preset-react",
  ]
}
```

Now you can run the script with `yarn babel-node generate-pdf.js`.

## Add React

Time to generate some HTML with React. We'll be using server-side rendering via `react-dom/server`.

Install `react` and `react-dom`

```sh
yarn add react react-dom
# or
npm install react react-dom
```

Now we use `ReactDOMServer.renderToStaticMarkup` to generate the HTML. We're using `renderToStaticMarkup` instead of `renderToString` because `renderToStaticMarkup` omits some attributes React uses for dynamic updates. Since we're generating a PDF, there are no dynamic updates, so we have no use for the extra attributes.

```js
// Import React packages
import React from 'react'
import ReactDOMServer from 'react-dom/server'

// The component to render
const Hello = () => (
  <div>Hello React!</div>
)

function buildHTMLToPDFPayload () {
  // Then generate an HTML string!
  const html = ReactDOMServer.renderToStaticMarkup(
    <Hello />
  )
  // css is still the same for now...
  const css = ''
  return {
    data: {
      html,
      css,
    },
  }
}
```

Run the script and you're cooking with gas (React):

![Hello react HTML to PDF](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/fay7if0ay5qhjdngt0uk.png)
*Hello react HTML to PDF*

## Add styled-components

Next up is the CSS. We will generate the CSS string with `styled-components`. First install `styled-components`:

```sh
yarn add styled-components
# or
npm install styled-components
```

Like React, `styled-components` has server rendering abilities. Our path forward will be to create a new `ServerStyleSheet`, render `sheet.collectStyles(<YourComponent />)`, then get all the styles as a string.

A quick snippet shows how it interacts with React.

```js
import { ServerStyleSheet } from 'styled-components'

// Generate the HTML, taking care to render the
// component with styled-components
const sheet = new ServerStyleSheet()
const html = ReactDOMServer.renderToStaticMarkup(
  sheet.collectStyles(
    <Hello />
  )
)
const css = sheet.instance.toString()
```

Then, here's that snippet in context with some actual styling:

```js
// Import styled-components
import styled, { ServerStyleSheet } from 'styled-components'

// Use styled components
const Container = styled.div`
  font-size: 20px;
`

const Magenta = styled.span`
  color: magenta;
`

const Blue = styled.span`
  color: blue;
`

const Hello = () => (
  <Container>
    Ooh, <Magenta>so</Magenta> <Blue>pretty</Blue>!
  </Container>
)

function buildHTMLToPDFPayload () {
  // Generate the HTML, taking care to render the
  // component with styled-components
  const sheet = new ServerStyleSheet()
  const html = ReactDOMServer.renderToStaticMarkup(
    sheet.collectStyles(
      <Hello />
    )
  )

  // Finally, get the CSS as a string
  const css = sheet.instance.toString()

  return {
    data: {
      html,
      css,
    },
  }
}
```

The result:

![React HTML and styled components to PDF](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/n081d7jyrsng0qiom093.png)
*React & styled components to HTML & CSS to PDF*

## Insert global styles

You will likely need to inject a few global styles into the PDF. For example, you may want to set the font size, color, page details, etc. You can leverage styled-components’ `createGlobalStyle()` function. In the last example, we set the `Container`'s font-size to `20px`, but we could just as well do that styling in the `body` rule.

Here's a simplified snippet:

```js
import styled, { ServerStyleSheet, createGlobalStyle } from 'styled-components'

const GlobalStyle = createGlobalStyle`
  body {
    font-size: 20px;
  }
`
const Hello = () => ( /* render hello */ )

const sheet = new ServerStyleSheet()
const html = ReactDOMServer.renderToStaticMarkup(
  sheet.collectStyles(
    <>
      <GlobalStyle />
      <Hello />
    </>
  )
)
const css = sheet.instance.toString()
```

Using this in our script gives us the same result as the last example:

![React HTML and styled components to PDF](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/z05ufn5njvjxyuxcgra0.png)
*Same result with global styles*


## The whole script

Here we are with the whole enchilada: React, `styled-components`, and global styles. This script contains everything you need to render whatever you want to PDF by way of React and styled-components.

```js
import fs from 'fs'
import path from 'path'
import Anvil from '@anvilco/anvil'

import React from 'react'
import ReactDOMServer from 'react-dom/server'
import styled, { ServerStyleSheet, createGlobalStyle } from 'styled-components'

const apiKey = 'YOUR_ANVIL_API_KEY'

const GlobalStyle = createGlobalStyle`
  body {
    font-size: 20px;
  }
`

const Package = styled.span`
  color: magenta;
`

const Hello = () => (
  <div>
    Hello from <Package>React</Package> & <Package>styled-components</Package>!
  </div>
)

function buildHTMLToPDFPayload () {
  const sheet = new ServerStyleSheet()
  const html = ReactDOMServer.renderToStaticMarkup(
    sheet.collectStyles(
      <Hello />
    )
  )
  const css = sheet.instance.toString()
  return {
    data: {
      html,
      css,
    },
  }
}

async function main () {
  const client = new Anvil({ apiKey })
  const exampleData = buildHTMLToPDFPayload()
  const { statusCode, data, errors } = await client.generatePDF(exampleData)

  if (statusCode === 200) {
    fs.writeFileSync('output.pdf', data, { encoding: null })
  } else {
    console.log(statusCode, JSON.stringify(errors || data, null, 2))
  }
}

main()
```

## An invoice example

Hello-world examples are cute and all, but you may want to see a real-life example.

We've created [React to PDF invoice example](https://github.com/anvilco/html-pdf-invoice-template/tree/main/react-pdf) using the approach in this blog post. Along with the tech discussed in this post, the invoice example uses [advanced HTML & CSS](/blog/engineering/advanced-html-to-pdf-api-features/) features like page numbering, header / footer rendering, special table features, etc.

![React and styled-components invoice pdf example](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/9gkpmegn0qgmezun41zq.png)
*Invoice PDF generation example using React & styled-components*

## Summary

You should have all the tools necessary to create PDFs from your own React & styled-components code. You can use other technologies of your choice (e.g. Vue + SCSS) by extrapolating the approach in this post. That is, if it can output HTML and CSS strings, you can use it with the HTML to PDF endpoint.

If you have questions or you're developing something cool with PDFs, let us know at [developers@useanvil.com](mailto:developers@useanvil.com). We’d love to hear from you.