React不要!標準JavaScriptで実装する軽量SPAの状態管理と高速DOM操作テクニック
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.html と app.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実装
同シリーズ記事
- 第1回:Node.jsとExpressで構築するローカルメディアビューアー:プロジェクト構成と全体アーキテクチャ
- 第2回:Node.js Expressによるセキュアなメディア配信サーバーの構築手法と実装の全貌
- 第3回:DB不要!Node.js CryptoモジュールとHttpOnly Cookieで実現する安全な簡易認証システム
- 第4回:Node.jsで数千ファイル規模のディレクトリツリーを高速に解析する再帰アルゴリズムの実装
- 第5回:React不要!標準JavaScriptで実装する軽量SPAの状態管理と高速DOM操作テクニック
- 第6回:快適なメディア閲覧体験を作る!クライアントサイド・ページネーションと高機能モーダルのUI/UX実装
- 第7回:Node.jsのchild_processとFFmpegで作る!非対応動画ファイルのMP4自動一括変換ツール