【React】react-pdfでポートフォリオMATRIXFLOWにpdf作成機能を実装してみた

本記事の内容について
---
ポートフォリオ「[MATRIXFLOW](https://matrixflow-d2987557bad9.herokuapp.com/)」の機能拡充としてpdf作成機能を実装しリリースしたので、本記事に投稿いたします！

https://qiita.com/maixhashi/items/5da649e290ec13e71465

https://matrixflow-d2987557bad9.herokuapp.com/

実装Before/After
---
### 実装前
マトリクスの横にあるサイドバー風ドキュメントに、マトリクスの内容が自動で反映されます。
![matrixflow_qiita.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/606327/493a934f-1a57-49aa-3af7-2ff80f66d60e.gif)

### 実装後
サイドバー風ドキュメント部分にpdfプレビューが表示され、ダウンロードできるようにしました。
![matrixflow_qiita_function_5_pdf_generation.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/606327/833fc5e0-08da-d694-d380-1f9e5079e156.gif)


実装方法について
---
### 使用技術
 - React 18.3.1
   - Redux 
   - react-pdf/renderer 4.0.0
 - Vite 5.4.8

実装手順
---
### react-pdf/renderer のインストール
[公式](https://react-pdf.org/)を参考にreact-pdf/rendererをインストールします。

npmを使っているため下記コマンドにて
``` none:terminal
npm install @react-pdf/renderer --save
```

### 公式のサンプルpdfを表示できるようにする
まず、[公式のサンプルpdf](https://react-pdf.org/repl)と同じ内容をアプリ上で表示できるかトライしました。

#### pdfを表示するページコンポーネント`CreateMatrixFlowPage.jsx`上の設定
 - `PDFViewer`を設定
 - pdfとして表示するコンポーネントを`DocumentPDF`コンポーネントとする
```js: src/resourses/js/Components/Pages/CreateMatrixFlowPage.jsx
import React from 'react';

// PDFViewerコンポーネントをreact-pdf/rendererからインポート
import { PDFViewer } from '@react-pdf/renderer';

// コンポーネントの読み込み
import DocumentPDF from '../Components/DocumentPDF';

const CreateMatrixFlowPage = () => {
    return (
        <>
            <div className="sidebar-on-PDFViewerMode">
                <PDFViewer style={{ width: '100%', height: '80vh' }}>
                    // pdf表示したいコンポーネント（DocumentPDF）をPDFViewerでラップ
                    <DocumentPDF/>
                </PDFViewer>
            </div>
        </>
    );
};

export default CreateMatrixFlowPage;
```

#### `DocumentPDF`コンポーネントの設定
[公式のサンプルpdf](https://react-pdf.org/repl)に対して`Image`コンポーネントを除外した内容を`DocumentPDF`コンポーネントへ

公式のサンプルpdfは下記のように画像パスが設定されていますが、MATRIXFLOWでも同様にこのパスに画像ファイルを配置しないとエラーになるため除外しました。
```js
<Image
  style={styles.image}
  src="/images/quijote1.jpg"
/>
```

```js:src/resourses/js/Components/DocumentPDF.jsx
import React from 'react';
import { Page, Text, Font, View, Document, StyleSheet } from '@react-pdf/renderer';

const DocumentPDF = () => (
  <Document>
    <Page style={styles.body}>
      <Text style={styles.header} fixed>
        ~ Created with react-pdf ~
      </Text>
      <Text style={styles.title}>Don Quijote de la Mancha</Text>
      <Text style={styles.author}>Miguel de Cervantes</Text>
      // ..省略(公式のサンプルpdf内容とほぼ同様、Imageは除外）
    </Page>
  </Document>
);

Font.register({
  family: 'Oswald',
  src: 'https://fonts.gstatic.com/s/oswald/v13/Y_TKV6o8WovbUd3m_X9aAA.ttf'
});

const styles = StyleSheet.create({
  body: {
    paddingTop: 35,
    paddingBottom: 65,
    paddingHorizontal: 35,
  },
  title: {
    fontSize: 24,
    textAlign: 'center',
    fontFamily: 'Oswald'
  },
  author: {
    fontSize: 12,
    textAlign: 'center',
    marginBottom: 40,
  },
  subtitle: {
    fontSize: 18,
    margin: 12,
    fontFamily: 'Oswald'
  },
  text: {
    margin: 12,
    fontSize: 14,
    textAlign: 'justify',
    fontFamily: 'Times-Roman'
  },
  image: {
    marginVertical: 15,
    marginHorizontal: 100,
  },
  header: {
    fontSize: 12,
    marginBottom: 20,
    textAlign: 'center',
    color: 'grey',
  },
  pageNumber: {
    position: 'absolute',
    fontSize: 12,
    bottom: 30,
    left: 0,
    right: 0,
    textAlign: 'center',
    color: 'grey',
  },
});

export default DocumentPDF;
```

### 公式サンプルのpdfが表示されることを確認
公式サンプルのpdfが表示されることを確認。（ここまでは順調...だった...）
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/606327/2c6017b8-c999-d98a-09cc-b134bb709f81.png)

バグ対処
---
公式サンプルpdfが表示できて順調と思いきや、いろいろバグ対処に見舞われたので記します...😭

### バグ対処①：Reduxから参照した内容をpdfに表示できない
#### 実装した内容
サイドバー風のドキュメントは、Reduxを使ってstoreに保持された状態を参照した内容を表示しています。
`DocumentPDF`コンポーネントに対して同様にReduxによりstoreの状態を参照しようとしました。
コードは次のようにしました。
```js: src/resourses/js/Components/DocumentPDF.jsx
import React from 'react';
import { useSelector } from 'react-redux';

import { Page, Text, Font, View, Document, StyleSheet } from '@react-pdf/renderer';

const DocumentPDF = () => {
　// Reduxからworkflowsを参照
      const workflows = useSelector((state) => state.workflows.workflow);
  // Reduxから参照したworkflowsからなる定数
      const workflowName = workflows[0]?.name;

  <Document>
    <Page style={styles.body}>
      <Text style={styles.header} fixed>
        ~ Created with react-pdf ~
      </Text>

      // Reduxから参照したworkflowsからworkflowNameが表示できるかトライ
      <Text style={styles.title}>{workflowName}</Text>
    </Page>
  </Document>
};

Font.register({
  family: 'Oswald',
  src: 'https://fonts.gstatic.com/s/oswald/v13/Y_TKV6o8WovbUd3m_X9aAA.ttf'
});

const styles = StyleSheet.create({
// ...(省略)
});

export default DocumentPDF;
```

#### バグ発生
...がエラー発生(下記)によりReduxから参照できませんでした。
``` none:console
CreateMatrixFlowPage-0nmkX1Hh.js:218 TypeError: Cannot destructure property 'store' of 't(...)' as it is null.
```

#### 対処
Reduxに参照するのではなく、親コンポーネントから参照したい内容をpropsとして渡すことでエラー発生せず表示することができました。
``` js: src/resourses/js/Componets/Pages/CreateMatrixFlowPage.jsx
import React from 'react';
import { useSelector } from 'react-redux';
import { PDFViewer } from '@react-pdf/renderer';

const CreateMatrixFlowPage = () => {
    // DocumentPDFコンポーネントに表示したいstateをReduxから参照
        const flowsteps = useSelector((state) => state.flowsteps);
        const checklists = useSelector((state) => state.checkLists);
        const workflows = useSelector((state) => state.workflow);
        const workflowId = useSelector((state) => state.workflow.workflowId);

    return (
        <div>
            <PDFViewer style={{ width: '100%', height: '80vh' }}>
                <DocumentPDF
                　　// propsとしてDocumentPDFコンポーネントに渡す
                    flowsteps={flowsteps}
                    checklists={checklists}
                    workflowId={workflowId}
                    workflows={workflows}
                />
            </PDFViewer>
        </div>
    );
};

export default CreateMatrixFlowPage;
```

### バグ対処②：日本語で入力した内容が文字化けする
#### 実装内容
`DocumentPDF`コンポーネントに、pdfのタイトルに日本語をハードコーディング
（タイトルとして「申請」）
```js: src/resourses/js/Components/DocumentPDF.jsx
import React from 'react';
import { useSelector } from 'react-redux';

import { Page, Text, Font, View, Document, StyleSheet } from '@react-pdf/renderer';

const DocumentPDF = () => {
  <Document>
    <Page style={styles.body}>
      <Text style={styles.header} fixed>
        ~ Created with react-pdf ~
      </Text>

      // 試しにタイトルとして「申請」と表示させたい
      <Text style={styles.title}>申請</Text>
    </Page>
  </Document>
};

Font.register({
  family: 'Oswald',
  src: 'https://fonts.gstatic.com/s/oswald/v13/Y_TKV6o8WovbUd3m_X9aAA.ttf'
});

const styles = StyleSheet.create({
// ...(省略)
});

export default DocumentPDF;
```

#### バグ発生
文字化けする。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/606327/45afd15e-c46d-863c-0ba6-7e08ca7a3aaa.png)


#### 対処
 - [kashimuraryo /font-nasu](https://github.com/kashimuraryo/font-nasu)からNasuフォントをダウンロード
 - Nasuフォントを保存
``` js: src/resourses/js/Components/DocumentPDF.jsx
import React from 'react';
import { Page, Text, Font, View, Document, StyleSheet } from '@react-pdf/renderer';

// Nasuフォントのインポート
import fontRegular from '../../assets/fonts/Nasu-Regular.ttf'; //Nasuフォントを保存したパスを指定
import fontBold from '../../assets/fonts/Nasu-Bold.ttf';　//Nasuフォントを保存したパスを指定        

const DocumentPDF = () => {
  return (
    <Document>
      <Page style={styles.body}>
        <Text style={styles.title}>申請</Text>
      </Page>
    </Document>
  );
};

// Nasuフォントを登録
Font.register({
  family: 'Nasu-Regular',
  src: fontRegular
});

Font.register({
  family: 'Nasu-Bold',
  src: fontBold
});

// Nasuフォントを指定
const styles = StyleSheet.create({
  body: { padding: 30 },
  title: { 
      fontSize: 24, textAlign: 'center', marginBottom: 10, 
      fontFamily: 'Nasu-Bold' 　//font-familyに'Nasu-Bold'を指定
  },
});

export default DocumentPDF;

```

#### 参考記事
下記の記事を参考にNasuフォントを使用いたしました。

https://qiita.com/kashimuuuuu/items/0d249be6bcea834c473c

終わりに
---
自分のスタイリングしたサイドバー風ドキュメントだと、本格的なドキュメントができている感じがあまりしないなぁ、と物足りなかったのですがpdf作成機能を実装して本格的に近づき嬉しいです！

もう1つのポートフォリオ「PlotForge」について
---
もう1つポートフォリオ「PlotForge」もリリースしております！
こちらもご覧いただけますと幸いです。

https://qiita.com/maixhashi/items/e9a3503913b16635905f