簡単にReactを使ったSSRができるNext.jsを使っていて外部APIをfetchするときに色々戸惑ったので覚書き。
公式ドキュメントが結構偏っているのと日本語情報も少ないのでわりと手探りでどうすればいいか考えていきます。
next 9.1.7 react 16.12.0
データフェッチ
ドキュメントではdata fetchingとしてgetInitialProps()
を使う方法があります。
これをこのまま他の外部APIに変更するとすぐにCORS policy: No 'Access-Control-Allow-Origin' header
と怒られることになります。
CORS(Cross-Origin Resource Sharing)許可をしていない大抵のAPIはサーバーサイドであれば大丈夫ですが、クライアント(ブラウザ)で実行したときに上記エラーになります。
つまりこの読み込みでAPIを叩くときはサーバーサイド実行を意識したり、クライアントで動く可能性があるときはCORS許可されたもの、あるいは自分自身のAPIを使う。
サーバーかクライアントか判断する
getInitialProps(ctx)
がサーバーサイドかクライアントサイドか判断するには引数オブジェクトの{ req, res }
が存在するかどうかを見ます。
1 2 3 4 5 |
Page.getInitialProps = async ({ req }) => { console.log(`${req ? "-server side" : "-client side"}`); ... return {} } |
カスタムページでは_app.js
は引数オブジェクトが一つ上でコンテクストがctx
にあるのでobj.ctx.req
で判断します。
1 2 3 4 |
MyApp.getInitialProps = async obj => { console.log(`${obj.ctx.req ? "-server side" : "-client side"}`); ... } |
また_document.js
はそもそもクライアントで実行されません。
Nextライフサイクルとコンテクストオブジェクト
上の例であるようにドキュメントのcontext objectはページのものです。
ライフサイクルと共に画像にしてみます。
まずは初回アクセス時の動作です。
getInitialProps()
から完了してからReactのライフサイクルです。
また内部では_app.js → page.js → _document.js
の順に実行されます。
また最初のアクセスでgetInitialProps()
はサーバーサイドでのみ実行されます。
次に内部で遷移するときの動きを見てみます。
サーバー側での処理はありません。なので_document
も動きません。
パラメータとしてはreq, res
がないので処理の分岐ができるわけです。
getInitialProps()
がクライアントで走るんですが、ここで外部APIを叩いていると最初に書いたエラーになります。
ところでカスタムページのgetInitialProps
は途中でawait
処理が入ります。
これをふまえてどういった順番で動くか確認してみます。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 |
//_document.js MyDocument.getInitialProps = async ctx => { ctx.doc = "doc"; console.log("doc1-", ctx.app, ctx.page, ctx.doc); const t = await Document.getInitialProps(ctx); console.log("doc2-", ctx.app, ctx.page, ctx.doc); return { ...t }; }; //_app.js MyApp.getInitialProps = async ({ Component, ctx }) => { ctx.app = "app"; console.log("app1-", ctx.app, ctx.page, ctx.doc); let pageProps = {}; if (Component.getInitialProps) { pageProps = await Component.getInitialProps(ctx); } console.log("app2-", ctx.app, ctx.page, ctx.doc); return { pageProps }; }; //page.js page.getInitialProps = async ctx => { ctx.page = "page"; console.log("page-", ctx.app, ctx.page, ctx.doc); return {}; }; |
ついでにレンダー処理もログに出すようにします。
1 2 3 4 5 6 7 8 |
app1- app undefined undefined page- app page undefined app2- app page undefined doc1- app page doc render app render page doc2- app page doc render doc |
クライアント側はこれの_document
をなくしたものとなります。
_app
はawait
後にpage
のコンテクストを使えるが_document
は他処理後に始まるようで思ってたより変な動きをしています。_document
はSSRされたものを注入するものだと考えればこの順番も納得できます。
それでどうするか
まず内部遷移をなくすという方法が考えられます。
コード的には<Link>
を<a>
に変える感じになりますが、Reactの利点を消しつつ多くのデメリットを抱えることになり全体的にもっさりした動作になると思います。
遷移に依存しないところでパラメータを管理してもいいです。
最初のサーバーアクセスで取得した情報を使いまわす感じです。
_document
で管理するなどの方法もできなくはないですが、_app
でサーバー判定しつつprops
に注入してconstructor
でグローバル管理すればそれっぽく動きます。
router.push
を使用して遷移時にパラメータを渡すというアクロバティックな方法も考えられます。
これらはサーバーとクライアントを意図的に分けて対策する方法なので注意書きにあるように適切な扱いをしないとサーバー用モジュールをクライアントに読み込ませてしまったりして速度の低下をもたらします。
If you are using server-side only modules inside getinitialProps, make sure to import them properly, otherwise it’ll slow down your app
別方向の対策としてCORSが問題になるなら自前でラップしたAPIを用意する方法があります。Next.jsなら数行。
1 2 3 4 5 |
//src/pages/api/[xxx].js export default async (req, res) => { const r = await fetch(`https://api.xxx.com/?abc=${req.query.xxx}`); res.json(r.body); }; |
パラメータもダイナミックルーティングもreq.query
に格納されるのでそのまま外部APIに流して結果を返すだけです。
同じNetx上に乗ってはいますがこっちの方が疎な関係です。
将来的にAPIを切り分けてnext export
で静的ファイルにしたりもできるかな。
この場合は無駄な通信が発生しないようにキャッシュ利用なんかが重要になりそう。
どの方法にしてもgetInitialProps()
で取得する以上、アクセスに時間がかかればその分表示が遅れることになるのでタイムアウトも気にしておきたい。
追記:サーバーかクライアントかの判定
process
の違いを見てて気づきましたがprocess.browser
で判定できました。
ブラウザのグローバルオブジェクトwindow
の有無で確かめる方法もあります。同様にブラウザ特有のdocument
でも判定できます。
逆にglobal
の有無でも行けるかと思ったけどブラウザでも定義されていたため判定には使えませんでした。
ドキュメントにはないですがサンプルでは使っているのもありましたし、こっちの方が使い勝手がよさそう。
1 |
console.log("isBrowser\n", process.browser, typeof window !== "undefined"); |