How To Print in React Using Iframes

June 4, 2019 (5y ago)

0 views

You want to print the contents of a page. If you use the standard browser API, this will print the entirety of the current page. In this scenario, you can use the print media query to show/hide certain parts of the page.

@media print {
  header,
  footer {
    display: none;
  }
}

What if you want to print a different page from the current page?

Use Case

Let's say you're building an e-commerce website. Users have requested they want the option to print a receipt after their purchase. You don't want to mess with PDFs, so you'd like to use the web to create the structure and styling for you.

After completing their payment, users should have the option to click a "print receipt" button. This only loads the contents of the receipt and invokes the browser print dialog for them. It should be reusable so we can reference the same receipt from their order history.

Using React & Iframes

Why do we need iframes to solve this problem? Our use case requires loading only the receipt and nothing else. It should be available in other parts of the application as well.

We can create an iframe which loads the contents of our receipt. Once the content has finished loading, we can invoke the browser print API. This means it will only print what we want and allows us to reuse this component/route in other places.

Example

First, let's create an iframe that routes to our receipt.

<iframe
  id="receipt"
  src="/payment/receipt"
  style={{ display: 'none' }}
  title="Receipt"
/>

We need to ensure the contents of that route have loaded, otherwise the page will be blank when trying to print. We can communicate with the iframe from the receipt route to let it know we've finished loading.

parent.postMessage({ action: 'receipt-loaded' });

Finally, we need a way to invoke the print dialog. Let's create a component which sets up an event listener for the receipt-loaded message from the iframe receipt route. When the route has finished loading, we can click the button.

import React, { useState, useEffect } from 'react';

function PaymentConfirmation() {
  const [isLoading, setIsLoading] = useState(true);

  const handleMessage = (event) => {
    if (event.data.action === 'receipt-loaded') {
      setIsLoading(false);
    }
  };

  const printIframe = (id) => {
    const iframe = document.frames
      ? document.frames[id]
      : document.getElementById(id);
    const iframeWindow = iframe.contentWindow || iframe;

    iframe.focus();
    iframeWindow.print();

    return false;
  };

  useEffect(() => {
    window.addEventListener('message', handleMessage);

    return () => {
      window.removeEventListener('message', handleMessage);
    };
  }, []);

  return (
    <>
      <iframe
        id="receipt"
        src="/payment/receipt"
        style={{ display: 'none' }}
        title="Receipt"
      />
      <button onClick={()=> printIframe('receipt')}>
        {isLoading ? 'Loading...' : 'Print Receipt'}
      </button>
    </>
  );
}

export default PaymentConfirmation;

Result

Printing in React using iframes