はじめに
この記事の内容は、以下のサイトと同じことをやってます
この記事は自分用のメモです
https://colinhacks.com/essays/building-a-spa-with-nextjs
https://blog.hey3.dev/posts/nextjs-spa
概要
Next.js アプリを静的ファイルにビルドし、S3 にデプロイ、CloudFront で公開していた
しかし、動的ルーティングを使うことになり、これ自体は Next.js がサポートしているので問題ないが、/task/[id].html
に対して URL から直接アクセスが来た場合に 403 エラーとなってしまう
単純にルートの/index.html
に飛ばすだけで良いなら、CloudFront のカスタムエラーページの設定で対応できるが、今回はシステムの仕様上、それはできず、/task/[id].html
の内容を表示させる必要がある
今回やりたいことは以下である
- システムは静的サイトホスティングする
- サーバー上でシステムは動かさない
- ルートの JS でルーティングを実装する
- Next.js のルーティングを利用しない
- URL 直接入力の場合、必ずルートの JS を経由させ、ルーティングを通るようにする
Next.js はファイルシステムに沿ったルーティングを持つ(pages ディレクトリにファイルを配置すると自動でルーティングされる)
しかし、サーバー上で動いていない Next.js では動的ルーティング + URL 直接入力
に対応できない
こちらはreact-router
で対応する
そうなると、create-react-app
で良くて、Next.js 使わなくてもいいんじゃないって感じだけど、バンドルの分割機能やページ単位の SSR など Next.js 機能の恩恵もあるのと、途中で Next.js を使うのやめるのちょっとしんどいので、Next.js の利用を継続する
Nuxt.js であれば、mode: spa
やssr: false
で対応できるのかもしれない
react-router
の実装
npm i react-router-dom
ルートの JS でルーティングを実装する
// pages/index.tsx import { BrowserRouter, Route, Switch } from 'react-router-dom' const App: React.VFC = () => ( <BrowserRouter> <Switch> <Route path="/sign-in" component={SignIn} exact /> <Route path="/" component={Home} exact /> <Route path="/task/:id" component={Task} exact /> </Switch> </BrowserRouter> )
エラーの対応
ルーティング実装後、サーバー上で動かすと以下のエラーが出る
Error: Invariant failed: Browser history needs a DOM
また、静的ビルドの場合は、以下のエラーになる
Error occurred prerendering page "/". Read more: https://err.sh/next.js/prerender-error
これはreact-router
がグルーバルオブジェクトの window を参照しているが、Node.js 上では window は未定義なので、エラーになっている
よって、react-router
を使うには Node.js のレンダリングを廃止する必要がある
// pages/_app.tsx const App = ({ Component, pageProps }: AppProps): JSX.Element => ( <div suppressHydrationWarning={process.env.NODE_ENV === 'development'}> {typeof window === 'undefined' ? null : <Component {...pageProps} />} </div> )
window が undefined の場合、null を返して、レンダリングさせないようにしている
また、上記を行ったことでクライアントとサーバーでレンダリング結果が異なるため、以下のエラーが出る
Warning: Expected server HTML to contain a matching <div> in <div>.
これはsuppressHydrationWarning
を指定することで警告を消せる
URL 直接入力の対応
今の状態だと URL 直接入力で画面表示を行う場合、react-router
を通さないと 403/404 エラーになる
たとえば、/task/1
でリロードすると、Next.js は/task/1.html
を探しにいくが、これはないので 403/404 エラーを返す
以下の rewrites 設定を行うことで、すべてのパスをルート(ルーティング書いた場所)に書き換えることができる
// next.config.js module.exports = { async rewrites() { return [{ source: '/:path*', destination: '/' }] } }
ただし、rewrites はサーバー上でのみ機能するため、静的サイトホスティングしている場合は機能しない
CloudFront の場合の対応としては、カスタムエラーレスポンスの設定で 403 エラーが起きた場合、レスポンスコードを 200 に変更し、レスポンスページをルート(/
)にすればいける
さいごに
サーバーレンダリングを抑制したり、next/router
とreact-router
が共存したりして、やってることは微妙かもしれないが、Next.js を利用しつつ、動的ルーティング + URL 直接入力
に対応するにはおそらく 1 番簡単なやり方だと思う
react-router
を使わないやり方だと、Lambda@Edge による URL 書き換えや静的サイトホスティングをやめて、ECS 等で Next.js を動かすやり方があると思うが、Lambda の場合は複雑化したり、ECS の場合は途中から切り替えるのはしんどかったりするかなと思う