Next.js + react-pdfを利用 iPhoneでPDF1ページしか表示されない解決方法

react-pdf 実装

iPhone Safari での PDF 全ページ表示問題を解決するため、<iframe> を使った PDF 表示を廃止し、react-pdf を用いた全ページレンダリングに切り替えた。 iframe では、 iOS Safari において PDF が1ページ目しか表示されなかった。

使用パッケージとバージョン

パッケージバージョン備考
react-pdf^10.4.1Document / Page コンポーネント提供
pdfjs-dist^5.4.296react-pdf が内部で使うバージョンに合わせる(重要)

インストール

pnpm install react-pdf pdfjs-dist@5.4.296

pdfjs-dist のバージョンは必ず react-pdf が要求するものに揃えること。 バージョンが異なると後述のバージョン不一致エラーが発生する。

遭遇したエラーと解決

エラー 1: DOMMatrix is not defined(SSRエラー)

エラー内容:
const SCALE_MATRIX = new DOMMatrix();
digest: ‘534975899’

原因: pdfjs-dist のモジュール初期化時に DOMMatrix(ブラウザ専用API)を使用しているため、 Next.js の SSR フェーズ(Node.js 環境)でクラッシュする。
'use client' を付けてもモジュールの初期化はサーバーで走るため回避できない。

解決策: react-pdf を使うコンポーネントを別ファイル(PdfViewer.tsx)に分離し、next/dynamic の ssr: false でクライアントのみ読み込む。

// IntroMediaOverlay.tsx
import dynamic from 'next/dynamic';

const PdfViewer = dynamic(
  () => import('@/components/PdfViewer'),
  { ssr: false }
);

エラー 2: API version does not match Worker version

エラー内容:
UnknownErrorException: The API version "5.4.296" does not match the Worker version "5.5.207".

原因: package.json に直接インストールされた pdfjs-dist が 5.5.207 だったのに対し、react-pdf 内部が使う pdfjs-dist は 5.4.296 だった。 Worker ファイルの解決に 5.5.207、API には 5.4.296 が使われバージョン不一致になった。

解決策: pdfjs-dist を react-pdf が要求するバージョンに固定する。

pnpm install pdfjs-dist@5.4.296

確認方法:

cat node_modules/react-pdf/package.json | grep pdfjs-dist
# → "pdfjs-dist": "5.4.296"

実装コード

PdfViewer.tsx(react-pdf 本体)

// src/components/PdfViewer.tsx
'use client';

import { useRef, useState, useCallback } from 'react';
import { Document, Page, pdfjs } from 'react-pdf';
import 'react-pdf/dist/Page/AnnotationLayer.css';
import 'react-pdf/dist/Page/TextLayer.css';

pdfjs.GlobalWorkerOptions.workerSrc = new URL(
  'pdfjs-dist/build/pdf.worker.min.mjs',
  import.meta.url,
).toString();

export default function PdfViewer({ url }: { url: string }) {
  const [numPages, setNumPages] = useState<number>(0);
  const containerRef = useRef<HTMLDivElement>(null);
  const [containerWidth, setContainerWidth] = useState<number>(600);

  const onDocumentLoadSuccess = useCallback(
    ({ numPages }: { numPages: number }) => {
      setNumPages(numPages);
      if (containerRef.current) {
        setContainerWidth(containerRef.current.clientWidth);
      }
    },
    []
  );

  return (
    <div ref={containerRef} className="overflow-y-auto max-h-[60vh] p-2">
      <Document
        file={url}
        onLoadSuccess={onDocumentLoadSuccess}
        loading={<span>PDF読み込み中...</span>}
      >
        {Array.from({ length: numPages }, (_, i) => (
          <Page
            key={i}
            pageNumber={i + 1}
            width={containerWidth - 16}
            className="mb-2"
          />
        ))}
      </Document>
    </div>
  );
}
Win版 Jsree スクショ 無音ガメラ Web版

Next.jsで「/#/」を使う方法|ハッシュルーティングの仕組みと実装例

Next.jsで「/#/」を使う方法|ハッシュルーティングの仕組み

1. 「/#/」とは?ハッシュルーティングの基本

ブラウザURLで見かける /#/ は、「ハッシュルーティング(Hash Routing)」と呼ばれる仕組みです。 これはシングルページアプリケーション(SPA)で、ページ遷移をクライアント側のみで制御するために使われます。

例えば https://mjeld.com/#/user/123 のようなURLでは、#以降の部分はサーバーには送信されません。 そのためサーバー設定を触らなくても、フロントエンドだけでルートを切り替えられるという利点があります。

2. Next.jsでハッシュルーティングを実装する

Next.jsは通常「ファイルベースルーティング(/pages や /app ディレクトリ)」を採用していますが、 どうしても /hash スタイルを使いたい場合は、クライアント側で window.location.hash を監視してルーティング処理を行います。

実装例(app/page.tsx)

'use client';

import { useEffect, useMemo, useState } from 'react';

type Route = { name: string; param?: string };

function parseHash(hash: string): Route {
  // 例: #/ , #/about , #/user/123
  const raw = hash.startsWith('#') ? hash.slice(1) : hash;
  const path = raw.replace(/^\/+/, ''); // 先頭の / を除去
  const seg = path.split('/').filter(Boolean);

  if (seg.length === 0) return { name: 'home' };
  if (seg[0] === 'about') return { name: 'about' };
  if (seg[0] === 'user' && seg[1]) return { name: 'user', param: seg[1] };
  return { name: 'notfound' };
}

export default function Page() {
  const [hash, setHash] = useState<string>(typeof window !== 'undefined' ? window.location.hash : '#/');

  useEffect(() => {
    const onChange = () => setHash(window.location.hash || '#/');
    window.addEventListener('hashchange', onChange);
    return () => window.removeEventListener('hashchange', onChange);
  }, []);

  const route = useMemo(() => parseHash(hash), [hash]);

  return (
    <main className="max-w-2xl mx-auto p-6 space-y-6">
      <nav className="flex gap-3">
        <a href="#/">Home</a>
        <a href="#/about">About</a>
        <a href="#/user/123">User 123</a>
      </nav>

      {route.name === 'home' && <h1 className="text-2xl font-bold">Home</h1>}
      {route.name === 'about' && <h1 className="text-2xl font-bold">About</h1>}
      {route.name === 'user' && (
        <section>
          <h1 className="text-2xl font-bold">User</h1>
          <p>ID: {route.param}</p>
        </section>
      )}
      {route.name === 'notfound' && <p>Not Found</p>}
    </main>
  );
}

このコードでは、hashchangeイベントを利用して#以降の変化を検知し、 表示するコンポーネントを切り替えています。

3. ハッシュルーティングを使うメリット・デメリット

項目メリットデメリット
設定の手軽さサーバー設定不要でSPA動作可
404対策リロードしても404にならない
SEO検索エンジンは#以降を無視しがち
URLの見た目古い設計に見えることがある

4. Next.jsでおすすめのルーティング方法

もしサーバー側(Vercel・Amplify・Cloudflare Pagesなど)でリライト設定が可能なら、 ハッシュ方式ではなく通常の/users/123形式を推奨します。

PowerShellでNode.jsの最新バージョン一覧を確認する方法【Volta/Windows対応】

Next.jsでbasePathを/homepage2にしてS3へ静的デプロイする完全手順

pnpmとは?npmとの違いとインストール方法

Next.jsのlayout.tsxで複数サイズのPNGファビコンを指定する方法

AWS Amplify CLIでS3やZIPから手動デプロイ【実行コマンド付き】

【JavaScript入門】即時実行関数 (async () => {})() はいつ実行されるのか?

即時実行関数イメージ

1. はじめに

JavaScript を学習していると、以下のようなコードを見かけることがあります。

(async () => {
    console.log("実行された");
})();

この (async () => {})() は「即時実行関数(IIFE: Immediately Invoked Function Expression)」の一種です。
しかし「非同期関数(async function)」と組み合わせた場合、いつ実行されるのか? という疑問を持つ方が多いでしょう。

本記事では、(async () => {})()どのタイミングで実行されるのか を解説


2. 即時実行関数(IIFE)とは?

通常の関数は定義しただけでは実行されません。

function test() {
    console.log("Hello");
}
test(); // ← 呼び出しが必要

一方、即時実行関数は「定義と同時に実行される」特殊な書き方です。

(() => {
    console.log("Hello");
})(); // ← 定義と同時に呼び出し

これにより グローバル変数の汚染を避けつつ、すぐに処理を開始 できます。


3. 非同期版即時実行関数 (async () => {})()

非同期処理を使いたい場合、以下のように async を付けて書きます。

(async () => {
    const data = await fetch("https://example.com/data.json");
    console.log(await data.json());
})();

ポイントは以下の通りです:

  • async をつけることで await が使える
  • 戻り値は Promiseオブジェクト になる
  • 定義した瞬間に即実行される

4. 実行タイミングはいつ?

結論から言うと、スクリプトが実行された瞬間に即時実行される という点は通常の IIFE と変わりません。

ただし、HTML 内のどこにスクリプトを書いたかで挙動が変わります。

(1) <head> 内に記述した場合

<head>
<script>
(async () => {
    console.log("head内で実行される");
})();
</script>
</head>

➡ ページの DOM 構築前に実行されます。


(2) <body> の末尾に記述した場合

<body>
    <p>本文</p>
<script>
(async () => {
    console.log("DOM構築後に実行される");
})();
</script>
</body>

➡ DOM が構築された後に実行されるため、要素操作に便利です。


(3) <script defer> を使った場合

<script defer>
(async () => {
    console.log("defer後に実行される");
})();
</script>

➡ DOM の構築完了後に実行されます。


5. await の処理タイミング

関数自体は スクリプト読み込み時に即実行 されますが、await を使った部分は 非同期処理の完了を待って再開 します。

つまり:

(async () => {
    console.log("開始");
    await new Promise(r => setTimeout(r, 1000));
    console.log("1秒後に実行");
})();

実行結果は次のようになります。

開始
(1秒待機)
1秒後に実行

6. まとめ

  • (async () => {})()非同期即時実行関数
  • 実行タイミングは スクリプトが読み込まれた瞬間
  • HTML のどこに書くか、defer / async を使うかでタイミングが変わる
  • await 部分は 非同期処理の完了後に続きが実行される