Create PDF documents using React.js
- Last updated on
- 19 Aug 2022
- tags
-
#PDF
-
#React
-
#Flexbox
Two months ago, a customer asked me for a proforma invoice. She wanted her company's accountant to approve our charges before she paid, that way she would avoid issues when claiming the money back from the company. Yet to find a template that I liked half an hour later, I decided to make my own using LibreOffice. It took me three good hours to prepare the invoice.
It was not until two weeks later, when another customer requested a similar document, that I considered automating this task.
- I would be less worried of errors - Some data like invoice number and dates would be generated automatically
- Documents would conform to the brand with minimum effort
- It would save me lots of time.
Choices
This problem looked like one that I could solve with JavaScript even though it wasn't clear how.
The first library that I came across on was PDFlib
. I was quick to rule it out because of it Canvas-like API, as I feared it would take me lots of time to achieve the structure I had in mind. I needed a more familiar approach, one in which I could write the document using HTML elements. I later landed on react-pdf
and I was impressed by the examples.
What we'll build
In this article, I will show you how to make a react component that generates this PDF proforma invoice.
I'll omit the business logic that precedes invoices like order management and instead focus on getting the invoice to look exactly like this.
Install a fresh react project
Now that its clear what we want, lets begin.
Install a fresh react typescript from the terminal/cmd
yarn create react-app invoice --template typescript
Navigate to the creates invoice
folder
cd invoice
Then, install the react-pdf/renderer module using yarn
yarn add @react-pdf/renderer
Pitfall
As of November 2022, installing the@react-pdf/renderer
using npm causes peer dependency mismatch errors with react version 18.
Then, open the invoice
folder in your favourite code editor.
Lets start with the header. Create a file named app.tsx
file in the src
folder. Add the App
and Invoice
to it.
import {
Document,
Page,
PDFViewer,
} from "@react-pdf/renderer";
export default function App() {
return (
<PDFViewer width="100%" height="900">
<Invoice />
</PDFViewer>
);
}
function Invoice() {
return (
<Document>
<Page
size="A4"
style={{ fontFamily: "Helvetica", fontSize: 10 , margin: 40 }}
></Page>
</Document>
);
}
Now create the index.tsx
file. This file acts as the React entry file.
import React from "react";
import ReactDOM from "react-dom/client";
import App from "./app";
const root = ReactDOM.createRoot(document.getElementById("root") as Element);
root.render(
<React.StrictMode>
<App />
</React.StrictMode>
);
Run the development server, from the terminal
yarn start
This will start the server and open the url in your default browser. If everything went well, you will see this in the browser.
Pitfall
The `PDFViewer`, the component responsible for the pdf widget, may not work as expected in Firefox. If having problems, use a chromium-based browser like chrome or edge.Here specified that out pages should be of the legal letter size by supplying the size prop to Page
.
Overview of @react-pdf/renderer
The @react-pdf/renderer library makes is easy to create PDF using the familiar React.js API. Although it does not support the entire HTML and CSS spec, it comes with everything you would ever need to create professional document.
We'll use these components to make the invoice.
Text
: A block level text container. Text not wrapped within this element will not appear on the document.View
: A general block level element that is equivalent to a HTMLdiv
Image
: Equivalent to HTMLimg
Page
: Equivalent to HTMLbody
Document
: Denotes a PDF document. equivalent to HTMLhtml
element.PDFViewer
: Utility to preview PDF documents in the browser.
Styling
React-pdf supports almost all CSS properties that are used to style text. It also comes bundled with three fonts: courier (monospace), and Times-Roman (serif), and Helvetica (sans-serif).
Most importantly, it supports position, margin, padding, and flex properties giving you power to create amazing documents with minimum effort.
Adding the header
Download this image and copy it into the public
folder located in the root of the react project.
Add a header component and render it in invoice, like this.
import {
Document,
Image,
Page,
PDFViewer,
Text,
View,
} from "@react-pdf/renderer";
export default function App() {
return (
<PDFViewer width="100%" height="900">
<Invoice />
</PDFViewer>
);
}
function Header() {
return (
<View
style={{
display: "flex",
flexDirection: "row",
alignItems: "center",
justifyContent: "space-between",
}}
>
<View>
<Image
src={logo}
style={{
width: "96pt",
}}
cache
></Image>
</View>
<Text
style={{
padding: "4pt",
fontSize: "20pt",
fontFamily: "Helvetica-Bold",
}}
>
PROFORMA INVOICE
</Text>
<View>
<Text style={{ marginLeft: "auto", textTransform: "uppercase" }}>
Tenda Soft Ltd
</Text>
<Text style={{ marginLeft: "auto"}}>
Lusaka rd, Nairobi, Kenya
</Text>
<Text style={{ marginLeft: "auto"}}>
+254790589898
</Text>
</View>
</View>
);
}
function Invoice() {
return (
<Document>
<Page
size="A4"
style={{
fontFamily: "Helvetica",
fontSize: "10pt",
backgroundColor: "white",
padding: "40pt",
}}
>
<Header />
</Page>
</Document>
);
}
Pitfall
As opposed what happens in CSS, theflex-direction
property defaults to column
in react-pdf/renderer.
You'll also notice that I am changing the font-family
to render bold text. This is because the fonts weights are provided as individual fonts , this means set font-width
styles have no effect.
Before we continue, let's clean the inline css by using the Stylesheet
helper to create reusable styles.
import {
Document,
Image,
Page,
PDFViewer,
StyleSheet,
Text,
View,
} from "@react-pdf/renderer";
export default function App() {
return (
<PDFViewer width="100%" height="900">
<Invoice />
</PDFViewer>
);
}
var styles = StyleSheet.create({
page: {
fontFamily: "Helvetica",
fontSize: 10,
padding: 40,
},
title: {
padding: "4pt",
fontSize: "20pt",
fontFamily: "Helvetica-Bold",
textTransform: "uppercase",
},
textRight: {
marginLeft: "auto",
},
rowBetween: {
display: "flex",
flexDirection: "row",
justifyContent: "space-between",
},
});
function Invoice() {
return (
<Document>
<Page size="A4" style={styles.page}>
<Header />
</Page>
</Document>
);
}
function Header() {
return (
<View style={styles.rowBetween}>
<View>
<Image
src="/logo.png"
style={{
width: "96pt",
}}
cache
></Image>
</View>
<Text style={styles.title}>PROFORMA INVOICE</Text>
<View>
<Text style={{ ...styles.textRight, textTransform: "uppercase" }}>
Tenda Soft Ltd
</Text>
<Text style={styles.textRight}>Lusaka rd, Nairobi, Kenya</Text>
<Text style={styles.textRight}>+254790589898</Text>
</View>
</View>
);
}
StyleSheet.create()
returns a regular JavaScript that can be extended using the spread syntax, like we have done here.
<Text style={{ ...styles.textRight, textTransform: "uppercase" }}>
Tenda Soft Ltd
</Text>
Sink
I don't plan my styles ahead, that is why I keep them inline as long they are short and not repetitive.I'm reluctant to create a new file for every new component I think of. I have a habit of writing components in a single file until they start to look misplaced and at the moment the Invoice
, looks just that.
Looking at the header component, it seems like something I want to appear on every pdf document, except for the 'INVOICE' title. We can refactor it to a generic component that accepts a title prop.
Next, the way we are using PDFViewer
in app doesn't look right. We are making App
aware of how PDF are rendered, a detail should be none of its business. To resolve this, we create a new file and call it pdf-document.tsx
to hold the pdf viewing logic and template.
import {
Document,
Image,
Page,
PDFViewer,
StyleSheet,
Text,
View,
} from "@react-pdf/renderer";
import React from "react";
const styles = StyleSheet.create({
page: {
fontFamily: "Helvetica",
fontSize: 10,
padding: 40,
},
title: {
padding: "4pt",
fontSize: "20pt",
fontFamily: "Helvetica-Bold",
},
textRight: {
marginLeft: "auto",
},
rowBetween: {
display: "flex",
flexDirection: "row",
justifyContent: "space-between",
},
});
export default function PDFDocument({
children,
title,
}: {
children: React.ReactNode,
title?: string,
}) {
return (
<PDFViewer width="100%" height="900">
<Document>
<Page size="A4" style={styles.page}>
<Header title={title} />
{children}
</Page>
</Document>
</PDFViewer>
);
}
function Header({ title }: { title?: string }) {
title = title || "Proforma Invoice";
return (
<View style={styles.rowBetween}>
<View>
<Image
src="/logo.png"
style={{
width: "96pt",
}}
cache
></Image>
</View>
<Text style={styles.title}>{title}</Text>
<View>
<Text style={{ ...styles.textRight, textTransform: "uppercase" }}>
Tenda Soft Ltd
</Text>
<Text style={styles.textRight}>Lusaka rd, Nairobi, Kenya</Text>
<Text style={styles.textRight}>+254790589898</Text>
</View>
</View>
);
}
Now create a invoice.tsx
and use the PDFDocument
as a template in the Invoice
template.
import { Text, View } from "@react-pdf/renderer";
import React from "react";
import PDFDocument from "./pdf-document";
export default function Invoice() {
return (
<PDFDocument>
<View>
<Text>Body of the invoice</Text>
</View>
</PDFDocument>
);
}
Change the app.tsx
to reflect the changes.
import React from "react";
import Invoice from "./invoice";
export default function App() {
return <Invoice />;
}
Now that everything is cleaner, let's build to the top section that shows the invoice details.
Add the details component into the invoice.tsx
export default function Invoice() {
return (
<PDFDocument>
<Details />
</PDFDocument>
);
}
const styles = StyleSheet.create({
section: {
marginTop: "4vh",
},
row: {
display: "flex",
flexDirection: "row",
},
equalSize: {
flex: "1 1 0%",
},
halfWidth: {
width: "50%",
},
quarterWidth: {
width: "25%",
},
tableCell: {
padding: "4",
},
pushRight: {
marginLeft: "auto",
},
dt: {
fontSize: "8pt",
marginBottom: "4pt",
},
});
function Details() {
return (
<View style={Object.assign({}, styles.section, styles.row)}>
<View style={Object.assign({}, styles.tableCell, styles.halfWidth)}>
<Text style={styles.dt}>Customer</Text>
<Text style={{ textTransform: "uppercase" }}>Janet Kiama</Text>
<Text>Nyayo House, Kenyatta Ave</Text>
<Text>Nairobi, Kenya</Text>
<Text>+254738092027</Text>
<Text>janetkiama2022@gmail.com</Text>
</View>
<View style={styles.equalSize}>
<View style={Object.assign({}, styles.tableCell, styles.equalSize)}>
<Text style={styles.dt}>Invoice number</Text>
<Text>589400</Text>
</View>
<View style={Object.assign({}, styles.tableCell, styles.equalSize)}>
<Text style={styles.dt}>Terms of payment</Text>
<Text>Advance</Text>
</View>
</View>
<View style={styles.equalSize}>
<View style={Object.assign({}, styles.tableCell, styles.equalSize)}>
<Text style={Object.assign({}, styles.dt, styles.pushRight)}>
Issued on
</Text>
<Text style={styles.pushRight}>09 Nov 2022</Text>
</View>
<View style={Object.assign({}, styles.tableCell, styles.equalSize)}>
<Text style={Object.assign({}, styles.dt, styles.pushRight)}>
Due on
</Text>
<Text style={styles.pushRight}>01 Dec 2022</Text>
</View>
</View>
</View>
);
}
Here, we create a table/grid like structure using the CSS flex box.
Sink
To make the width of the children of a flex element equal we set the flex property on each child `flex: 1 1 0%`. To deeply understand theflex
property consider reading Robin Rendle's article Understanding flex-grow, flex-shrink, and flex-basis
We use Object.assign()
to combine styles, mimicking what we would do if we were using a CSS utility library.
To conclude, we'll add the remaining sections using the techniques we learnt.
export function BankDetails() {
return (
<View style={styles.section}>
<View style={Object.assign({}, styles.row, styles.sectionHeading)}>
<Text>HOW TO PAY</Text>
</View>
<View style={styles.row}>
<View style={Object.assign({}, styles.tableCell, styles.equalSize)}>
<Text style={styles.dt}>Bank</Text>
<Text>National Bank of Kenya</Text>
</View>
<View style={Object.assign({}, styles.tableCell, styles.equalSize)}>
<Text style={styles.dt}>Account Number</Text>
<Text>9012909012</Text>
</View>
<View style={Object.assign({}, styles.tableCell, styles.equalSize)}>
<Text style={styles.dt}>Account Name</Text>
<Text>Tenda Soft Ltd</Text>
</View>
<View style={Object.assign({}, styles.tableCell, styles.equalSize)}>
<Text style={styles.dt}>Amount</Text>
<Text>Ksh 117,000.00</Text>
</View>
</View>
</View>
);
}
function Summary() {
let lineItems = [
{
item: "MS Office",
description:
"Installation of Microsoft Office Professional with a lifetime group licence",
quantity: 3,
price: 30000,
amount: 90000,
},
{
item: "Kaspersky",
description: "1 year antivirus protection group licence",
quantity: 3,
price: 3000,
amount: 9000,
},
];
return (
<View style={Object.assign({}, styles.section, styles.tableRow)}>
<View style={Object.assign({}, styles.row, styles.tableRow, styles.bold)}>
<Text style={Object.assign({}, styles.quarterWidth, styles.tableCell)}>
Item
</Text>
<Text style={Object.assign({}, styles.halfWidth, styles.tableCell)}>
Description
</Text>
<Text style={Object.assign({}, styles.width7, styles.tableCell)}>
Qty
</Text>
<Text
style={Object.assign(
{},
styles.width9,
styles.tableCell,
styles.alignRight
)}
>
Price
</Text>
<Text
style={Object.assign(
{},
styles.width9,
styles.tableCell,
styles.alignRight
)}
>
Amount
</Text>
</View>
{lineItems.map((item) => (
<View style={Object.assign({}, styles.row, styles.tableRow)}>
<Text
style={Object.assign({}, styles.quarterWidth, styles.tableCell)}
>
{item.item}
</Text>
<Text style={Object.assign({}, styles.halfWidth, styles.tableCell)}>
{item.description}
</Text>
<Text style={Object.assign({}, styles.width7, styles.tableCell)}>
{item.quantity}
</Text>
<Text
style={Object.assign(
{},
styles.width9,
styles.tableCell,
styles.alignRight
)}
>
{item.price}
</Text>
<Text
style={Object.assign(
{},
styles.width9,
styles.tableCell,
styles.alignRight
)}
>
{item.amount}
</Text>
</View>
))}
<View
style={Object.assign({}, styles.row, styles.bold, styles.justifyEnd)}
>
<Text
style={Object.assign(
{},
styles.width16,
styles.tableCell,
styles.alignRight
)}
>
Total (Ksh)
</Text>
<Text
style={Object.assign(
{},
styles.width9,
styles.tableCell,
styles.alignRight
)}
>
117000
</Text>
</View>
<View
style={Object.assign({}, styles.row, styles.bold, styles.justifyEnd)}
>
<Text
style={Object.assign(
{},
styles.width16,
styles.tableCell,
styles.alignRight
)}
>
Paid (Ksh)
</Text>
<Text
style={Object.assign(
{},
styles.width9,
styles.tableCell,
styles.alignRight
)}
>
0
</Text>
</View>
<View
style={Object.assign({}, styles.row, styles.bold, styles.justifyEnd)}
>
<Text
style={Object.assign(
{},
styles.width16,
styles.tableCell,
styles.alignRight
)}
>
Due
</Text>
<Text
style={Object.assign(
{},
styles.width9,
styles.tableCell,
styles.alignRight
)}
>
117000
</Text>
</View>
</View>
);
}
function Notes() {
return (
<View style={styles.section}>
<Text style={Object.assign({}, styles.bold, styles.paragraph)}>
Notes
</Text>
<Text style={styles.paragraph}>All prices are in kenya shillings</Text>
<Text style={styles.paragraph}>Payments are non refundable</Text>
</View>
);
}
And finally, change the styles to
const styles = StyleSheet.create({
section: {
marginTop: "4vh",
},
row: {
display: "flex",
flexDirection: "row",
},
equalSize: {
flex: "1 1 0%",
},
halfWidth: {
width: "50%",
},
quarterWidth: {
width: "25%",
},
width7: {
width: "7%",
},
width9: {
width: "9%",
},
width16: {
width: "16%",
},
tableCell: {
padding: "4",
},
pushRight: {
marginLeft: "auto",
},
dt: {
fontSize: "8pt",
marginBottom: "4pt",
},
sectionHeading: {
backgroundColor: "#eee",
justifyContent: "center",
paddingVertical: "4",
},
tableRow: {
borderBottom: "1px solid #ccc",
},
bold: {
fontFamily: "Helvetica-Bold",
},
justifyEnd: {
justifyContent: "flex-end",
},
paragraph: {
marginBottom: 10,
},
alignRight: {
textAlign: "right",
},
});
In the next post, we will dynamically generate a PDF proforma invoice on the server by replacing all the static data with props.