Skip to content
New issue

Have a question about this project? # for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “#”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? # to your account

Add a onComplete callback prop to <QRCodeCanvas> component #388

Open
josharens opened this issue Jan 27, 2025 · 2 comments
Open

Add a onComplete callback prop to <QRCodeCanvas> component #388

josharens opened this issue Jan 27, 2025 · 2 comments

Comments

@josharens
Copy link

I have a use case where I want users to be able to click a link to download a QR code shown via the <QRCodeCanvas> component. My first attempt was to get a data URL from the canvas via toDataURL() in a ref callback, and set that as the href on a link.

function MyApp() {
  const [ dataUrl, setDataUrl ] = useState('');
  
  const canvasRefCallback = useCallback((canvas) => {
    if (!canvas) {
      return;
    }

    setDataUrl(canvas.toDataURL());
  }, []);

  return (
    <div>
      <QRCodeCanvas
        ref={canvasRefCallback}
        value={window.location.href}
      />

      {dataUrl &&
        <a href={dataUrl} download="qrcode">Download QR Code</a>
      }
    </div>
  );
}

This unfortunately results in a transparent PNG because the <canvas> element is rendered and the ref callback is invoked before the useEffect() that draws the QR code into the canvas has run.

As a workaround, I'm having users click on a button first where I then create a link dynamically and call .click() on it to initiate the download. This is fine, but it's a little silly making users click on a button, so that I can create a link and call .click() on it, when the users themselves could just click on that link.

function MyApp() {
  const canvasRef = useRef();
  const downloadLinkRef = useRef();

  const onDownloadQRCode = useCallback(() => {
    const canvas = canvasRef.current;
    const donwloadLink = downloadLinkRef.current;

    if (!canvas || !downloadLink) {
      return;
    }

    downloadLink.href = canvas.toDataURL();
    downloadLink.download = 'qrcode';
    downloadLink.click();
  }, []);

  return (
    <div>
      <QRCodeCanvas
        ref={canvasRef}
        value={window.location.href}
      />

      <button onClick={onDownloadQRCode}>Download QR code</button>

      <a hidden ref={downloadLinkRef} />
    </div>
  );
}

Since drawing to the canvas is synchronous, I think the first example could work if <QRCodeCanvas> took a onComplete callback prop that it called at the end of the useEffect() after drawing the QR code has been completed.

function MyApp() {
  const [ dataUrl, setDataUrl ] = useState('');
  const canvasRef = useRef();
  
  const onDrawQRCodeComplete = useCallback(() => {
    const canvas = canvasRef.current;

    if (!canvas) {
      return;
    }

    setDataUrl(canvas.toDataURL());
  }, []);

  return (
    <div>
      <QRCodeCanvas
        onComplete={onDrawQRCodeComplete}
        ref={canvasRef}
        value={window.location.href}
      />

      {dataUrl &&
        <a href={dataUrl} download="qrcode">Download QR Code</a>
      }
    </div>
  );
}

I'd be happy to open a PR if this is something everyone is open to.

@abhijith-b-259
Copy link

I'm facing a similar issue. An onComplete prop would help, as my current workaround involves adding an artificial delay, which isn't ideal.

@zpao
Copy link
Owner

zpao commented Feb 19, 2025

I could see this helping. There are a couple "complete" phases so it may make sense to be versatile. For Canvas specifically (where 2 & 3 may actually be done in single pass though I'd expect that to be rare, and 3 may never be done even if an image was specified):

  1. initial DOM created (this also kicks off loading the embedded image) - this is what you're getting stuck on for your ref, which is fair
  2. qrcode drawn to canvas
  3. embedded image drawn to canvas

SVG does 1 & 2 together so would have some differences in reporting. Then there's also the matter of handling updates.

If y'all have an API design with more detail to propose here, I'm open to seeing what we can do :)

# for free to join this conversation on GitHub. Already have an account? # to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

3 participants