技術記事に戻る

React不要!標準JavaScriptで実装する軽量SPAの状態管理と高速DOM操作テクニック

JavaScriptHTML

React不要!標準JavaScriptで実装する軽量SPAの状態管理と高速DOM操作テクニック

ソースコード: https://github.com/hayate-hu6/local-media-viewer.git

はじめに

以前、レガシーブラウザ環境での動作が必須となる業務システムを改修した際、モダンなフレームワークが使えず標準のJavaScriptで実装し、苦労した経験があります。その時の試行錯誤から学んだ、フレームワークに依存しない軽量なDOM操作とデータフローの構築手法を、本プロジェクトに活かしました。

昨今のWeb開発ではReactやVue.jsなどのフレームワークを使うのが当たり前になっています。しかし、今回のLocal Media Viewerではあえてフレームワークを使用せず、標準のJavaScript(Vanilla JS)のみでフロントエンドを構築しています。

ビルドツール不要、index.htmlapp.js だけで動くシンプルさは、学習用としても、長期的なメンテナンス性の観点からも魅力的です。今回は、フレームワークなしでどのようにアプリケーションの状態(State)を管理しているのかを解説します。

グローバル変数を「ストア」として使う

Reactの useState や Redux のような複雑な仕組みの代わりに、単純なグローバル変数で状態を管理しています。

// State
let allMediaFiles = []; // サーバーから取得した全データ
let filteredFiles = []; // 現在のタブでフィルタリングされたデータ
let currentModalIndex = -1; // モーダルで表示中の画像のインデックス
let currentTab = 'all'; // 現在選択中のタブ
let currentPage = 1; // 現在のページ番号
const PAGE_SIZE = 100; // 1ページあたりの表示数

これらが「Single Source of Truth(信頼できる唯一の情報源)」となります。 例えばフィルターを変更した場合、filteredFiles を更新し、それに依存するUI(グリッド表示やページネーション)を再描画するというフローになります。

データの取得と初期化 (fetchFilesFromServer)

アプリが起動(またはログイン完了)すると、サーバーからデータを取得します。

async function fetchFilesFromServer() {
    try {
        const response = await fetch('/api/files');
        // ...エラーハンドリング...
        const files = await response.json();

        // データの加工(URLの付与)
        allMediaFiles = files.map(f => ({
            ...f,
            url: `/media/${f.path}` // サーバーの仕様に合わせてパスを補完
        }));

        // フィルタリングと描画
        filterFiles(); 
        statusText.textContent = `Loaded ${allMediaFiles.length} items.`;
        
        currentPage = 1;
        renderPage(currentPage); // 1ページ目を描画

    } catch (err) {
        console.error('Error fetching files:', err);
    }
}

ここで重要な設計思想は、「データの取得」と「データの表示」を明確に分けている点です。 fetch で取得した生データはいったん allMediaFiles に格納され、UI描画関数である renderPage はこの変数のみを参照します。これにより、ページ切り替え時に再度通信する必要がなくなります。

タブ切り替えとフィルタリング (switchTab, filterFiles)

「すべて」「動画のみ」「画像のみ」といったタブ切り替え機能があります。

function switchTab(tab) {
    if (currentTab === tab) return;
    currentTab = tab;
    filterFiles();    // ステートを更新
    currentPage = 1;  // ページをリセット
    renderPage(currentPage); // 再描画
}

function filterFiles() {
    if (currentTab === 'all') {
        filteredFiles = allMediaFiles;
    } else {
        filteredFiles = allMediaFiles.filter(item => item.type === currentTab);
    }
}

switchTab がトリガーとなり、まず currentTab を更新します。次に filterFiles を呼び出して allMediaFiles から条件に合うものだけを filteredFiles に抽出します。 最後に renderPage を呼ぶことで、画面は最新の filteredFiles に基づいて更新されます。これがフレームワークを使わない場合の「リアクティブな動作」の基本形です。

DOM操作のパフォーマンス最適化 (DocumentFragment)

DOM操作はコストが高い処理です。ループの中で appendChild を繰り返すと、ブラウザの再描画(リフロー)が頻発して動作が重くなります。 これを防ぐために DocumentFragment を使用しています。

// Create DOM
galleryGrid.innerHTML = ''; // 既存の表示をクリア
const fragment = document.createDocumentFragment(); // メモリ上の仮想DOMコンテナ

pageItems.forEach((item, index) => {
    const card = document.createElement('div');
    // ... cardの中身を作成 ...
    fragment.appendChild(card); // フラグメントに追加(画面描画はまだ発生しない)
});

galleryGrid.appendChild(fragment); // 最後に一度だけDOMに追加(ここで描画される)

DocumentFragment は画面上に表示されないメモリ内のDOMノードです。ここに要素を全て詰め込んでおき、最後にまとめて galleryGrid に追加することで、ブラウザの再描画コストを最小限(1回)に抑えています。これにより、100件程度の画像リストであれば一瞬で表示できます。

まとめ

Vanilla JSでの開発は「不便」と思われがちですが、状態管理の基本(データ取得→加工→描画)さえ守れば、非常に見通しの良いコードが書けると思います。

  • 状態の集中管理: グローバル変数でデータの整合性を保つ
  • 関心の分離: fetch(通信)、filter(ロジック)、render(描画)を関数で分ける
  • パフォーマンス: DocumentFragment でDOM操作をまとめる

これらはReactやVueの内部で行われていることの原始的な形ですが、それを理解して書くことで、フレームワークへの理解もより深まると思います。

次回は、ページネーションとモーダルウィンドウ、そして動画再生におけるUX向上テクニックについて解説します。

次回: 快適なメディア閲覧体験を作る!クライアントサイド・ページネーションと高機能モーダルのUI/UX実装


同シリーズ記事