Generating PDFs with React on the server
In this article, you can learn how to generate a PDF document using Firebase Cloud Functions and save it to Firebase Cloud Storage.
Generating PDFs is tedious and can require a lot of work, but, we found react-pdf/renderer which helped us build complex PDF documents using components.
However, the library has a major drawback, it’s 1.5MB minified or 392 kB minified and gzipped, and since we were using it client-side it increased our bundle size significantly. We knew that at some point we will have to rethink the current approach.
We’ll skip creating a Firebase project/function, and jump to PDF generation.
To generate a PDF document react-pdf/renderer has a renderToStream function.
const renderToStream: (document: React.ReactElement<DocumentProps>)
=> Promise<NodeJS.ReadableStream>;
From the function definition, we can see that it accepts a React component and returns a ReadableStream wrapped in a Promise.
save(data: string | Buffer, options?: SaveOptions): Promise<void>;
Cloud Storage save method accepts a string or a Buffer. For this example, we will work with Buffer. This means that we need to convert a ReadableStream to a Buffer.
What is a ReadableStream?
A Stream is a collection of data broken down into smaller chunks that can be processed chunk by chunk. As the name suggests ReadableStream is a Stream from which data can be read. Checking Stream docs we can see that there are two reading modes:
- Flowing mode - data is read from the underlying system automatically and provided to an application as quickly as possible using events.
- Paused mode - the stream.read() method must be called explicitly to read chunks of data from the stream.
For this implementation we choose the flowing mode, so we will write a function that will create a Buffer from a Stream using stream ‘data’ and ‘end’ events.
export const streamToBuffer = async (
stream: NodeJS.ReadableStream,
): Promise<Buffer> => {
return new Promise((resolve, reject) => {
const chunks: Buffer[] = [];
stream.on('data', (data) => {
chunks.push(data);
});
stream.on('end', () => {
resolve(Buffer.concat(chunks));
});
stream.on('error', reject);
});
};
We are filling a Buffer array with the data until the ‘end’ event when we concatenate all chunks together.
A sample pdf component that will display only a string:
type Props = {
data: string;
}
const PdfDocument: React.FC<Props> = ({ data }) => {
return (
<Document>
<Page size="A4">
<View>
<Text>{data}</Text>
</View>
</Page>
</Document>
);
};
export const generatePdfDoc = async (props: Props) =>
await renderToStream(<PdfDocument data={props.data} />);
Everything put together:
export const generatePdf = https.onCall(async (data) => {
const pdfStream = await generatePdfDoc({ data: "Some String" });
const pdfBuffer = await streamToBuffer(pdfStream);
const fileName = `MyFile.pdf`;
await admin
.storage()
.bucket()
.file(fileName)
.save(pdfBuffer);
});
Closing words
We tried different solutions for generating pdf documents and none of them came close to React-pdf in terms of developer experience. React-pdf enabled us to generate pdf documents in a declarative way using React components. Moving the implementation server-side helped us to reduce the application bundle size and increase overall performance. Note that even though the current implementation is using Firebase Functions and Cloud Storage, concepts shown in the article can be applied in any node js server-side framework.
Thank you for your the time to read this blog! Feel free to share your thoughts about this topic in the comments or drop us an email at hello@prototyp.digital.