ReactでQRコードを作って表示する方法

​​React​Canvas API,Playwright,qrcode.react,TypeScript

React で QR コードを表示するには、qrcode.react の QRCodeSVG に、QR コードにしたい文字を value で渡します。この記事では、入力した文字をその場で QR コードにし、PNG で保存できるコンポーネントを作ります。あわせて、読み取れなくなる原因になりやすい余白の不足と、入る文字数の上限を確かめました。真ん中にロゴを重ねたときに、誤り訂正レベルによって読めるかどうかが変わることも調べています。

表示した QR コードと保存した PNG が正しく読めるかは、Playwright で撮った画像を jsQR で読み取って確かめています。

補足

検証環境は Ubuntu 24.04、Node.js 22.22.0、Chromium 153 です。使ったのは React 19.3.1、qrcode.react 4.2.0、TypeScript 7.0.2、esbuild 0.28.2、Playwright 1.63.0、読み取りの確認に jsQR 1.4.0 です。.tsx を tsc で型チェックしてから esbuild で変換しました。StrictMode を付けた開発ビルドでも、操作して React の警告が出ないことを確かめています。スマートフォンのカメラで実際に読み取ること、Firefox と Safari、Next.js などのフレームワークの上での動きは試していません。

文字の入力欄、バイト数の表示、QR コード、「PNG で保存」ボタンが縦に並んでいる。入力欄の「https://example.com/」の後ろに「neko」を打つと、1文字ごとに QR コードの模様とバイト数が変わる。次に入力欄をいったん空にすると「文字を入れると、ここに QR コードが出ます」という案内が出て、「ねこの手」と打つと、QR コードがまた変わる。最後にボタンを押すと、右下に「qrcode.png を保存しました」と出る
入力に合わせて QR コードが変わり、ボタンで PNG を保存する様子。バイト数は変わった瞬間だけピンクにしている。カーソル、押した場所の輪、右下の操作の説明、保存したときの表示は GIF 用に重ねたもの

qrcode.react を入れて、QRCodeSVG に文字を渡す

qrcode.react は、QR コードを SVG で描く QRCodeSVG と、canvas で描く QRCodeCanvas の2つのコンポーネントを持つライブラリです。検証した 4.2.0 の peerDependencies は react: '^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0' で、React 19 で使えます。

bash
npm i qrcode.react

いちばん短い使い方は <QRCodeSVG value="https://example.com/" /> です。何も指定しないと、大きさは 128px、誤り訂正レベルは L、周りの余白はなしになります(qrcode.react の README の既定値)。

入力した文字をその場で QR コードにし、PNG で保存できるようにしたのが次のコンポーネントです。

tsx
import { useId, useRef, useState } from 'react';
import { QRCodeCanvas, QRCodeSVG } from 'qrcode.react';

// 誤り訂正レベル M の QR コードに入る最大のバイト数(qrcode.react 4.2.0 で確かめた値)
const MAX_BYTES = 2331;

export function QRCodeMaker() {
  const inputId = useId();
  const [text, setText] = useState('https://example.com/');
  const canvasRef = useRef<HTMLCanvasElement>(null);

  // QR コードには UTF-8 のバイト列で入るので、文字数ではなくバイト数で上限を見る
  const bytes = new TextEncoder().encode(text).length;
  const empty = text.trim() === '';
  const tooLong = bytes > MAX_BYTES;

  const download = () => {
    const a = document.createElement('a');
    a.href = canvasRef.current!.toDataURL('image/png');
    a.download = 'qrcode.png';
    a.click();
  };

  return (
    <div className="qr-maker">
      <label htmlFor={inputId}>QR コードにする文字</label>
      <textarea id={inputId} value={text} onChange={(e) => setText(e.target.value)} rows={2} />
      <p className="qr-bytes">
        {bytes} / {MAX_BYTES} バイト
      </p>
      {empty ? (
        <p className="qr-message">文字を入れると、ここに QR コードが出ます</p>
      ) : tooLong ? (
        <p className="qr-message" role="alert">長すぎて QR コードにできません</p>
      ) : (
        <>
          <QRCodeSVG value={text} size={200} level="M" marginSize={4} title={text} />
          {/* ダウンロード用。画面には出さず、大きめのサイズで描いておく */}
          <QRCodeCanvas ref={canvasRef} value={text} size={512} level="M" marginSize={4} hidden />
          <button type="button" onClick={download}>PNG で保存</button>
        </>
      )}
    </div>
  );
}

value が変わるたびに QR コードが描き直されるので、入力欄の state をそのまま渡すだけで、打った文字に合わせて模様が変わります。検証環境で「ねこの手で作った QR コード」と入力したところ、バイト数の表示は「37 / 2331 バイト」になり、画面の QR コードを jsQR で読むと同じ文字列が返りました。日本語も、そのまま value に渡せます。

title を渡すと、<svg> に role="img" と <title> が付きます。README の title の説明では、アクセシビリティのための指定とされています(スクリーンリーダーでの読み上げは試していません)。

周りの余白は marginSize={4} で付ける

qrcode.react の既定では、QR コードの周りに余白を付けません。README の marginSize の説明にあるとおり、QR コードの規格では、周りに4セル分の余白が必要です。そのため、上のコンポーネントでは marginSize={4} を付けています。

白いページに置くなら、余白がなくてもページの白が余白の代わりになります。困るのは、ページの背景が暗い色のときです。検証環境で、背景 #1f2a30(ほぼ黒)のページに QR コードを置いたところ、余白なしでは jsQR で読み取れず、marginSize={4} にすると読み取れました。背景が白と水色(#00c8e8)のときは、どちらも読み取れました。

ポイント

QR コードを置く場所の背景色に関係なく読めるように、marginSize={4} を付けて QR コード自身に白い余白を持たせます。

長い文字は、上限を見てから渡す

QR コードに入る量には上限があります。qrcode.react 4.2.0 で、誤り訂正レベルを上げずに(boostLevel: false)試したところ、a をくり返した文字列で入った最大のバイト数は次のとおりでした。どのレベルでも、上限のときは1辺 177 セルの、いちばん大きい QR コードになりました。

誤り訂正レベル 入った最大のバイト数
L 2953
M 2331
Q 1663
H 1273

上限を1バイトでも超えると、QRCodeSVG はレンダリングの途中で RangeError: Data too long を投げます。上限を見ずにそのまま渡したところ、エラーが出て、ページの中身がすべて消えました。React の公式リファレンスにある Error Boundary の説明のとおり、レンダリング中のエラーを受け止める仕組みがないと、React は画面から UI を取り除きます。

そこで、上のコンポーネントでは、渡す前に TextEncoder でバイト数を数えています。TextEncoder.encode() は文字列を UTF-8 のバイト列にします。日本語のひらがなは1文字3バイトなので、文字数で数えると上限を見誤ります。検証環境では「あ」777文字(2331 バイト)は QR コードになり、778文字(2334 バイト)では「長すぎて QR コードにできません」と出て、エラーにはなりませんでした。

README の Encoding Mode の説明によると、数字だけの文字列などは、より多く入る形で詰めることがあります。この記事のようにバイト数で判定すると、そうした文字列では、実際にはまだ入る長さでも「長すぎて QR コードにできません」と出ることがあります(数字だけの文字列での上限は試していません)。

文字が長いほど、また誤り訂正レベルが高いほど、QR コードのセルは細かくなります。68 バイトの URL では、1辺のセルの数が L で 33、M で 37、Q で 41、H で 49 でした。表示する大きさが同じなら、セルが細かいほど1セルが小さくなります。誤り訂正レベルを上げるのは、次の節のように必要なときだけにします。

真ん中にロゴを重ねるなら、誤り訂正レベルを H にする

imageSettings を渡すと、QR コードの真ん中に画像を重ねられます。excavate: true にすると、画像と重なるセルを背景色で塗り、画像の周りをきれいに抜きます(imageSettings.excavate の説明)。

tsx
<QRCodeSVG
  value={url}
  size={256}
  level="H"
  marginSize={4}
  imageSettings={{ src: '/logo.svg', width: 64, height: 64, excavate: true }}
/>

ロゴで隠れた部分は、誤り訂正で補って読み取ります。どこまで隠してよいかは誤り訂正レベルしだいです。検証環境で、68 バイトの URL の QR コード(256px)に正方形のロゴを重ね、boostLevel: false でレベルを固定して jsQR で読めるかを調べました。

誤り訂正レベル ロゴ 38px ロゴ 51px ロゴ 64px ロゴ 77px
L 読めた 読めない 読めない 読めない
M 読めた 読めた 読めない 読めない
Q 読めた 読めた 読めた 読めた
H 読めた 読めた 読めた 読めた
上に誤り訂正レベル L、下に H の QR コードが縦に並んでいる。どちらも真ん中に水色の肉球のロゴが同じ大きさで重なっている。L の下には「読み取れない」、H の下には「読み取れた」と書かれている
同じ URL・同じ 64px のロゴで、誤り訂正レベルだけを変えた QR コード。下の文字は jsQR で読み取った結果

誤り訂正レベルを L に固定すると、QR コードの幅(256px)の 20%(51px)のロゴで、もう読めなくなりました。この検証では Q でも 77px まで読めました。ただし、この結果は jsQR という1つの読み取り方で調べたものです。ほかの読み取り方でも読めるように余裕を残すため、上のコードでは level="H" にしています。スマートフォンのカメラでの読み取りは、印刷の大きさや明るさにも左右されるので、公開する前に実物で確かめてください。

PNG で保存するときは、QRCodeCanvas の toDataURL を使う

保存には、画面に出している SVG とは別に、hidden を付けた QRCodeCanvas を 512px で描いておき、その canvas から PNG を作っています。qrcode.react は、渡した属性をそのまま <canvas> に付けます(README の Custom Styles の説明)。検証環境では、hidden を付けた canvas は display: none で画面に出ませんでしたが、中身は描かれていて、そこから保存した PNG も読み取れました。

ボタンを押すと、ref で受け取った canvas の toDataURL() で PNG の data URL を作り、download 属性を付けたリンクをクリックして保存します。Playwright で押したところ、qrcode.png が保存され、512×512px の PNG を jsQR で読むと、入力した URL が返りました。同じ MDN のページでは、大きな画像なら toBlob() を使うよう勧めています。この記事で保存した 512px の画像では toDataURL() で保存できましたが、toBlob() は試していません。

保存される PNG の大きさは、画面のピクセル密度で変わります。qrcode.react は高密度の画面でぼやけないように、canvas の中のピクセル数を増やして描きます。検証環境で Playwright の deviceScaleFactor を 2 にしたところ、同じ size={512} でも、保存した PNG は 1024×1024px になりました。決まった大きさの画像が必要なら、保存したあとに大きさを確かめてください。

​Posted by 86