DEV Community

Cover image for How to create dynamic PDF using React?
Subrat Kumar
Subrat Kumar

Posted on • Edited on

How to create dynamic PDF using React?

Introduction

In this article, we will learn how to generate dynamic PDFs by taking inputs from user. Some of the use-cases include generating invoices, certificates, resumes, reports based on data received, etc.

To enable PDF downloading, we will use react-pdf package which provides useful components like Document, Page, View,Image, Text,PDFDownloadLink, PDFViewer etc.

Let's check each component:

  • Document : This tag represents the PDF document itself and must be the root of our PDF.
  • Page : It represents single page inside the PDF documents and should always be rendered inside Document component only.
  • View : This component helps to build UI for the PDFs. It can be nested inside other views.
  • Image : It's used for displaying network or local (Node only) JPG or PNG images in the PDFs.
  • Text : Used to display text in PDFs. It also supports nesting of other Text components.
  • PDFDownloadLink : It enables generate and download of pdf documents.
  • PDFViewer : It is used for rendering client-side generated documents.

Installations

Create pdf-invoice react app using following cmd:

npx create-react-app react-pdf-invoice
Enter fullscreen mode Exit fullscreen mode

After successful creation of app, use following commands to go to directory and start the project -

cd react-pdf-invoice
npm start
Enter fullscreen mode Exit fullscreen mode

Command to install react-pdf in react app:

  • Using npm
npm install @react-pdf/renderer --save
Enter fullscreen mode Exit fullscreen mode
  • Using yarn
yarn add @react-pdf/renderer
Enter fullscreen mode Exit fullscreen mode

Folder structure:

folder structure

Creating Invoice Form

Since our PDF is dynamic in nature, there will be options to add/delete items, change price/quantity of the products, computation of total amount based on items mentioned.
as a result, we need to take inputs from user and show the data accordingly.

src > components > createInvoice > InvoiceForm.js

import React, { useState } from 'react';
import InvoicePDF from '../getPDF/InvoicePDF';
import { PDFDownloadLink } from '@react-pdf/renderer';
import './styles.css';

const InvoiceForm = () => {

// state for storing info about user creating Invoice
  const [billFrom, setBillFrom] = useState({
    name: '',
    address: '',
    invoiceNumber: '',
  })

// state for capturing info of person who needs to pay
  const [client, setClient] = useState({
    clientName: '',
    clientAddress: '',
  })

// items description containing name, price and quantity
  const [items, setItems] = useState([{ name: '', quantity: 0, price: 0}]);

  const handleBillFromData = (e) => {
    e.preventDefault();
    const {name, value} = e.target;  
    setBillFrom({
      ...billFrom,
      [name] : value
    })
  }

  const handleClientData = (e) => {
    e.preventDefault();
    const {name, value} = e.target;  
    setClient({
      ...client,
      [name] : value
    })
  }

  const handleItemChange = (e, index, field, value) => {
    e.preventDefault();
    const updatedItems = [...items];
    updatedItems[index][field] = value; // updating the item field (using index) according to user's input

    setItems(updatedItems);  // updating the items array
  };

  const handleAddItem = () => {
    setItems([...items, { name: '', quantity: 0, price: 0}]);  // adding new item to items array
  };

  const handleRemoveItem = (index) => {
    const updatedItems = [...items];
    updatedItems.splice(index, 1); // removing the selected item

    setItems(updatedItems);  // updating the items array 
  };

// to compute the items' total amount
  const total = () => {
    return items.map(({price, quantity}) => price * quantity).reduce((acc, currValue) => acc + currValue, 0);
  }

  return (
    <div className="invoice">
      <div>
        <h1 className='title'>Invoice</h1>

        <div className='firstRow'>
          <div className='inputName'>
            <label>Invoice Number:</label>
            <input name="invoiceNumber" className="input" type="text" value={billFrom.invoiceNumber} onChange={handleBillFromData} />
          </div>
        </div>

        <div className='firstRow'>
          <div className='inputName'>
            <label>Name:</label>
            <input name="name" className="input" type="text" value={billFrom.name} onChange={handleBillFromData} />
          </div>
          <div className='inputName'>
            <label>Address:</label>
            <textarea name="address" className="textarea" type="text" value={billFrom.address} onChange={handleBillFromData} />
          </div>
        </div>

        <hr/>

        <h2>Bill To:</h2>

        <div className='firstRow'>
          <div className='inputName'>
            <label>Client Name:</label>
            <input name="clientName" className="input" type="text" value={client.clientName} onChange={handleClientData} />
          </div>
          <div className='inputName'>
            <label>Address:</label>
            <textarea name="clientAddress" className="textarea" type="text" value={client.clientAddress} onChange={handleClientData} />
          </div>
        </div>

        <h2 className='title'>Add Details</h2>
        <div className='subTitleSection'>
          <h2 className='subTitle item'>Item</h2>
          <h2 className='subTitle quantity'>Quantity</h2>
          <h2 className='subTitle price'>Price</h2>
          <h2 className='subTitle action'>Amount</h2>
        </div>

        {items?.map((item, index) => (
          <div key={index} className='firstRow'>
            <input className="input item"
              type="text"
              value={item.name}
              onChange={(e) => handleItemChange(e, index, 'name', e.target.value)}
              placeholder="Item Name"
            />
            <input className="input quantity"
              type="number"
              value={item.quantity}
              onChange={(e) => handleItemChange(e, index, 'quantity', e.target.value)}
              placeholder="Quantity"
            />
            <input className="input price"
              type="number"
              value={item.price}
              onChange={(e) => handleItemChange(e, index, 'price', e.target.value)}
              placeholder="Price"
            />
            <p className='amount'>$ {item.quantity * item.price}</p>
            <button className='button' onClick={() => handleRemoveItem(index)}>-</button>
          </div>
        ))}
        <button className='button' onClick={handleAddItem}>+</button>
        <hr/>

        <div className='total'>
          <p>Total:</p>
          <p>{total()}</p>
        </div>
        <hr/>

        <PDFDownloadLink document={<InvoicePDF billFrom={billFrom} client={client} total={total} items={items} />} fileName={"Invoice.pdf"} >
          {({ blob, url, loading, error }) =>
                loading ? "Loading..." : <button className='button'>Print Invoice</button>
              }
        </PDFDownloadLink>
      </div>
    </div>
  );
};

export default InvoiceForm;

Enter fullscreen mode Exit fullscreen mode

Storing User's Info

const handleBillFromData = (e) => {
    e.preventDefault();
    const {name, value} = e.target;  
    setBillFrom({
      ...billFrom,
      [name] : value
    })
  }
Enter fullscreen mode Exit fullscreen mode

In the above-mentioned code, we are storing the information (name, invoiceNumber, address) of the person who is creating an Invoice.

Storing Client's Info

const handleClientData = (e) => {
    e.preventDefault();
    const {name, value} = e.target;  
    setClient({
      ...client,
      [name] : value
    })
  }
Enter fullscreen mode Exit fullscreen mode

In the above code-block, the function will update the Client's information (name, address) who needs to pay the total amount based on user's input.

Create/Update/Delete operations on Items

  const handleItemChange = (e, index, field, value) => {
    e.preventDefault();
    const updatedItems = [...items];
    updatedItems[index][field] = value; // updating the item field (using index) according to user's input

    setItems(updatedItems);  // updating the items array
  };

  const handleAddItem = () => {
    setItems([...items, { name: '', quantity: 0, price: 0}]);  // adding new item to items array
  };

  const handleRemoveItem = (index) => {
    const updatedItems = [...items];
    updatedItems.splice(index, 1); // removing the selected item

    setItems(updatedItems);  // updating the items array 
  };
Enter fullscreen mode Exit fullscreen mode

handleAddItem() will add a new item having itemName, quantity, price as input fields whenever user clicks on + button.

handleRemoveItem() will remove the selected item when user clicks on - button.

handleItemChange() will update the selected item by grabbing index of that particular item with the values (input by the user).

Props of PDFDownloadLink

<PDFDownloadLink document={<InvoicePDF billFrom={billFrom} client={client} total={total} items={items} />} fileName={"Invoice.pdf"} >
          {({ blob, url, loading, error }) =>
                loading ? "Loading..." : <button className='button'>Print Invoice</button>
              }
        </PDFDownloadLink>
Enter fullscreen mode Exit fullscreen mode

Above mentioned code-snippet is responsible for generating PDF document using all the inputs taken by the user.

  • document: To implement PDF document functionality
  • filename: Name of the PDF once downloaded
  • style: Tag for adding styling

Adding Styling in Invoice Form

src > components > createInvoice > styles.css

.invoice {
    display: flex;
    padding: 10px;
    margin: 20px;
    border-radius: 12px;
    justify-content: center;
    width: 1200px;
    box-shadow: rgba(0, 0, 0, 0.35) 0px 5px 15px;
}

.title {
    font-size: 30px;
}

.firstRow {
    display: flex;
    justify-content: space-between;
    flex-grow: 1;
}

.inputName {
    display: flex;
    flex-direction: column;
}

.total {
    display: flex;
    justify-content: space-between;
    font-size: 20px;
}

.total > p {
    align-items: center;
    justify-content: center;
    font-weight: 700;
}

.inputName > label {
    font-size: 20px;
    font-weight: 600;
    margin-right: 5px;
    text-align: left;
}

.input, .textarea{
    font-size: 20px;
    border-radius: 5px;
    padding-left: 10px;
    margin: 10px 0px;
}

.subTitleSection {
    background: bisque;
    display: flex;
    justify-content: space-between;
    border-radius: 12px;
    flex-grow: 1;
    padding-right: 15px;
}

.subTitle {
    font-size: 20px;
    font-weight: 700;
    margin: 10px 10px;
    text-align: left;
}

.item {
    width: 50%;
}

.quantity {
    width: 15%;
}

.price {
    width: 15%;
}

.action {
    width: 10%;
}

.amount {
    font-size: 18px;
    font-weight: 700;
}

.remove {
    border-radius: 12px;
    height: 20px;
    width: 20px;
}

.button {
    background-color: #405cf5;
    border-radius: 6px;
    border-width: 0;
    box-sizing: border-box;
    color: #fff;
    cursor: pointer;
    font-size: 18px;
    height: 44px;
    padding: 0 25px;
}
Enter fullscreen mode Exit fullscreen mode

Invoice Form UI

Invoice Form UI

  • Invoice with multiple items: Invoice UI with multiple items

Generating PDF Document based on Invoice data

Once we get the required data from user's end, we feed the data to the component responsible for generating pdf document. In our case, InvoicePDF is that component.

src > components > getPDF > InvoicePDF.js

import React from 'react';
import { Page, Text, View, Document, StyleSheet } from '@react-pdf/renderer';
import ItemsTable from './ItemsTable';

const styles = StyleSheet.create({
  page: {
    flexDirection: 'column',
    padding: 20,
  },
  name: {
    flexDirection: 'column',
    justifyContent: 'flex-start',
    fontSize: 22,
    marginBottom: 5
  },
  invoiceNumber: {
    flexDirection: 'column',
    justifyContent: 'flex-start',
  },
  section: {
    margin: 10,
    padding: 10,
    flexGrow: 1,
  },
  header: {
    fontSize: 24,
    marginBottom: 10,
    textAlign: 'center'
  },
  label: {
    fontSize: 12,
    marginBottom: 5,
  },
  input: {
    marginBottom: 10,
    paddingBottom: 5,
  },
  client: {
    borderTopWidth: 1,
    marginTop: 20,
    marginBottom: 10
  },
});

const InvoicePDF = ({ billFrom, client, total, items }) => { // destructuring props
  return (
    <Document>
      <Page size="A4" style={styles.page}>
        <View>
          <Text style={styles.header}>Invoice Form</Text>
          <View>
            <Text style={styles.name}>{billFrom.name}</Text>
          </View>

          <View style={styles.invoiceNumber}>
            <Text style={styles.label}>INVOICE NO.</Text>
            <Text style={styles.input}>{billFrom.invoiceNumber}</Text>
          </View>

          <View style={styles.invoiceNumber}>
            <Text style={styles.label}>ADDRESS</Text>
            <Text style={styles.input}>{billFrom.address}</Text>
          </View>

          <View style={styles.client}></View>

          <Text style={styles.label}>BILL TO</Text>

          <View>
            <Text style={styles.name}>{client.clientName}</Text>
          </View>

          <View style={styles.invoiceNumber}>
            <Text style={styles.label}>CLIENT ADDRESS</Text>
            <Text style={styles.input}>{client.clientAddress}</Text>
          </View>

          <ItemsTable items={items} total={total} />

        </View>
      </Page>
    </Document>
  );
};

export default InvoicePDF;
Enter fullscreen mode Exit fullscreen mode

Create Items Table listing all the products

src > components > getPDF > ItemsTable.js

import React from 'react'
import { Text, View, StyleSheet } from '@react-pdf/renderer';

const styles = StyleSheet.create({
    row: {
        flexDirection: 'row',
        borderBottomWidth: 1,
        backgroundColor: '#D3D3D3',
        borderTopColor: 'black',
        borderTopWidth: 1,
        borderBottomColor: 'black',
        fontStyle: 'bold',
        alignItems: 'center',
        height: 22,
      },
      quantity: {
        width: '10%',
        borderRightWidth: 1,
        textAlign: 'right',
        borderRightColor: '#000000',
        paddingRight: 10,
      },
      description: {
          width: '60%',
          borderRightColor: '#000000',
          borderRightWidth: 1,
          textAlign: 'left',
          paddingLeft: 10,
      },
      price: {
        width: '15%',
        borderRightColor: '#000000',
        borderRightWidth: 1,
        textAlign: 'right',
        paddingRight: 10,
      },
      amount: {
        width: '15%',
        textAlign: 'right',
        paddingRight: 10,
      },
      total: {
        display: 'flex',
        flexDirection: 'row',
        justifyContent: 'space-between',
        borderBottomColor: 'black',
        borderBottomWidth: 1,
      }
})

const ItemsTable = ({ items, total }) => { // destructuring props
  return (
    <View>
        <View style={styles.row}>
            <Text style={styles.description}>Item Description</Text>
            <Text style={styles.quantity}>Qty</Text>
            <Text style={styles.price}>Price</Text>
            <Text style={styles.amount}>Amount</Text>
        </View>

        { 
            items.map((item, index) => (
            <View key={index} style={styles.row}>
                <Text style={styles.description}>{item.name}</Text>
                <Text style={styles.quantity}>{item.quantity}</Text>
                <Text style={styles.price}>{item.price}</Text>
                <Text style={styles.amount}>$ {item.quantity * item.price}</Text>
            </View>
        ))}

        <View style={styles.total}>
            <Text>Total: </Text>
            <Text>$ {total()} </Text>
        </View>
    </View>

  )
}

export default ItemsTable;
Enter fullscreen mode Exit fullscreen mode

Updated App.js

src > App.js

import './App.css';
import InvoiceForm from './components/createInvoice/InvoiceForm';

function App() {
  return (
    <div className="App">
      <InvoiceForm />
    </div>
  );
}

export default App;

Enter fullscreen mode Exit fullscreen mode

Output

On clicking Print Invoice, a pdf document named Invoice.pdf gets downloaded with the following structure:

final invoice

Conclusion

With this article, we got to know how to create a dynamic PDF document with the help of react-pdf based on user's inputs. To gain more insights, play around with the other components.

References

https://react-pdf.org/

Top comments (3)

Collapse
 
jacktt profile image
JackTT • Edited

Could you share your complete repository? By the way, I've also written an article about 'Creating Dynamic Thumbnails,' which you can read here: dev.to/jacktt/makding-dynamic-webs...
Image description

Collapse
 
mitchiemt11 profile image
Mitchell Mutandah

Great article!