diff --git a/src/__test__/index.test.tsx b/src/__test__/index.test.tsx
index 24687c1..877319f 100644
--- a/src/__test__/index.test.tsx
+++ b/src/__test__/index.test.tsx
@@ -1,6 +1,7 @@
import React from 'react';
import {QRCodeSVG, QRCodeCanvas} from '..';
-import {describe, expect, test} from '@jest/globals';
+import type {QRCodeCanvasUpdateAPIs} from '..';
+import {describe, expect, test, jest, beforeAll, afterAll} from '@jest/globals';
import {render} from '@testing-library/react';
import '@testing-library/jest-dom';
@@ -170,3 +171,60 @@ describe('`style` is passed to rendered nodes and merged correctly', () => {
expect(container.firstChild).toMatchSnapshot();
});
});
+
+describe('onUpdate prop', () => {
+ beforeAll(() => {
+ jest.spyOn(HTMLCanvasElement.prototype, 'getContext').mockReturnValue({
+ fillRect: jest.fn(),
+ scale: jest.fn(),
+ drawImage: jest.fn(),
+ } as unknown as CanvasRenderingContext2D);
+ });
+
+ afterAll(() => {
+ jest.restoreAllMocks();
+ });
+
+ test('calls onUpdate with canvas APIs', () => {
+ const onUpdateMock = jest.fn();
+ render();
+ expect(onUpdateMock).toHaveBeenCalledTimes(1);
+ expect(onUpdateMock).toHaveBeenCalledWith(
+ expect.objectContaining({
+ toDataURL: expect.any(Function),
+ toBlob: expect.any(Function),
+ })
+ );
+ });
+
+ test('keeps api reference stable on rerenders to prevent infinite loops', () => {
+ const onUpdateMock = jest.fn();
+ const {rerender} = render(
+
+ );
+
+ expect(onUpdateMock).toHaveBeenCalledTimes(1);
+ const firstApis = onUpdateMock.mock.calls[0][0] as QRCodeCanvasUpdateAPIs;
+
+ // Rerender with different props (e.g. value changes)
+ rerender(
+
+ );
+
+ expect(onUpdateMock).toHaveBeenCalledTimes(2);
+ const secondApis = onUpdateMock.mock.calls[1][0] as QRCodeCanvasUpdateAPIs;
+
+ // The API object reference must remain identical
+ expect(firstApis).toBe(secondApis);
+ expect(firstApis.toDataURL).toBe(secondApis.toDataURL);
+ expect(firstApis.toBlob).toBe(secondApis.toBlob);
+
+ // Verify calling toDataURL works and returns a string
+ const dataUrl = firstApis.toDataURL();
+ expect(typeof dataUrl).toBe('string');
+ });
+});
diff --git a/src/index.tsx b/src/index.tsx
index b6dd37a..8791949 100644
--- a/src/index.tsx
+++ b/src/index.tsx
@@ -135,7 +135,14 @@ type QRProps = {
*/
imageSettings?: ImageSettings;
};
-type QRPropsCanvas = QRProps & React.CanvasHTMLAttributes;
+export type QRCodeCanvasUpdateAPIs = {
+ toDataURL: HTMLCanvasElement['toDataURL'];
+ toBlob: HTMLCanvasElement['toBlob'];
+};
+type QRPropsCanvas = QRProps &
+ React.CanvasHTMLAttributes & {
+ onUpdate?: (apis: QRCodeCanvasUpdateAPIs) => void;
+ };
type QRPropsSVG = QRProps & React.SVGAttributes;
const DEFAULT_SIZE = 128;
@@ -358,6 +365,7 @@ const QRCodeCanvas = React.forwardRef(
boostLevel,
marginSize,
imageSettings,
+ onUpdate,
...extraProps
} = props;
const {style, ...otherProps} = extraProps;
@@ -365,6 +373,22 @@ const QRCodeCanvas = React.forwardRef(
const _canvas = React.useRef(null);
const _image = React.useRef(null);
+ const apis = React.useMemo(() => {
+ return {
+ toDataURL: (type?: string, quality?: number) => {
+ return _canvas.current?.toDataURL(type, quality) ?? '';
+ },
+ toBlob: (callback: BlobCallback, type?: string, quality?: number) => {
+ _canvas.current?.toBlob(callback, type, quality);
+ },
+ };
+ }, []);
+
+ const onUpdateRef = React.useRef(onUpdate);
+ React.useEffect(() => {
+ onUpdateRef.current = onUpdate;
+ });
+
// Set the local ref (_canvas) and also the forwarded ref from outside
const setCanvasRef = React.useCallback(
(node: HTMLCanvasElement | null) => {
@@ -464,6 +488,10 @@ const QRCodeCanvas = React.forwardRef(
calculatedImageSettings.h
);
}
+
+ if (onUpdateRef.current) {
+ onUpdateRef.current(apis);
+ }
}
});