Print stylesheets make it straightforward to generate a printable page from a text-heavy web app. However, for data-centric apps with interactive maps and charts, like the apps we build here at Azavea, generating a printable document that is well-formatted and reflects the app’s current state can be challenging.
In the past, we have used a series of promises (or worse, setTimeouts) to ensure the app was property staged for printing. For a recent project, we decided to take a different approach and use a React-specific library for creating a PDF.
The client wanted to use the PDF for presentations and reports and have the PDF reflect the state of the app. The app contained a Mapbox GL JS map created with ReactMapGL, and some charts rendered in SVGs. After evaluating the landscape of React PDF libraries, we decided to use React-pdf because of its advanced features.
The complete example code of our implementation can be found here. Below, I’ll break down each step of the implementation process.
PDF implementation process
1. Create a PDF document
There are a few ways to render a PDF document using React-pdf. We chose to use usePDF hook, which makes it easy to regenerate the PDF when certain components of the page are updated. In our demo code, this is set up here.
2. Generate an image from the map
React-pdf has a limited set of components for constructing a PDF. This means it can’t directly render a Mapbox map. However, we can generate an image of the map and render it in the PDF using the Image component.
To generate an image from the map canvas, you must set the preserveDrawingBuffer map option to “true”. This setting has to be set when the map is first initialized, and can’t be toggled on and off. You’ll also need a ref to the map to be able to access the canvas DOM element.
Using the toDataUrl method of the canvas element, we can generate a base64-encoded PNG string. The base64 string can then be provided to the PDF component as a prop and set as the image src prop.
const updateMapImage = () => {
const mapImage = mapRef?.current
?.getMap()
.getCanvas()
.toDataURL('image/png');
setMapImage(mapImage);
};
3. Ensure the map image is kept up-to-date
There are two instances in which the map image needs to be updated: when the map is moved, and when the data on the map changes. You can listen to the Mapbox GL JS events to update the image when this happens. We also debounced the event listeners to ensure they aren’t being fired too frequently.
4. Only re-render the PDF when necessary
While updating the map image is relatively fast, updating the PDF is not. Recreating the PDF every time the map image is updated is expensive and can significantly degrade performance to the point where interacting with the application is laggy and slow. Instead, we only want to recreate the PDF when it is needed, which is when the user clicks the download button. This can be accomplished with a pair of component state variables that operate like a state machine.
One last note: the map attribution must follow the map content wherever it goes, including in the PDF. You can do this manually by making use of the React-pdf Text component and copying the text of the Mapbox attribution element.