Next.js の状態管理 2020
Next.js といえば、SSG(JAMstack)が最近は特に話題ですね。1年前まではgetInitialProps
を用いて、どう SSR するのかという事が話題の中心でした。Next.js 9.3 以降、SSR をする際にはgetInitialProps
ではなくgetServerSideProps
を使用することを推奨すると記載されています。(そして、getInitialProps
を使用することで自動最適化が無効となってしまう旨も)getStaticProps
やgetServerSideProps
を利用することで、私たちは SSG・SSR をページ単位で切り替えることができます。
「SSG・SSR」が共存する可能性がある場合、SSR にはgetServerSideProps
を利用することになります。この変化による影響範囲は多大で、状態管理とデータフェッチについて、再考する必要がでてきました。
一辺倒にはいかない状態管理設計
SPA が登場する以前、MVCフルスタックフレームワークの状態管理といえば、SessionID を軸としたページ間の状態共有が普通でした。状態に関するビジネスロジックはサーバーサイド側で処理され、Template Engine に渡された値を利用しマークアップを完成させることが従来フロントエンドエンジニアの仕事でした。
時代は SPA に移り「ページ間状態共有」といえば、Redux が筆頭候補にあがっていました。9.3 以前では「Next.js / SSR / Redux」というスタックはよく採用されていたのではないでしょうか。初期リクエスト時の内容を元に initalStore を生成すれば、状態を反映した SSR を初期提供することができます。これはgetInitialProps
を利用していた Next.js にも当てはまります。
これらの背景を鑑みて、今後の状態管理設計はどうするべきなのか? 個人的に判断材料としている3つの観点を紹介します。
(1)CSR / SSR の確認
これから作るものに対し、まずはそのページが「CSR・SSG・SSR」なのかを決めなければいけません。next build
コマンドで出力されるページには「λ / ○ / ●」のアイコンが振られます。page ファイル各々で宣言された、initial props
関数をもとに、Next.js がビルド時に解決します。
λ (Server) server-side renders at runtime (uses getInitialProps or getServerSideProps)
○ (Static) automatically rendered as static HTML (uses no initial props)
● (SSG) automatically generated as static HTML + JSON (uses getStaticProps)
(ISR) incremental static regeneration (uses revalidate in getStaticProps)
本稿では CSR(Client Side Rendering)という言葉を用いていますが、上記のうちでは 「SSRではないもの」 を指していると捉えてください。CSRは静的ファイルで構成されます。静的ファイルならばCDNにキャッシュ可能であり、キャッシュを利用するほどフロントエンドのパフォーマンスは高まります。
「要件としてSSRでなければならない」ことは多々あれど、静的ファイルで賄えるように実装していくことで、Next.js が推奨する JAMstack Oriented に近づきます。状態管理のライブラリ選定についても、この観点抜きには出来なくなっています。
(2)アプリケーション要件の確認
これから作るものに対し 「Read要件 / Write要件」 がどれほどか?というアプリケーション要件を確認します。作るものに対し、適切な道具(ライブラリ)は変わってきます。
-
「Read要件」の多いアプリケーション例
- ニュースサイトなどのメディア媒体
- 画面単位で責務が切り替わるショッピングサイト
- 技術ブログなど
-
「Write要件」の多いアプリケーション例
- SaaS のダッシュボード
- ユーザー編集の伴うアプリケーション
- 社内インナーツール
(3)アプリケーション特性の確認
もう一つ忘れてはならない観点が 「部分再描画頻度」 です。上記で示した「ユーザー編集の伴うアプリケーション」と一口に言っても「部分再描画頻度」はバラバラです。図で示している様に「赤枠部分」で入力操作をおこない編集したとします。「部分再描画」されている A と比較し、B は画面全体が再描画されています。B はまさに「従来MVC」で作っていたアプリケーションですね。
B はサーバーに全処理を委ね、得られた結果をフロントが表示するだけですから、フロントは簡素に・サーバーは複雑になってきます。どちらが適切なのか?という判断は 「何を作り・誰が運用し・どれほど公開されるのか」 という、プロダクト背景に強く依存します。
ライブラリ選定
ひとえに「フロントエンド」といっても上記3観点で、アプリケーションの作りはまったく異なることがわかりました。よく「状態管理談義」に上がってくる3つのアプローチを取り上げ、5段階評価(私感)してみましたので、参考にしてみてください。※ Recoil / MobX / jotai に関しては、検証不足のため扱いません。ご了承ください
React.useContext
モジュール上でReact.createContext
をコールし、生成されたインスタンスを参照することで、一元管理された値を取得します。
CSR | SSG | SSR | Read | Write | ReRender | Easy |
---|---|---|---|---|---|---|
3 | 5 | 5 | 5 | 4 | 3 | 5 |
- pros
- フェッチデータを注入するだけの「従来MVC」アプローチである場合は最適
- 参照が直感的なため、片手間フロント開発でも理解しやすく、運用しやすい
- cons
- 値の合成・メモ化観点で Redux(reselect) に劣る
- 再描画最適化を目指すほど、Provider を分割し幾重にも重ねる必要がある
【総評】
「部分再描画頻度」が低いアプリケーションに最適です。
サーバーサイドから得られた値を表示するだけ、という場合は特に効果が高いです。props down を積極的に減らし、必要なコンポーネントでReact.useContext
することで、React.memo
化を減らすことができます。
「複雑UI」がアプリケーション要件の場合、独自設計で乗り切る必要があります。「複雑UI」構築に Redux が別候補に挙がっているなら、reselect による値単位でのメモ化が強力なため、そちらに軍配があがります。
SWR ・ React Query
一度フェッチしたデータをキャッシュし、再利用する Hooks を提供しています。revalidate option を調整することで使用用途は多岐に渡ります。「polling を用いて一定間隔で最新のデータを取得する」という極端なことから「ページをリロードしない限り、一度fetchしたデータを再利用し続ける」という応用も可能です。
CSR | SSG | SSR | Read | Write | ReRender | Easy |
---|---|---|---|---|---|---|
5 | 4 | 3 | 4 | 5 | 5 | 4 |
- pros
- キャッシュ再利用により再描画遅延が減る
- 一定期間の重複リクエストは排除する機構を標準で備えている
- 複数コンポーネントから雑多に呼びやすい
- 片手間でも理解しやすい
- cons
- APIレスポンスに強依存したコンポーネントになる(GraphQL とセットなら強そう)
- 取得したデータをフロントで再加工する処理が多い場合不向き
- REST の場合、API設計段階からフロント都合を盛り込むべき
【総評】
「部分再描画頻度」が高いアプリケーションに最適です。
Read / Write を高機能にこなす有力な選択肢であることは間違いありません。「画面から離れフォーカスが再び当たったタイミングで検証する」など諸機能は、他ライブラリで再現するには骨の折れる魅力的な機能です。
しかしながら、一見簡単そうに見えて、本領発揮のためには練度が求められるライブラリだという感想を持っています。正しくチューニングが行えない場合、リクエストがみだりに増えてしまう可能性が高いです。Network パネルを常に見ながら開発することに慣れていない(慣れていないメンバーが運用)する場合は特に、ライブラリ詳細をきちんと理解する必要があります。
SSR評価が低いですが、SSRはどちらも対応しています。これは非推奨とされているgetInitialProps
の方が、よりライブラリ特性を活かすことができたのではないか、という感想によるものです。(getServerSideProps
は毎回 Server で fetch が走るため)
Redux
「Action」とうインターフェィスで、データとUIを統合します。登場初期の「大きなコンポーネントからバケツリレーすること」という言説が原因で、現場に多くの誤解を招いているライブラリでもあります。
CSR | SSG | SSR | Read | Write | ReRender | Easy |
---|---|---|---|---|---|---|
5 | 3 | 3 | 4 | 5 | 5 | 2 |
- pros
- 「部分再描画・ページ横断状態共有・取得データの算出結果メモ化」に優れる
- 様々な状態を統合することが可能
- ライブラリとして枯れている
- cons
- 片手間では理解できない
- 間違った使い方をすると逆にパフォーマンスが落ちる
【総評】
周知のとおり、本領発揮のためには練度が求められるライブラリです。3者の中では特に、フロントエンドにビジネスロジックが集中するアプリケーションに最適です。reselect による共有メモは特に魅力が高く、粒度の小さいコンポーネント単位で connect することにより、コンポーネント単位でのメモ化が不要になることもあります。
従来から言われていることですが、アプリケーション要件を見据え「本当に必要なのか」判断する必要があります。しかし、長年様々な現場で利用されてきた実績があり、今後も新規案件の筆頭として選ばれるだけの底力があります。
最後に
昨今の Next.js「JAMstack推し」が与えた影響はとても大きいです。SWRに関しては単なるデータフェッチライブラリではなく 「SSG出来ない認証必要な部分レンダリングはSSRを使わず、SWR(CSR)を使ってほしい。そうすればSSG可能なページは多くなる」 という、Vercel提供ライブラリならではのメッセージが伝わってきます。
一方でreq
を必ず取得できるgetServerSideProps
は SessionID を軸にした「従来MVC」パターンも可能となっており、getInitialProps
も deprecated になった訳ではありません。この様な柔軟な姿勢から、段階的に最適化(静的化)を進めることができます。
これらの背景などを鑑みて、実践においては複数の状態管理ライブラリを併用することになるでしょう。何よりアプリケーション特性を見極めることが肝要です。成功体験は都度リセットし、都度要件と向き合って選択していきましょう。
Discussion