{"componentChunkName":"component---src-templates-blog-template-js","path":"/2026-07-12-weekly-knowledge","result":{"data":{"site":{"siteMetadata":{"title":"KenRp Blog"}},"markdownRemark":{"html":"<p><code class=\"language-text\">shared-shopping-app</code> は、Next.js 15 と React 19、TypeScript で構成された共有買い物リストアプリである。2026年07月06日から2026年07月12日のワークログを見直すと、この週で最も再利用しやすい知見は、設定画面の初回ロード中に旧ヘッダーや本文コンテナが一瞬見えてしまう問題を、全画面ローダーと CSS 制御、Playwright の受け入れテストで潰した実装だった。</p>\n<p>画面のロード中にローダーは出ているのに、背後で古い UI がちらつく不具合は、見た目の小さな違和感で終わらない。PC レイアウト最適化後は特に、<code class=\"language-text\">main</code> の余白や <code class=\"language-text\">topbar</code> が残っているだけで「前の画面が一瞬露出した」ように見え、アプリ全体の完成度を下げる。この記事では、共通ローダーを使う Next.js アプリで、このちらつきをどう抑えるかを整理する。</p>\n<h2>この記事で解決すること</h2>\n<ul>\n<li>全画面ローダーを出しているのに、旧 UI が背後で一瞬見える問題をどう止めるか分からない。</li>\n<li>React コンポーネントと CSS のどちらに責務を置くべきか判断したい。</li>\n<li>UI 修正だけで終わらせず、E2E、build、配備導線まで含めて変更の境界を整理したい。</li>\n</ul>\n<h2>実装の要点</h2>\n<p>この週次記事で扱う変更は、<code class=\"language-text\">shared-shopping-app</code> の設定画面ロード中表示の改善に絞っている。具体的には、設定画面コンポーネント <code class=\"language-text\">components/list-settings-client.tsx</code> でロード中専用の暫定ヘッダーと簡易カードを返していた構成をやめ、既存の共通部品 <code class=\"language-text\">FullScreenAppLoader</code> に統一した。</p>\n<p>同時に <code class=\"language-text\">app/globals.css</code> へ、<code class=\"language-text\">.app-loading-screen</code> が存在する間は <code class=\"language-text\">.page-shell &gt; .topbar</code> を隠し、<code class=\"language-text\">main</code> の幅・余白・padding をリセットするルールを追加した。これでローダーを表示している間に、PC 向けレイアウトで広がった本文領域や旧ヘッダーが背後に残らないようにした。</p>\n<p>検証面では <code class=\"language-text\">tests/e2e/guest-list.spec.ts</code> を更新し、ローダー表示中に <code class=\"language-text\">.topbar</code> が hidden であること、<code class=\"language-text\">main</code> 配下に <code class=\"language-text\">.app-loading-screen</code> 以外の旧 UI がないことを Playwright で確認する受け入れ条件を追加した。</p>\n<h2>得られた知見</h2>\n<p>成果は、ローディング中の体験を「ローダーを表示する」から「ローダー以外を見せない」へ定義し直せたことにある。設定画面だけ別の待機 UI を持つ状態をやめ、アプリ共通の待機表示に統一したことで、ページごとに異なる仮表示が混ざる状態を解消できた。</p>\n<p>コード上の成果として確認できたのは次の3点である。</p>\n<ul>\n<li>設定画面のロード中表示が <code class=\"language-text\">FullScreenAppLoader</code> 1つに集約された。</li>\n<li>CSS がページシェル全体を抑制するようになり、ヘッダーと本文コンテナの露出経路が塞がれた。</li>\n<li>Playwright が「ローダーが見える」だけでなく「旧 UI が残っていない」ことまで検証するようになった。</li>\n</ul>\n<p>実際に根拠として確認できた差分は、commit <code class=\"language-text\">d435e20</code> で <code class=\"language-text\">app/globals.css</code>、<code class=\"language-text\">components/list-settings-client.tsx</code>、<code class=\"language-text\">components/nav.tsx</code>、<code class=\"language-text\">tests/e2e/guest-list.spec.ts</code> の 4 ファイルに限定されていた点である。変更が UI と E2E に閉じていたため、今回の記事も API や DB の話を混ぜずに説明できる。</p>\n<p>一方で、ワークログから確認できる範囲は commit <code class=\"language-text\">d435e20</code> の差分とテスト更新までである。<code class=\"language-text\">npm run build</code>、<code class=\"language-text\">npm run test:e2e</code>、<code class=\"language-text\">npm run cf:build</code>、<code class=\"language-text\">npm run cf:deploy</code> はリポジトリ側に用意された build / test / deployment / release 導線として確認できるが、この日の source material から実行結果までは確認できなかった。そのため、この変更は「配備可能な構成へ寄せた UI 改修」であり、「本番反映まで完了した変更」とは書かないのが正確である。</p>\n<h2>実装プロセス</h2>\n<p>最初に切り分けるべきだったのは、問題がローダー部品の見た目ではなく、ページシェル全体の責務にあるという点だった。設定画面ではロード中に専用の app bar と status card を描いていたため、React ツリー上で旧ページ構造が一度出てから、新しい待機表示へ置き換わる余地が残っていた。</p>\n<p>そこで実装は 2 段階に分けた。1 段階目は <code class=\"language-text\">components/list-settings-client.tsx</code> で <code class=\"language-text\">isLoading</code> 中の返り値を <code class=\"language-text\">FullScreenAppLoader</code> だけにし、不完全な設定画面構造を出さないようにすること。2 段階目は <code class=\"language-text\">app/globals.css</code> に <code class=\"language-text\">body:has(.app-loading-screen)</code> 条件のルールを追加し、ローダーが存在する間は <code class=\"language-text\">.topbar</code> と <code class=\"language-text\">main</code> の通常レイアウトを抑制することだった。</p>\n<p>さらに <code class=\"language-text\">components/nav.tsx</code> を fragment 構造へ調整し、ログアウト時ローダーのような全画面オーバーレイもヘッダー外に重ねやすい DOM に整えた。最後に Playwright で、初回表示時に <code class=\"language-text\">.topbar</code> が hidden であり、<code class=\"language-text\">.category-card-skeleton</code> や古い本文パネルが出ていないことを受け入れ条件に固定した。見た目の違和感を DOM 条件へ落とし込んだことで、将来の CSS 変更でも同じ不具合を検出しやすくなった。</p>\n<h2>利用した技術</h2>\n<ul>\n<li>アプリ本体は Next.js 15 の App Router、React 19、TypeScript で構成されている。</li>\n<li>共通ローディング UI は <code class=\"language-text\">components/app-loader.tsx</code> の <code class=\"language-text\">FullScreenAppLoader</code> を使い、設定画面専用の暫定 UI を新設しない方針を取った。</li>\n<li>画面制御は <code class=\"language-text\">app/globals.css</code> に集約し、<code class=\"language-text\">body:has(.app-loading-screen)</code> を使ってページシェル全体の chrome を抑制した。</li>\n<li>設定画面の実装は <code class=\"language-text\">components/list-settings-client.tsx</code>、共通ヘッダーは <code class=\"language-text\">components/nav.tsx</code> が担う。</li>\n<li>受け入れテストは Playwright の <code class=\"language-text\">tests/e2e/guest-list.spec.ts</code> で、ローダー表示中の DOM 条件を確認する。</li>\n<li>リポジトリには <code class=\"language-text\">npm run test:e2e</code> が E2E、<code class=\"language-text\">npm run build</code> が Next.js build、<code class=\"language-text\">npm run cf:build</code> と <code class=\"language-text\">npm run cf:deploy</code> が Cloudflare 向け deployment / release 導線として定義されている。</li>\n</ul>\n<p>今回の変更そのものは UI 実装と E2E テスト更新に閉じており、build 設定や deployment スクリプトの定義自体は変更していない。つまり、ここで使った技術の中心は「表示責務の整理」と「回帰確認の強化」である。</p>\n<h2>アーキテクチャ</h2>\n<p>この変更の要点は、ローディング状態の責務を 3 層に分けたことにある。待機表示そのものは <code class=\"language-text\">FullScreenAppLoader</code> が担い、ページ固有のロード判断は <code class=\"language-text\">ListSettingsClient</code> が担い、待機中に通常 chrome を引っ込める責務は CSS が担う。これにより、ローダーの文言や見た目を共通化しつつ、ページシェルの露出抑制を個別ページへ書き散らさずに済む。</p>\n<div class=\"gatsby-highlight\" data-language=\"mermaid\"><pre class=\"language-mermaid\"><code class=\"language-mermaid\">flowchart TD\n  A[&quot;ListSettingsClient&lt;br/&gt;isLoading を判定&quot;] --&gt; B[&quot;FullScreenAppLoader を表示&quot;]\n  B --&gt; C[&quot;body:has(.app-loading-screen)&quot;]\n  C --&gt; D[&quot;topbar を非表示&quot;]\n  C --&gt; E[&quot;main の幅・余白をリセット&quot;]\n  F[&quot;Playwright E2E&quot;] --&gt; D\n  F --&gt; E</code></pre></div>\n<p>ここで重要なのは、React 側だけで解決しようとしなかった点である。ローダーが出ていても、共通ナビゲーションや <code class=\"language-text\">main</code> のレイアウト制約が別レイヤーで残っていれば、見た目としては不完全な画面が露出する。だからこそ、コンポーネント、DOM 構造、CSS、E2E がそれぞれ違う責務を持つ構成にした判断が効いている。</p>\n<h2>選定理由</h2>\n<p>設定画面専用の loading app bar を維持せず共通ローダーへ寄せたのは、待機中 UI をページごとに増やすと、表示差異と回帰確認のコストが増えるからである。共通ローダー 1 つに寄せれば、ローディング体験の変更点を一か所に集約できる。</p>\n<p><code class=\"language-text\">topbar</code> や <code class=\"language-text\">main</code> を React の条件分岐で個別に制御しなかったのは、ページシェル全体を抑制したい要件に対して、CSS の方が責務の置き場所として素直だったからである。<code class=\"language-text\">body:has(.app-loading-screen)</code> はやや強いセレクタだが、「全画面ローダーが存在する間は通常 chrome を出さない」という契約を明快に表現できる。</p>\n<p>Playwright を使って hidden 判定と DOM 件数確認を入れたのは、スクリーンショットの印象比較だけでは「旧 UI が残っていない」ことを安定して表現しづらいためである。今回は見た目の調整ではなく、受け入れ条件の明文化が価値だった。</p>\n<p>また、この改善を UI 層と E2E 層に閉じたのは妥当だった。問題の原因は API やデータストアではなく、初回描画とページシェルの見せ方にあったからである。影響範囲を最小化しながら修正できる選択だった。</p>\n<p>採用しなかった案もある。ひとつは設定画面専用の loading header を残したまま見た目だけ調整する案で、これはページごとの差異と保守コストを増やすため見送った。もうひとつは <code class=\"language-text\">nav</code> や <code class=\"language-text\">main</code> を React 条件分岐で個別に隠す案で、共通 chrome の抑制責務が複数コンポーネントへ分散するため採らなかった。</p>\n<h2>導入手順</h2>\n<p>同じ問題を別の Next.js アプリへ導入するなら、次の順で進めると再現しやすい。</p>\n<ol>\n<li>ロード中に表示している専用ヘッダーや仮カードがないか確認し、ページ固有の暫定 UI を洗い出す。</li>\n<li><code class=\"language-text\">isLoading</code> などの待機判定で、ページ固有の暫定 UI を返す代わりに共通の全画面ローダーだけを返す。</li>\n<li><code class=\"language-text\">.app-loading-screen</code> のようなマーカーを使い、CSS 側で <code class=\"language-text\">topbar</code>、<code class=\"language-text\">main</code>、余白、幅制約などの通常 chrome を抑制する。</li>\n<li>全画面ローダーが共通ヘッダーやログアウト処理と競合しないよう、必要ならヘッダー側の DOM 構造を調整する。</li>\n<li>Playwright などの E2E で「ローダーが表示される」だけでなく、「旧 UI が残っていない」ことを hidden 判定や DOM 件数で固定する。</li>\n<li>実装後は少なくとも <code class=\"language-text\">npm run test:e2e</code> と <code class=\"language-text\">npm run build</code> を回して、UI 回帰と build 崩れを確認する。</li>\n<li>Cloudflare Workers や OpenNext で配備する構成なら、必要に応じて <code class=\"language-text\">npm run cf:build</code>、<code class=\"language-text\">npm run cf:deploy</code> を release 手順へつなぐ。ただし今回の source material では、この配備実行までは確認できていない。</li>\n</ol>\n<p>この事例で根拠確認に使えたコマンドは次のとおりである。</p>\n<div class=\"gatsby-highlight\" data-language=\"bash\"><pre class=\"language-bash\"><code class=\"language-bash\"><span class=\"token function\">git</span> -C ~/git/shared-shopping-app show --stat --summary --format<span class=\"token operator\">=</span>fuller d435e20\n<span class=\"token function\">git</span> -C ~/git/shared-shopping-app show --format<span class=\"token operator\">=</span>medium --unified<span class=\"token operator\">=</span><span class=\"token number\">80</span> d435e20 -- app/globals.css components/list-settings-client.tsx components/nav.tsx tests/e2e/guest-list.spec.ts</code></pre></div>\n<p>テストの観点では、<code class=\"language-text\">tests/e2e/guest-list.spec.ts</code> に <code class=\"language-text\">await expect(page.locator(&quot;.topbar&quot;)).toBeHidden()</code> と <code class=\"language-text\">await expect(page.locator(&quot;main &gt; :not(.app-loading-screen)&quot;)).toHaveCount(0)</code> が追加されており、ちらつき対策の受け入れ条件がコードとして固定されている。</p>\n<h2>導入時の課題</h2>\n<p>一番の課題は、全画面ローダーを出していても、背後の <code class=\"language-text\">topbar</code> や <code class=\"language-text\">main</code> が残るだけで不具合に見える点だった。特に PC レイアウト最適化後は、余白や幅制約が強く見えるため、ローダーの背後に旧 UI が残ると違和感が増幅される。</p>\n<p>次の課題は、共通ヘッダーがアプリ全体で使われていることだった。設定画面だけ直したくても、ローダーとの重なり方は <code class=\"language-text\">nav</code> 側の DOM 構造に依存する。そのため、ページ単体の条件分岐だけでは足りず、共通部品との接続面まで見直す必要があった。</p>\n<p>最後に、見た目の問題はテストへ落とし込みにくい。今回は <code class=\"language-text\">.topbar</code> の hidden 判定と <code class=\"language-text\">main &gt; :not(.app-loading-screen)</code> の件数確認へ変換したが、このように DOM 条件へ翻訳しないと、将来の変更で同じちらつきが戻りやすい。</p>\n<h2>次に検討した方が良いこと</h2>\n<ul>\n<li>設定画面以外でも <code class=\"language-text\">FullScreenAppLoader</code> を使う箇所を洗い出し、同じ CSS 契約を横展開する。</li>\n<li><code class=\"language-text\">npm run test:e2e</code> を継続的に回し、ログイン画面、公開リンク画面、ログアウト処理でも旧 chrome が露出しないか確認する。</li>\n<li><code class=\"language-text\">npm run build</code> と <code class=\"language-text\">npm run cf:build</code> を CI へ載せ、UI 修正が OpenNext / Cloudflare 向け build を壊していないか自動検知する。</li>\n<li><code class=\"language-text\">body:has(...)</code> の依存を続けるなら、サポートブラウザ方針とフォールバック方針を明文化する。</li>\n<li>本番配備前には preview 環境で初回描画を実機確認し、E2E では拾いきれない体感差分を補う。</li>\n</ul>","excerpt":"は、Next.js 15 と React 19、TypeScript で構成された共有買い物リストアプリである。2026年07月06日から2026年07月12日のワークログを見直すと、この週で最も再利用しやすい知見は、設定画面の初回ロード中に旧ヘッダーや本文コンテナが一瞬見えてしまう問題を、全画面ローダーと CSS…","frontmatter":{"date":"2026年07月12日","path":"/2026-07-12-weekly-knowledge","title":"Next.js の全画面ローダーで旧 UI のちらつきを防ぐ実装","thumbnail":"/kenRp-blog/assets/kenrp-blog-replace-wireframe.jpg","metaDescription":"shared-shopping-app の設定画面で、ロード中に旧ヘッダーや本文が一瞬見える問題を、共通ローダーと CSS 制御、Playwright 検証で抑えた実装を整理する。","category":"Knowledge","tags":["nextjs","playwright","weekly-knowledge"]}}},"pageContext":{"slug":"/2026-07-12-weekly-knowledge"}}}