Next.js における レスポンシブの考察
フロントエンドエンジニアの茶木です。
Next.js におけるレスポンシブ、PC/SPの出し分けについてSSRの観点も含めて考察します。
具体的には、以下の切り替えについて、どのように実現するかについての考察です。
- スタイリングの切り替え
- 要素自体の描画を行うかどうか
はじめに
サーバーサイドレンダリング(SSR)
Next.js はサーバーサイドレンダリング(SSR)をサポートしています。
SSRは、サーバー側で HTML を生成し、クライアントに返し表示することで、初期表示の高速化を実現します。
Next.js では、SSRは、クライアントサイドレンダリング(CSR)と混在させられます。基本的には SSRでレンダリングするべきなのですが、サーバーサイドで行えない、クライアントに依存した動的な表示を行う場合が存在します。
PC/SPの出し分け
踏まえて、PC/SP の出し分けの判定について考えます。出し分けは、画面幅や UserAgent
から判定をするのが一般的だと思います。ここでは画面幅の判定として話を進めます。画面幅は実行時の情報で window.innerWidth
で取得できます。
しかし、サーバーサイドレンダリングの際に window
は取得できません。このため PC/SPの出し分けはクライアントサイドで行う必要があります。
考察
対象(実現方法)
以下の対象について考察します。
- CSS Media query
- MUI useMediaQuery
- react-responsive MediaQuery
- window.matchMedia
PC/SPの出し分けの内容
出し分けの内容は以下を想定しました。
- 要素自体を描画するかどうか
- 要素に当たっているスタイルの変更
判断基準
- SSRのHTML
- コードの簡便さ
補足
Next.js は 13以上を想定(デフォルトがSSRになる。12以下は CSR)
結論
先に結論を述べます
- 要素自体の描画切り替えは
MUI useMediaQuery
が良い - スタイルの変更は
CSS Media query
が良い
組み合わせて使用するのが良いと思います。
考察結果
基本のコード
export default function Index() {
return (
<Card>
<p>PC</p> OR <p>SP</p> /* 1. 要素自体の描画切り替え */
<p>テキストサイズ</p> /* 2. スタイルの切り替え(例として font-size の変更) */
</Card>
)
}
1024px
をブレイクポイントとして、SPとPCの出し分けについて 1, 2 を実現するコードを書きます。
CSS Media query
CSSの Media query によって、非表示の記述とスタイルをPCとSP用にそれぞれ準備します。
@media screen and (min-width: 1024px) {
/* PC */
.sp-only { display: none; }
.size-normal { font-size: 1.4rem; }
}
@media screen and (max-width: 1023px) {
/* SP */
.pc-only { display: none; }
.size-normal { font-size: 1.2rem; }
}
export default function Index() {
return (
<Card>
<p className="pc-only">PC</p><p className="sp-only">SP</p>
<p className="size-normal">テキストサイズ</p>
</Card>
)
}
コンポーネントはステートレスです。JavaScript による DOM要素の書き換えは発生しません。
SSR結果
<p class="pc-only">PC</p><p class="sp-only">SP</p>
<p class="size-normal">テキストサイズ</p>
要素自体の描画の切り替えはできません。(ここでは、pc-only
sp-only
の display: none
で、非表示として代替しています)SSRの結果は PC/SP の両方がレンダリングされます。
スタイルの切り替えも JavaScript を介しません。
MUI useMediaQuery
MUI が提供しているフックです。
export default function Index() {
const isPc = useMediaQuery("(min-width: 1024px)");
return (
<Card>
{isPc ? <p>PC</p> : <p>SP</p>}
<p style={{ fontSize: isPc ? "1.4rem" : "1.2rem" }}>テキストサイズ</p>
</Card>
)
}
SSR結果
<p>SP</p>
<p style="font-size: 1.2rem;">テキストサイズ</p>
要素自体の描画が切り替わります。また、SSRではSPのみがレンダーされます。
このため、ロード時にPC幅であってもSP版が一瞬表示されます。
スタイルの切り替えは style
の値で行っています。MUI の styled
などでも変更も可能です。
react-responsive MediaQuery
react-responsive ライブラリが提供しているコンポーネントです。
import dynamic from "next/dynamic";
const MediaQuery = dynamic(() => import("react-responsive"), {
ssr: false,
});
動的 import を使用し、 ssr: false
を指定しています。これにより、SSR時に読み込まれなくなります。
(ちなみに、Next12 以前はデフォルトがCSRなので通常の import でも良さそうです)
export default function Index() {
return (
<Card>
<MediaQuery minWidth={1024}>PC</MediaQuery>
<MediaQuery maxWidth={1023}>SP</MediaQuery>
<p>テキストサイズ</p>
</Card>
)
}
SSR結果
<p>テキストサイズ</p>
要素自体の描画の切り替え可能です。ただし、SSRでは、SP/PCの出し分けはどちらもレンダリングされません。また、スタイリングの変更ができません。
window.matchMedia
matchMedia を使ってスクラッチすることもできます。
const useIsPc = () => {
const [isPc, setIsPc] = useState(false);
useEffect(() => {
const mediaQueryList = window.matchMedia("(min-width: 1024px)");
const listener = () => setIsPc(mediaQueryList.matches);
mediaQueryList.addEventListener("change", listener);
return () => mediaQueryList.removeEventListener("change", listener);
}, []);
return isPc;
};
export default function Index() {
const isPc = useIsPc();
return (
<Card>
{isPc ? <p>PC</p> : <p>SP</p>}
<p style={{ fontSize: isPc ? "1.4rem" : "1.2rem" }}>テキストサイズ</p>
</Card>
)
}
SSR結果
<p class="pc-only">PC</p><p class="sp-only">SP</p>
<p class="size-normal">テキストサイズ</p>
ソースの解説をすると、useEffect
内は、SSR時には実行されないので、window
を参照しても問題ありません。
要素自体の描画が切り替わります。また、SSRではSPのみがレンダーされ、ロード時にPC幅であってもSP版が一瞬表示されます。スタイルの切り替えは style の値で行っています。ようするに MUI useMediaQuery
とほぼ同じ結果になります。
まとめ
window.matchMedia
を除けば、それほど長く読みにくいコードもない印象でした。
スタイリングは CSS Media query
結論をおさらいすると、スタイリングの変更には JavaScript を記述せず、DOMツリーの変更が伴わない、CSS Media query を使うのが最もシンプルでセマンティックなHTMLになると考えられます。
ただし、描画自体の切り替えは、display:none
での擬似的な非表示しかができないので、PC/SPで表示内容が違ったり、大きくレイアウトが変わる場合には適応しきれないケースがあるでしょう。
要素自体の描画の切り替えは MUI useMediaQuery
続いては、要素自体の描画の切り替えです。MUI useMediaQuery
と react-responsive MediaQuery
の二択です。
( window.matchMedia
は useMediaQuery
のほぼスクラッチなので除外)
MediaQuery
は、SSR時にレンダーされない点が気になります。
初期状態が未表示なので画面がちらつかず、ユーザー体験観点での利点でもあるのですが、セマンティックなHTMLという観点からは、SP/PC どちらかだけがレンダリングされている方が良いという判断がありそうです。この点では MUI useMediaQuery に理があります。
実は、MediaQuery と同じように、MUI useMediaQuery でも SSR時にどちらもレンダーしないようにも書けます。以下に示します。
const isPc = useMediaQuery("(min-width: 1024px)");
const isSp = useMediaQuery("(max-width: 1023px)");
return (
<Card>
{isPc && <p>PC</p>}
{isSp && <p>SP</p>}
<p>テキストサイズ</p>
</Card>
)
というわけで、 MUI useMediaQuery は、SSR のHTMLまで含めて、描画するしないをコントロールできる点で良さそうです。
補足: react-responsive の useMediaQuery
実は、react-responsive
にも同名のフックの useMediaQuery
があります。
機能も近いのですが、選考から除外しています。
理由は、SSR時には この useMediaQuery は false を返すのですが、一方で、ロードの初回から queryの判定に準じた値、つまり場合よっては true を返すため、SSRとCSRの初回でDOM構造が一致しないエラー Hydration Error を引き起こすためです。回避のためには、matchMedia
と同様に、useEffect で初期レンダー時に 必ず false を返すなどの工夫が必要になります。もっとふさわしい回避方法があるのかもしれませんが、発見できませんでした。なお、Next 12以前では、デフォルトでは、おそらく発生しない問題です。
おわりに
次は、MUI useMediaQuery を使った描画の出し分けのための、カスタムフックやコンポーネントを考えようと思います。
Gaji-Laboでは、 Next.js 経験が豊富なフロントエンドエンジニアを募集しています
弊社では Next.js の知見で事業作りに貢献したいフロントエンドエンジニアを募集しています。大きな制作会社や事業会社とはひと味もふた味も違う Gaji-Labo を味わいに来ませんか?
Next.js の設計・実装を得意とするフロントエンドエンジニア募集要項
もちろん、一緒にお仕事をしてくださるパートナーさんも随時募集中です。まずはお気軽に声をかけてください。お仕事お問い合わせや採用への応募、共に大歓迎です!