React初心者がQRコード生成ページを作成してみた

# はじめに
Reactの参考書を一通り終え、何か簡単なWebアプリケーションを作成してみたいと思っていた際に母から
「チラシとかにQRコードたくさん載ってるけどどうやって作成されてるんだろう」
と言われたのをきっかけにQRコードを生成できるWebアプリケーションを作成することにしました。

ただ、今回はReactの学習を行いたかったため、QRコードの作成部分の処理については、一から作成したのではなく下記のリポジトリを使用させていただきました。

# 開発にあたり使用させていただいたリポジトリ

https://github.com/ciaochaos/qrbtf

https://github.com/canvg/canvg

# どんなアプリ？
## 概要
inputformに作成したいURLを入力し、気に入ったQRコードをダウンロードするといった簡単なWebアプリケーションです。

QRコード生成アプリケーション

https://powerninja.github.io/create_qrcode/

リポジトリ

https://github.com/powerninja/create_qrcode

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/867144/b2ae83de-28f1-be57-4f03-cf883443b354.png)
## 使い方
1. QRコードに変換したいURLを入力する
2. 生成ボタンを押下する
3. ダウンロードしたいQRコードを選択する
4. ダウンロードボタンを押下して、選択したQRコードをダウンロードする


# こだわった点
## 外見
- 複数デザインのQRコードを一覧で表示することができます。
- QRコードを選択した際に、選択されたQRコードに枠をつけたり、画面下部に選択したQRコードを大きく表示したりといったユーザに対するアクションをつけるようにしました。
- httpから始まらない文字を入力された際は、QRコードが生成されないようにしています。
- 生成ボタンを押下しないとダウンロードボタンは非活性化にしています。(tabキーで選択すると押せてしまう。。)
- 一応faviconも設定しています。pcのブラウザだと表示されているはず。。(モバイルは未確認)
- デザインはBootstrapを使用しています。

## 内部処理
- app.tsxに全ての処理を記載するのではなく、componentに分けたりカスタムhookを使用したりと今後大規模なReact開発を行う際にも対応できるよう開発を進めました。
- また、上記に加え*TypeScript*を使用した開発を行い、型宣言もanyを使用しないようにしました。(anyないはず。。。)
- Reactを使用したWebアプリケーションを作成するのは、初めてだったため1週間くらいで作成できる規模にしました。
- Qiitaに記事投稿するのも初めてなので、READMEもカッコよく英語で記載してみました(Deepl翻訳ですが。。。)
- いろいろな方に見ていただきたいため、mainブランチに開発ブランチがマージされた際にGitHub Pagesに自動でデプロイされるようにしました。

# システム構成&コード
必要な箇所のみ抜粋
```
.
├── src
│    ├── App.tsx
│    ├── hooks
│    │    └── useQRCodeHandler.tsx
│    └── components
│         ├── InputForm.tsx
│         └── QRCodeDisplay.tsx
│                 
├── package.json
└── README.md
```
長いので折りたたんでます。
<details>
<summary>app.tsx</summary>

```Javascript
import { useRef, FC } from 'react';
import './App.css';
import { useQRCodeHandler } from './hooks/useQRCodeHandler';
import { InputForm } from './components/InputForm';
import { QRCodeDisplay } from './components/QRCodeDisplay';

export const App: FC = () => {
  const svgRef = useRef<HTMLDivElement | null>(null);

  //カスタムhook呼び出し
  const {
    setInputUrl,
    generateQrCode,
    clearQrCode,
    downloadQrCode,
    visible,
    downloadButton,
    qrCodebgColor,
    selectedQrCode,
    downloadUrl,
    setQrcodeStyle,
    urlLink,
  } = useQRCodeHandler();

  return (
    <div>
      <InputForm
        url={urlLink.url}
        setInputUrl={setInputUrl}
        generateQrCode={generateQrCode}
        clearQrCode={clearQrCode}
        downloadQrCode={downloadQrCode}
        downloadUrl={downloadUrl}
        downloadButton={downloadButton}
        svgRef={svgRef}
      />

      <QRCodeDisplay
        url={urlLink.url}
        setQrcodeStyle={setQrcodeStyle}
        qrCodebgColor={qrCodebgColor}
        visible={visible}
        svgRef={svgRef}
        selectedQrCode={selectedQrCode}
      />
    </div>
  );
};

```
</details>

<details>
<summary>InputForm.tsx</summary>

```Javascript
import { FC, RefObject } from 'react';

interface QRCodeDisplayProps {
  url: string;
  setInputUrl: (urlValue: string) => void;
  generateQrCode: () => void;
  clearQrCode: () => void;
  downloadQrCode: (element: RefObject<HTMLDivElement>) => void;
  downloadUrl: string;
  downloadButton: string;
  svgRef: RefObject<HTMLDivElement>;
}

export const InputForm: FC<QRCodeDisplayProps> = (props) => {
  const { url, setInputUrl, generateQrCode, clearQrCode, downloadQrCode, downloadUrl, downloadButton, svgRef } = props;

  return (
    <div>
      <div
        style={{
          height: 'auto',
          margin: '0 auto',
          maxWidth: 300,
          width: '100%',
        }}
      >
        <h1>QRコード作成</h1>

        <p>URLを入力してください</p>
      </div>
      <div
        className="input-group mb-3"
        style={{
          height: 'auto',
          margin: '0 auto',
          maxWidth: 300,
          width: '100%',
        }}
      >
        <input
          type="text"
          className="form-control"
          placeholder="https://example.com"
          aria-describedby="basic-addon2"
          value={url}
          onChange={(event) => {
            setInputUrl(event.target.value);
          }}
        />
      </div>

      <div className="button-container">
        <button
          className="btn btn-primary ms-3"
          onClick={generateQrCode}
          style={{
            height: 'auto',
            maxWidth: 200,
          }}
        >
          生成
        </button>
        <button
          className="btn btn-danger"
          onClick={clearQrCode}
          style={{
            height: 'auto',
            maxWidth: 200,
          }}
        >
          クリア
        </button>
        <a
          className={downloadButton}
          download="qr-code.jpg"
          href={downloadUrl}
          onClick={() => downloadQrCode(svgRef)}
          style={{
            height: 'auto',
            maxWidth: 200,
          }}
        >
          ダウンロード
        </a>
      </div>
    </div>
  );
};

```

</details>

<details>
<summary>QRCodeDisplay.tsx</summary>

```JavaScript
import { FC, RefObject } from 'react';
import { QR25D, QRRandRect, QRBubble, QRDsj, QRNormal, QRFunc, QRLine } from 'react-qrbtf';

interface QRCodeDisplayProps {
  url: string;
  setQrcodeStyle: (qrnum: number) => void;
  qrCodebgColor: {
    color0: string;
    color1: string;
    color2: string;
    color3: string;
    color4: string;
    color5: string;
    color6: string;
  };
  svgRef: RefObject<HTMLDivElement>;
  selectedQrCode: React.ReactNode;
  visible: boolean;
}

export const QRCodeDisplay: FC<QRCodeDisplayProps> = (props) => {
  const { url, setQrcodeStyle, qrCodebgColor, visible, svgRef, selectedQrCode } = props;

  return (
    <div
      style={{
        height: 'auto',
        margin: '0 auto',
        maxWidth: 800,
        width: '100%',
        padding: '5px',
        visibility: visible ? 'visible' : 'hidden',
      }}
    >
      <div className="d-flex flex-row">
        <div id="1" className="my-box w-25" onClick={() => setQrcodeStyle(0)} style={{ padding: '1px', border: qrCodebgColor.color0 }}>
          <QRNormal value={url} />
        </div>
        <div className="my-box w-25" onClick={() => setQrcodeStyle(1)} style={{ padding: '1px', border: qrCodebgColor.color1 }}>
          <QRDsj value={url} />
        </div>
        <div className="my-box w-25" onClick={() => setQrcodeStyle(2)} style={{ padding: '1px', border: qrCodebgColor.color2 }}>
          <QRBubble value={url} />
        </div>
        <div className="my-box w-25" onClick={() => setQrcodeStyle(3)} style={{ padding: '1px', border: qrCodebgColor.color3 }}>
          <QR25D value={url} />
        </div>
        <div className="my-box w-25" onClick={() => setQrcodeStyle(4)} style={{ padding: '1px', border: qrCodebgColor.color4 }}>
          <QRRandRect value={url} />
        </div>
        <div className="my-box w-25" onClick={() => setQrcodeStyle(5)} style={{ padding: '1px', border: qrCodebgColor.color5 }}>
          <QRFunc value={url} funcType={'B'} />
        </div>
        <div className="my-box w-25" onClick={() => setQrcodeStyle(6)} style={{ padding: '1px', border: qrCodebgColor.color6 }}>
          <QRLine value={url} />
        </div>
      </div>
      <div ref={svgRef}>{selectedQrCode}</div>
    </div>
  );
};
```

</details>

<details>
<summary>useQRCodeHandler.tsx</summary>

```Javascript
import React, { useState, useCallback, RefObject } from 'react';

//QRコード生成ライブラリ https://github.com/ciaochaos/qrbtf
import { QR25D, QRRandRect, QRBubble, QRDsj, QRNormal, QRFunc, QRLine } from 'react-qrbtf';

//SVGを画像に変換するライブラリ https://github.com/canvg/canvg
import { Canvg } from 'canvg';

type Linktype = {
  url: string;
};

type BgColor = {
  color0: string;
  color1: string;
  color2: string;
  color3: string;
  color4: string;
  color5: string;
  color6: string;
};

export const useQRCodeHandler = () => {
  //inputフォームに入力されたurl
  const [urlLink, setUrlLink] = useState<Linktype>({ url: '' });

  //QRコードの表示非表示を制御
  const [visible, setVisible] = useState<boolean>(false);

  //ダウンロードボタンの制御
  const [downloadButton, setDownloadButton] = useState<string>('btn btn-secondary disabled');

  //QRコードが選択された際に色を変更
  const [qrCodebgColor, setqrCodebgColor] = useState<BgColor>({
    color0: '',
    color1: '',
    color2: '',
    color3: '',
    color4: '',
    color5: '',
    color6: '',
  });

  //選択されたQRコードを保存するuseState
  const [selectedQrCode, setSelectedQrCode] = useState<React.ReactElement | null>(null);

  //ダウンロードリンク
  const [downloadUrl, setDownloadUrl] = useState<string>('');

  //inputフォーム入力
  const setInputUrl = useCallback((urlValue: string) => {
    setUrlLink({ url: urlValue });
    setVisible(false);
    setDownloadButton('btn btn-secondary disabled');
  }, []);

  //QRコード生成
  const generateQrCode = useCallback(() => {
    if (urlLink.url.indexOf('http', 0) === -1) {
      alert('URLを入力してください');
      return;
    }
    setVisible(true);
  }, [urlLink]);

  //表示していたQRコードを非表示
  const clearQrCode = useCallback(() => {
    setVisible(false);
    setUrlLink({ url: '' });
    setDownloadButton('btn btn-secondary disabled');
  }, []);

  //生成したQRコードをダウンロード
  const downloadQrCode = (element: RefObject<HTMLDivElement>) => {
    if (element && element.current) {
      const svgElement = element.current.querySelector('svg');

      if (svgElement) {
        const svgData = new XMLSerializer().serializeToString(svgElement);
        const canvas = document.createElement('canvas');
        const ctx = canvas.getContext('2d');
        if (ctx) {
          // SVG要素の大きさに合わせてキャンバスサイズを設定する
          const svgWidth = svgElement.viewBox.baseVal.width;
          const svgHeight = svgElement.viewBox.baseVal.height;

          // 解像度設定
          const scale = 10;

          canvas.width = svgWidth * scale;
          canvas.height = svgHeight * scale;

          // stringをDocumentに型変換
          const parser = new DOMParser();
          const svgDocument = parser.parseFromString(svgData, 'image/svg+xml');
          // canvg を使ってSVGデータをcanvasに描画
          const canvgInstance = new Canvg(ctx, svgDocument);
          canvgInstance.start();

          const pngUrl = canvas.toDataURL('image/png');
          setDownloadUrl(pngUrl);
        }
      } else {
        console.error('CanvasRenderingContext2D could not be created.');
      }
    }
  };

  // 押下されたQRコードに枠をつける処理
  const setQrcodeStyle = (qrnum: number) => {
    switch (qrnum) {
      case 0:
        setqrCodebgColor({ color0: '1px solid #000', color1: '', color2: '', color3: '', color4: '', color5: '', color6: '' });
        setSelectedQrCode(<QRNormal value={urlLink.url} />);
        break;
      case 1:
        setqrCodebgColor({ color0: '', color1: '1px solid #000', color2: '', color3: '', color4: '', color5: '', color6: '' });
        setSelectedQrCode(<QRDsj value={urlLink.url} />);
        break;
      case 2:
        setqrCodebgColor({ color0: '', color1: '', color2: '1px solid #000', color3: '', color4: '', color5: '', color6: '' });
        setSelectedQrCode(<QRBubble value={urlLink.url} />);
        break;
      case 3:
        setqrCodebgColor({ color0: '', color1: '', color2: '', color3: '1px solid #000', color4: '', color5: '', color6: '' });
        setSelectedQrCode(<QR25D value={urlLink.url} />);
        break;
      case 4:
        setqrCodebgColor({ color0: '', color1: '', color2: '', color3: '', color4: '1px solid #000', color5: '', color6: '' });
        setSelectedQrCode(<QRRandRect value={urlLink.url} />);
        break;
      case 5:
        setqrCodebgColor({ color0: '', color1: '', color2: '', color3: '', color4: '', color5: '1px solid #000', color6: '' });
        setSelectedQrCode(<QRFunc value={urlLink.url} />);
        break;
      case 6:
        setqrCodebgColor({ color0: '', color1: '', color2: '', color3: '', color4: '', color5: '', color6: '1px solid #000' });
        setSelectedQrCode(<QRLine value={urlLink.url} />);
        break;
    }
    setDownloadButton('btn btn-secondary');
  };

  return {
    setInputUrl,
    generateQrCode,
    clearQrCode,
    downloadQrCode,
    visible,
    downloadButton,
    qrCodebgColor,
    selectedQrCode,
    downloadUrl,
    setQrcodeStyle,
    urlLink,
  };
};

```

</details>

# 最後に
Reactの学習のために作成したWebアプリケーションですが、使っていただけると嬉しいです。
また、バグやコードに関するフィードバックをいただけるとさらに嬉しいです。