DEV Community

Cover image for React & PDF Rendering
Damian Piwowarczyk
Damian Piwowarczyk

Posted on • Edited on

React & PDF Rendering

Portable Document Format (PDF) - developed 30 years ago still exists and is one of the most widely-used documents formats. There are many reasons why people still prefer to use them such as the widely supported document format which works is compatible with many devices and apps, and the content always remains the same format.

What is React-PDF ?

React-pdf lets us render documents on server and web.
It exports a set of React primitives that can be used to render things into documents easily and we can use CSS properties for styling and flexbox for layout. A list of supported primitives can be found here It supports rendering text, images, SVGs and many more.

What we going to build ?

Today we will be looking at how we can create and style PDF with react-pdf renderer. React-pdf package lets us create awesome looking PDFs using React. Its simple to use and the documentation is developer-friendly. We will create a simple application that dynamically updates our PDF-styled template which we render in DOM.

This example will show how you can render the document in DOM and how directly save the document into the file without the need of displaying it.


Demo


1. Setup



npx create-react-app app && cd app && yarn add @react-pdf/renderer


Enter fullscreen mode Exit fullscreen mode

As in the time of writing tutorial react-pdf render need some extra dependencies and craco configuration.



yarn add process browserify-zlib stream-browserify util buffer assert @craco/craco


Enter fullscreen mode Exit fullscreen mode

Change the scripts section in package.json as below:



  "scripts": {
    "start": "craco start",
    "build": "craco build",
    "test": "craco test",
    "eject": "react-scripts eject"
  },


Enter fullscreen mode Exit fullscreen mode

Next, create a new file in the root of the project
craco.config.js with



const webpack = require("webpack");

module.exports = {
  webpack: {
    configure: {
      resolve: {
        fallback: {
          process: require.resolve("process/browser"),
          zlib: require.resolve("browserify-zlib"),
          stream: require.resolve("stream-browserify"),
          util: require.resolve("util"),
          buffer: require.resolve("buffer"),
          asset: require.resolve("assert"),
        },
      },
      plugins: [
        new webpack.ProvidePlugin({
          Buffer: ["buffer", "Buffer"],
          process: "process/browser",
        }),
      ],
    },
  },
};



Enter fullscreen mode Exit fullscreen mode


mkdir Components && cd Components && mkdir PDF && cd PDF && touch Preview.js && touch LeftSection.js && touch RightSection.js


Enter fullscreen mode Exit fullscreen mode


├── App.css
├── App.js
├── index.js
├── PDF
│   ├── LeftSection.js
│   ├── Preview.js
│   └── RightSection.js
└── styles
    └── index.js


Enter fullscreen mode Exit fullscreen mode

In our App.js we will create a state that updates on user input when changes are detected we will re-render our page.


 javascript
import Preview from './PDF/Preview'
import React, { useState } from 'react'
function App() {
  const [profile, setProfile] = useState({
    type: 'Profile',
    name: 'John Doe',
    profession: 'Junior Developer',
    profileImageURL: 'https://i.imgur.com/f6L6Y57.png',
    display: true,
    about: 'About...',
  })

  const handleChange = (name, value) => {
    setProfile({ ...profile, [name]: value })
  }

  return (
    <div
      style={{
        width: '100%',
        height: '100vh',
        display: 'flex',
      }}
    >
      <div style={{ width: '50%' }}>
        <div>
          <label>Name</label>
          <input
            name='name'
            defaultValue={profile.name}
            onChange={(e) => {
              handleChange(e.target.name, e.target.value)
            }}
          />
        </div>
        <div>
          <label>Profession</label>
          <input
            name='profession'
            defaultValue={profile.profession}
            onChange={(e) => {
              handleChange(e.target.name, e.target.value)
            }}
          />
        </div>
        <div>
          <label>ImageURL</label>
          <input
            name='profileImageURL'
            defaultValue={profile.profileImageURL}
            onChange={(e) => {
              handleChange(e.target.name, e.target.value)
            }}
          />
        </div>
        <div>
          <label>About</label>
          <input
            name='about'
            defaultValue={profile.about}
            onChange={(e) => {
              handleChange(e.target.name, e.target.value)
            }}
          />
        </div>
      </div>
      <Preview profile={profile} />
    </div>
  )
}

export default App



Enter fullscreen mode Exit fullscreen mode

Preview.js
This will let us render a preview on half of the page and embed the Template document that we are about to create.
We also have PDFDownloadLink which can be used to download pdf without the need of rendering it in the DOM.



import React from 'react'
import { Document, Page, PDFViewer, PDFDownloadLink } from '@react-pdf/renderer'
import LeftSection from './LeftSection'
import { RightSection } from './RightSection'
import styles from '../styles'

const Preview = ({ profile }) => {
  return (
    <div style={{ flexGrow: 1 }}>
      <PDFViewer
        showToolbar={false}
        style={{
          width: '100%',
          height: '95%',
        }}
      >
        <Template profile={profile} />
      </PDFViewer>
      <PDFDownloadLink
        document={<Template profile={profile} />}
        fileName='somename.pdf'
      >
        {({ loading }) => (loading ? 'Loading document...' : 'Download now!')}
      </PDFDownloadLink>
    </div>
  )
}
// Create Document Component
const Template = ({ profile }) => {
  return (
    <Document>
      <Page size='A4' style={styles.page}>
        // We will divide our document into 2 columns
        <LeftSection profile={profile} />
        <RightSection about={profile.about} />
      </Page>
    </Document>
  )
}

export default Preview





Enter fullscreen mode Exit fullscreen mode

We will also create folder with styles where we will keep stylesSheet for react-render primitives.



mkdir styles && cd styles && mkdir index.js


Enter fullscreen mode Exit fullscreen mode

styles



import { StyleSheet } from '@react-pdf/renderer'

export default StyleSheet.create({
  page: {
    display: 'flex',
    flexDirection: 'row',
  },
  section_right: {
    margin: 10,
    padding: 10,
    paddingTop: 20,
    width: '75%',
  },
  section_left: {
    width: '25%',
    height: '100%',
    backgroundColor: '#084c41',
  },
  profile_container: {
    display: 'flex',
    flexDirection: 'column',
    alignItems: 'center',
    marginTop: '20',
    marginBottom: '20px',
    height: '150',
    fontFamily: 'Helvetica-Bold',
  },
  name_text: {
    paddingTop: '10px',
    paddingBottom: '5px',
    fontSize: '14px',
    fontWeight: '900',
    color: 'white',
  },
  profession_text: {
    color: '#d1d5db',
    fontSize: '11px',
  },
  profile_img: {
    width: '60px',
    height: '60px',
    borderRadius: '90',
  },
  profile_line: {
    marginTop: '10px',
    width: '10%',
    height: '1px',
    backgroundColor: '#FFF',
    textAlign: 'center',
  },
})


Enter fullscreen mode Exit fullscreen mode

LeftSection.js



import { View, Text, Image } from '@react-pdf/renderer'
import styles from '../styles'

export const Profile = ({ profile }) => {
  return (
    <View style={styles.profile_container}>
      <Image style={styles.profile_img} src={profile.profileImageURL} />

      <View
        style={{
          justifyContent: 'center',
        }}
      >
        <Text style={styles.name_text}>{profile.name}</Text>
      </View>
      <Text style={styles.profession_text}>{profile.profession}</Text>
      <View style={styles.profile_line} />
    </View>
  )
}

const LeftSection = ({ profile }) => {
  return (
    <View style={styles.section_left}>
      <Profile profile={profile} />
    </View>
  )
}

export default LeftSection



Enter fullscreen mode Exit fullscreen mode

RightSection.js



import styles from '../styles'
import { View, Text } from '@react-pdf/renderer'

export const RightSection = ({ about }) => {
  return (
    <View style={styles.section_right}>
      <Text>{about}</Text>
    </View>
  )
}


Enter fullscreen mode Exit fullscreen mode

Now you know it works you could create something yourself.

More functional example of a resume builder that I built is here.
Resume builder

To sum up, this is only a simple demo to demonstrate how the pdf renderer can be used with react. React pdf package very cool tool that could be used to create things like resume builders, invoicing templates or tickets or receipts, etc. These could be either generated based on the existing data or dynamically updated on user input like in the case of our simple demo.

I hope this article was helpful to some of you guys. Thanks for reading!
Github repo

Top comments (10)

Collapse
 
jeetiss profile image
Dmitry Ivakhnenko

Your article is so helpful, thanks 🙌🏻

Collapse
 
souravojha profile image
sourav-ojha

Could not resolve dependency:
npm ERR! peer react-scripts@"^4.0.0" from @craco/craco@6.4.3

Collapse
 
przpiw profile image
Damian Piwowarczyk

You need to update your react-script package. As craco@6.4.3 needs at least react-script@4.0.0 to run.
create-react-app.dev/docs/updating...
Hope that helped.

Collapse
 
pedrinho_force profile image
Pedro Henrique Cardoso Lima

Hi @przpiw,

Thanks for the article.I have been trying to play around with React-PDF... and I keep receiving the following error:

Module not found: Error: Can't resolve 'stream' in '/Users/pedrolima/Desktop/React_Projects/app/node_modules/@react-pdf/pdfkit/lib'

BREAKING CHANGE: webpack < 5 used to include polyfills for node.js core modules by default.
This is no longer the case. Verify if you need this module and configure a polyfill for it.

If you want to include a polyfill, you need to:
- add a fallback 'resolve.fallback: { "stream": require.resolve("stream-browserify") }'
- install 'stream-browserify'
If you don't want to include a polyfill, you can use an empty module like this:
resolve.fallback: { "stream": false }

I have looked the error online and I am not yet what I am doing wrong. I would appreciate so much your help.

My settings:
"@craco/craco": "^6.4.3",
"@react-pdf/renderer": "^2.1.1",
"react": "^17.0.2",
"react-dom": "^17.0.2",
"react-scripts": "5.0.0"

Thread Thread
 
pedrinho_force profile image
Pedro Henrique Cardoso Lima

Hi @przpiw ,

When I run your project this is what i get (img below). I don't think that's expected to happen. Right?

Thread Thread
 
rdani95 profile image
Rausz Dániel

Craco doesn't support CRA 5 yet, but there is an open issue about it
github.com/gsoft-inc/craco/issues/378

In the meantime, there is an alpha version (v7.0.0-alpha.0). I could install it with CRA5 app, withouth ejecting.

npm install @craco/craco@v7.0.0-alpha.0

react-pdf-renderer works fine with this version.

CC @souravojha

Collapse
 
anandh profile image
Anandha Krishnan

./node_modules/@react-pdf/font/lib/index.browser.es.js
Attempted import error: 'create' is not exported from 'fontkit' (imported as 'fontkit').
Hi, I got this error while installing and build the react app.
{
"name": "frontend",
"version": "1.0.0",
"private": true,
"dependencies": {
"@craco/craco": "^6.4.3",
"@react-pdf/renderer": "^2.0.21",
"@reduxjs/toolkit": "^1.2.5",
"animate.css": "^4.1.1",
"apexcharts": "^3.29.0",
"apexcharts-clevision": "^3.28.3",
"aws-sdk": "^2.1315.0",
"axios": "^0.24.0",
"axios-mock-adapter": "^1.19.0",
"bootstrap": "5.1.0",
"bs-stepper": "^1.7.0",
"chart.js": "^3.6.0",
"chroma-js": "~2.1.0",
"classnames": "^2.3.1",
"cleave.js": "^1.6.0",
"crypto-js": "^4.1.1",
"draft-js": "^0.11.7",
"draftjs-to-html": "^0.9.1",
"file-saver": "^2.0.2",
"flatpickr": "^4.6.3",
"history": "^5.1.0",
"html-to-draftjs": "^1.5.0",
"html2pdf.js": "^0.10.1",
"i18next": "^21.4.0",
"i18next-browser-languagedetector": "^6.1.2",
"i18next-xhr-backend": "^3.2.2",
"jquery": "^3.5.1",
"jsonwebtoken": "~8.5.1",
"lodash": "^4.17.21",
"moment": "^2.29.1",
"moment-msdate": "^2.0.4",
"nouislider": "^15.5.0",
"nouislider-react": "^3.3.8",
"postcss-rtl": "^1.5.0",
"prismjs": "^1.19.0",
"prop-types": "~15.7.2",
"rc-input-number": "^7.3.3",
"react": "^17.0.2",
"react-apexcharts": "^1.3.9",
"react-chartjs-2": "^3.3.0",
"react-contexify": "^5.0.0",
"react-copy-to-clipboard": "~5.0.2",
"react-country-flag": "^2.0.1",
"react-countup": "^6.4.2",
"react-data-table-component": "^7.4.5",
"react-dom": "^17.0.2",
"react-draft-wysiwyg": "^1.14.5",
"react-dropzone": "^11.4.2",
"react-feather": "~2.0.3",
"react-flatpickr": "^3.9.1",
"react-google-autocomplete": "^2.6.1",
"react-hook-form": "7.18.1",
"react-i18next": "^11.13.0",
"react-paginate": "^7.0.0",
"react-perfect-scrollbar": "^1.5.5",
"react-player": "^2.6.2",
"react-rating": "^2.0.5",
"react-redux": "^7.2.0",
"react-router-dom": "^5.2.0",
"react-scripts": "^4.0.2",
"react-select": "^5.2.0",
"react-shepherd": "^3.3.6",
"react-slidedown": "^2.4.5",
"react-sortablejs": "^6.0.0",
"react-toastify": "^8.0.3",
"reactstrap": "9.0.1",
"recharts": "^2.0.4",
"redux": "^4.0.5",
"redux-debounced": "~0.5.0",
"redux-thunk": "^2.4.0",
},
"scripts": {
"start": "craco start",
"build": "craco build",
"test": "craco test",
"eject": "react-scripts eject",
"lint": "eslint src//*.js src//.jsx",
"lint:fix": "eslint src/
/.js --fix"
},
"eslintConfig": {
"extends": "react-app"
},
"devDependencies": {
"@types/sortablejs": "^1.10.6",
"eslint": "^7.11.0",
"eslint-plugin-import": "^2.22.0",
"eslint-plugin-react": "^7.20.6",
"sass-loader": "8.0.2"
},
"browserslist": {
"production": [
">0.2%",
"not dead",
"not op_mini all"
]
},
"homepage": ""
}
this is my package,json file and webpack version is 4, I tried to upgrade the webpack to 5, it shows some other error, so I plan to move with version 4 and can anyone please help me! and I check with different versions of "@react-pdf/renderer": "^2.0.21", and it also shows error!

Collapse
 
beej profile image
Beej

working online sample: stackblitz.com/edit/react-pdf-demo...

Collapse
 
deepak22448 profile image
Deepak Sharma

Why does @react-pdf/renderer not work on mobile devices? just a blank page is rendered on mobile

Collapse
 
limaocode profile image
João Vitor

Thanks so much, your article helped me a lot!!!
Your craco.config.js helped me with a problem between react v18 and webpack5