Use when implementing QR code generation - provides complete patterns for generating QR codes from text/URLs, displaying QR codes in modals, and customizing QR code appearance
Complete implementation guide for QR code generation. Generate QR codes from any text or URL and display them in user-friendly modals with copy functionality.
Core Capabilities:
IMPORTANT: Before adding dependencies, review your project's package.json to check if any of these packages already exist. If they do, verify the versions are compatible with the requirements below. Only add packages that are missing or need version updates.
Required packages:
{
"qrcode": "^1.5.4"
}
qrcode)// hooks/useQRCodeGenerator.ts
import { useState, useCallback } from 'react';
import QRCode from 'qrcode';
// optional import { useToast } from '@/hooks/useToast';
export function useQRCodeGenerator() {
const [isGenerating, setIsGenerating] = useState(false);
const [error, setError] = useState<string | null>(null);
// Optional: User feedback notifications
// Option 1: Console logging
// const logMessage = (message: string) => console.log(message);
// Option 2: Toast notifications (if useToast hook is available)
// const { toast } = useToast();
// Option 3: No notification handler
const generateQRCode = useCallback(async (text: string): Promise<string> => {
setIsGenerating(true);
setError(null);
try {
const qrCodeUrl = await QRCode.toDataURL(text, {
width: 256,
margin: 2,
color: {
dark: '#000000',
light: '#FFFFFF',
},
});
setIsGenerating(false);
return qrCodeUrl;
} catch (err) {
const errorMessage = 'Failed to generate QR code';
console.error(errorMessage, err);
setError(errorMessage);
setIsGenerating(false);
// Optional: User feedback - choose one:
// Option 1: Console logging
// console.error('QR Code Failed:', errorMessage);
// Option 2: Toast notification (if toast is available)
// toast({ variant: "destructive", title: "QR Code Failed", description: errorMessage });
// Option 3: No notification (silent failure)
return '';
}
}, []);
return {
generateQRCode,
isGenerating,
error,
};
}
Usage:
const { generateQRCode, isGenerating } = useQRCodeGenerator();
const qrUrl = await generateQRCode('bc1p...');
// Use qrUrl as image src: <img src={qrUrl} />
Customize appearance:
await QRCode.toDataURL(text, {
width: 512, // Larger size for printing
margin: 4, // White border
color: {
dark: '#000000', // QR code color
light: '#FFFFFF', // Background color
},
errorCorrectionLevel: 'M', // L, M, Q, H
});
Error Correction Levels:
L - Low (~7% recovery)M - Medium (~15% recovery) - Recommended defaultQ - Quartile (~25% recovery)H - High (~30% recovery) - Use for damaged QR codesColor Customization:
// Dark mode friendly
await QRCode.toDataURL(text, {
color: {
dark: '#FFFFFF', // White QR code
light: '#000000', // Black background
},
});
// Brand colors
await QRCode.toDataURL(text, {
color: {
dark: '#1F2937', // Dark gray
light: '#F9FAFB', // Light gray
},
});
// components/QRModal.tsx
import { Dialog, DialogContent, DialogHeader, DialogTitle, DialogDescription } from '@/components/ui/dialog';
import { Button } from '@/components/ui/button';
import { Copy, Check } from 'lucide-react';
import { useState } from 'react';
import * as VisuallyHidden from '@radix-ui/react-visually-hidden';
interface QRModalProps {
isOpen: boolean;
onClose: () => void;
qrCodeUrl: string;
content: string; // Text content shown below QR code
title?: string;
description?: string;
}
export function QRModal({ isOpen, onClose, qrCodeUrl, content, title, description }: QRModalProps) {
const [copied, setCopied] = useState(false);
const handleCopy = async () => {
try {
await navigator.clipboard.writeText(content);
setCopied(true);
setTimeout(() => setCopied(false), 2000);
} catch (err) {
console.error('Failed to copy to clipboard:', err);
}
};
return (
<Dialog open={isOpen} onOpenChange={onClose}>
<DialogContent>
<DialogHeader>
{title && <DialogTitle>{title}</DialogTitle>}
{description && (
<VisuallyHidden.Root asChild>
<DialogDescription>{description}</DialogDescription>
</VisuallyHidden.Root>
)}
</DialogHeader>
<div className="flex flex-col items-center gap-4">
<img src={qrCodeUrl} alt="QR Code" className="w-64 h-64" />
<div className="flex items-center gap-2 w-full">
<code className="flex-1 text-sm break-all p-2 bg-muted rounded">{content}</code>
<Button onClick={handleCopy} size="icon" variant="outline" aria-label="Copy to clipboard">
{copied ? <Check className="h-4 w-4" /> : <Copy className="h-4 w-4" />}
</Button>
</div>
</div>
</DialogContent>
</Dialog>
);
}
Usage:
const [showQR, setShowQR] = useState(false);
const [qrUrl, setQrUrl] = useState('');
const { generateQRCode } = useQRCodeGenerator();
const handleShowQR = async () => {
const url = await generateQRCode('bc1p...');
setQrUrl(url);
setShowQR(true);
};
<QRModal
isOpen={showQR}
onClose={() => setShowQR(false)}
qrCodeUrl={qrUrl}
content="bc1p..."
title="Bitcoin Address"
description="Scan this QR code to send Bitcoin"
/>
// components/QRCodeDisplay.tsx
import { useEffect, useState } from 'react';
import { useQRCodeGenerator } from '@/hooks/useQRCodeGenerator';
import { Skeleton } from '@/components/ui/skeleton';
interface QRCodeDisplayProps {
content: string;
size?: number;
className?: string;
}
export function QRCodeDisplay({ content, size = 256, className }: QRCodeDisplayProps) {
const { generateQRCode, isGenerating } = useQRCodeGenerator();
const [qrUrl, setQrUrl] = useState<string>('');
useEffect(() => {
if (content) {
generateQRCode(content).then(setQrUrl);
}
}, [content, generateQRCode]);
if (isGenerating) {
return <Skeleton className={className} style={{ width: size, height: size }} />;
}
if (!qrUrl) {
return null;
}
return (
<img
src={qrUrl}
alt="QR Code"
className={className}
style={{ width: size, height: size }}
/>
);
}
Problem: Generating QR codes from empty strings or invalid data causes errors.
Solution: Validate input before generation:
const generateQRCode = useCallback(async (text: string): Promise<string> => {
if (!text || !text.trim()) {
setError('Cannot generate QR code from empty content');
return '';
}
// ... rest of generation logic
}, []);
Problem: Users don't know QR code is being generated, causing confusion.
Solution: Always show loading state:
{isGenerating ? (
<Skeleton className="w-64 h-64" />
) : (
<img src={qrUrl} alt="QR Code" />
)}
Problem: Errors during generation cause UI to break or show nothing.
Solution: Always handle errors gracefully:
try {
const qrCodeUrl = await QRCode.toDataURL(text, options);
return qrCodeUrl;
} catch (err) {
console.error('Failed to generate QR code:', err);
setError('Failed to generate QR code');
return '';
}
Problem: Using too low error correction causes QR codes to be unreadable when damaged.
Solution: Use appropriate error correction level:
M (Medium) - Good balanceH (High) - For printed materialsL (Low) - Only if space is limitedProblem: QR codes too small are hard to scan, too large waste space.
Solution: Choose appropriate size:
import { describe, it, expect, vi } from 'vitest';
import { renderHook, waitFor } from '@testing-library/react';
import { useQRCodeGenerator } from '@/hooks/useQRCodeGenerator';
import QRCode from 'qrcode';
vi.mock('qrcode');
describe('useQRCodeGenerator', () => {
it('generates QR code successfully', async () => {
const mockDataUrl = 'data:image/png;base64,...';
vi.mocked(QRCode.toDataURL).mockResolvedValue(mockDataUrl);
const { result } = renderHook(() => useQRCodeGenerator());
const qrUrl = await result.current.generateQRCode('test content');
expect(qrUrl).toBe(mockDataUrl);
expect(result.current.isGenerating).toBe(false);
});
it('handles generation errors', async () => {
vi.mocked(QRCode.toDataURL).mockRejectedValue(new Error('Generation failed'));
const { result } = renderHook(() => useQRCodeGenerator());
const qrUrl = await result.current.generateQRCode('test');
expect(qrUrl).toBe('');
expect(result.current.error).toBeTruthy();
});
it('sets isGenerating state correctly', async () => {
const mockDataUrl = 'data:image/png;base64,...';
vi.mocked(QRCode.toDataURL).mockImplementation(
() => new Promise(resolve => setTimeout(() => resolve(mockDataUrl), 100))
);
const { result } = renderHook(() => useQRCodeGenerator());
const promise = result.current.generateQRCode('test');
expect(result.current.isGenerating).toBe(true);
await promise;
await waitFor(() => {
expect(result.current.isGenerating).toBe(false);
});
});
});
import { describe, it, expect, vi } from 'vitest';
import { render, screen, fireEvent } from '@testing-library/react';
import { QRModal } from '@/components/QRModal';
describe('QRModal', () => {
it('displays QR code and content', () => {
render(
<QRModal
isOpen={true}
onClose={vi.fn()}
qrCodeUrl="data:image/png;base64,..."
content="bc1p..."
title="Test QR"
/>
);
expect(screen.getByText('Test QR')).toBeInTheDocument();
expect(screen.getByText('bc1p...')).toBeInTheDocument();
expect(screen.getByAltText('QR Code')).toHaveAttribute('src', 'data:image/png;base64,...');
});
it('copies content to clipboard', async () => {
const writeText = vi.fn();
Object.assign(navigator, { clipboard: { writeText } });
render(
<QRModal
isOpen={true}
onClose={vi.fn()}
qrCodeUrl="data:image/png;base64,..."
content="bc1p..."
/>
);
const copyButton = screen.getByLabelText('Copy to clipboard');
fireEvent.click(copyButton);
expect(writeText).toHaveBeenCalledWith('bc1p...');
});
});
To implement QR code generation:
qrcodeQRCode.toDataURL() for generationKey principle: Always validate input and handle errors gracefully to ensure reliable QR code generation.