next.jsでSSRにこだわるとどうなるか

前提

  "dependencies": {

	"next": "10.0.3",

	"react": "17.0.1",

	"react-dom": "17.0.1"

  }

言語的な癖はさておき、あえてサーバサイドレンダリングにこだわってコードを書く中で出くわした問題点やら何やらを書き残しておく。

POSTしてもbodyが見れない

IDとPassのログインフォームをSSRでつくろうとした。しかし、"getServerSideProps(context)"で"context.req"内にPOSTデータのボディがないのである。何かしらのrestfulなAPIのurlにfetchするのがnextの掟だそうだ。

不完全すぎる。

pagesディレクトリとリライト

まずはディレクトリ遊びを書いておく。"pages/index.js"のみが設置されている前提とする。ここに"pages/index"というディレクトリを作ると"/"にアクセスされると思いきや"pages/index"の方にアクセスされ"pages/index/index.jsファイル"がないので404のレスポンスが返ってくる。ここまでは良しとする。

次に"pages/index.js"と"pages/index/index.js"の表示内容が同じなのでrewriteを使って"/"へのアクセスを内部的に"/index/:number"へと振る内容を書いた。

// next.config.js

module.exports = {

	async rewrites() {

		return [

			{

				source: "/",

				destination: "/index",

			},

			{

				source: "/index/:slug",

				destination: "/index",

			},

		]

	},

}

ところがどっこい、"pages/index.js"を無効化しないとリライトは効かないのである。SSGでpages内に対象ファイルがなかった場合にのみハンドリングされるのではと勘ぐらずにはいられない。

不完全すぎる。

エラーハンドリング

SSRでエラーレスポンス(404)を返すには"return {notFound: true}"を返すだけでいい。だが、これだけではそれ以外の406やら503やらを返すことができない。そこで登場するのが"pages/404.js"、"pages/_error.js"、"next/error"である。

"pages/404.js"は"return {notFound: true}"でのみ使われるので"getServerSideProps(context)"内で"context.res.statusCode = 404"にしても呼び出されない。結局コンポーネント内でステータスコードで振り分ける必要があるのでカスタマイズされたエラーページを提供するのであれば"return {notFound: true}"を使う必要はない。ちなみに"pages/404.jsが"設置されていなければ"pages/_error.js"、"next/error"の優先度で対応される。

// 200以外のステータスコードをハンドリングするなら各コンポーネント内でエラーオブジェクトを呼び出す必要がある

import Error from "next/error"

const page = ({statusCode}) => {

	if(errorCode != 200){

		return (<Error code={statusCode} />)

	}

	return(

		<>

			 /*** some contents ***/

		</>

	)

}

export default page



export const getServerSideProps = async ({req, res}) => {

	res.statusCode = 406

	return {

		props: {

			statusCode: res.statusCode,

		},

	}

}

独自のエラー画面"pages/_error.js"を使う場合は下記になる。

import Error from "next/error"

		↓

import Error from "./path/to/_error.js"

独自のエラー画面を使う際に"pages/404.js"がなければ下記のメッセージが表示される。

Warning: You have added a custom /_error page without a custom /404 page. This prevents the 404 page from being auto statically optimized.  

See here for info: https://err.sh/next.js/custom-error-no-custom-404 

非常にめんどくさいので"pages/404.js"は下記のように独自のエラー画面かデフォルトのエラー画面のうち使いたいものを指定してやればいい。

import Err from 'next/error';

// import Err from './path/to/_error.js';

const page = () => {

	return <Err statusCode="404"/>

}

export default page

不完全すぎる。

htmlの書き出し

他コンテンツからhtml等のデータファイルを引き継ぎ、そのデータをコンポーネントに流し込むパターンを想定する。とりあえずデータ自体を変数に納めることは楽にできるが、htmlを含んだプロパティをコンポーネントに流し込むには一癖が必要となる。

const page = ({article}) => {

	const text = `

	<header>

		<h1>${article.title}</h1>

	</header>

	${article.text}

`

	return(<article  dangerouslySetInnerHTML={{__html: text}} />)

}

"article.title"はプレーンな文字列、"article.text"はhtmlを含む文字列としよう。htmlを含む文字列をエレメントに流し込むには"dangerouslySetInnerHTML"というプロパティが必要となる。これを安直にdivでやってしまうと無駄なdiv打ちが発生するのでせっかくのマークアップが台無しにになり、無駄なcssコードも必要となってくる。それを解消するにはヒアドキュメントで子要素全てを変数に収めてから親要素に流し込む必要がある。

不完全すぎる。

import(...)の罠

この問題はnext.jsが悪い訳ではなくJSの仕様のせいで頓挫した。流れ的にはMVCフレームワークのごとくindex.jsに全てのアクセスを集めてその中で各コンポーネントを呼び出すという悪魔的な改造をしようとしたのが原因。やるだけ時間の無駄なので真似しないように

// next.config.js

module.exports = {

	async rewrites() {

		return [

			{

				source: "/:path*",

				destination: "/",

			},

		]

	},

}

index.jsのgetServerSidePropsとコンポーネントそれぞれで呼び出すコンポーネントのファイルパスの変数を共有する必要があったが、import(...)がハードコーディングでしか機能しないという落ちだった。

const path = "../component/test.js"

const hard = import("../component/test.js").catch(error => console.log(error))	//問題なし

const soft = import(path).catch(error => console.log(error))	//エラーになる

下記のようにテンプレートリテラルだと動いてしまうことを発見した。なんかよくわからん。

const dynamicImport = path => {

	return dynamic(import(`../component/${path}`))

}



const page = ({prop}) => {

	const path = "test2.js"

	const Component = dynamicImport(path)

	return(<Component />)

}

JS不完全すぎる。

結論

SSRで縛ると他要素に振り回されるので実用的とは言い難い。フロントエンドに毛が生えた程度という表現がしっくりくる。

実に不完全すぎる。