SSR非対応コンポーネントを next/dynamic でラップしてHydration errorを回避する

2023/09/13に公開
1

SSRでレンダリングすると問題が出るコンポーネントを、SSR時にはレンダリング対象から外したいというときに使える Tips

TL;DR

以下のように dynamic(..., { ssr: false }) でラップしたコンポーネントは Server-side でレンダリングされず、Client-side でのみレンダリングされる(はず)

export const PurchaseButton = dynamic(
  Promise.resolve((props: Props) => {
    // コンポーネントの実装
  }),
  { ssr: false }
)

問題

SSR されるページで利用しているコンポーネントが以下のような実装されていると、Server-side のレンダリング結果と Client-side でのレンダリングに齟齬が発生してしまい、React の Hydration フェーズで以下のようなエラーが発生してしまう。

Unhandled Runtime Error
Error: XXXXX content does not match server-rendered HTML.

See more info here: https://nextjs.org/docs/messages/react-hydration-error

この原因と考えられるのが以下のようなケース

  • window の有無でレンダリング内容を変えている
  • 現在日時などレンダリングのタイミングで表示内容が変わる

など詳細は以下を参照

https://nextjs.org/docs/messages/react-hydration-error

コンポーネント利用側での対応方法

このようなケースに対応する方法として、対象コンポーネントを Server-side でレンダリング対象から外すという方法がある。

Solution 2: Disabling SSR on specific components

これには next/dynamic を利用するが、公式ドキュメントではファイル分割を意識して(?)動的インポートと組み合わせた例となっている。

https://nextjs.org/docs/pages/building-your-application/optimizing/lazy-loading

const DynamicComponent = dynamic(() =>
  import('../components/hello').then((mod) => mod.Hello)
)

そうすると、コンポーネントの利用する側でnext/dynamicを意識して利用する必要がある。
利用する場所で SSR の有効・無効を切り替えたい場合はそれで良いが、該当コンポーネントはどこで利用してもSSR非対応なので、該当コンポーネント側で対応を完結したい。 ( ファイル分割はどうでもよくて「SSR対象から除外する」という挙動だけを適用したい )

対象コンポーネントで完結する対応方法

以下のように対象コンポーネントを dynamic(...) でまるごとラップすることで、そのコンポーネントを利用する側で意識する必要がなくなる。
注意すべきは dynamic() が受け取る第1引数は Promise<...> なので、Promise.resolve() でコンポーネントラップしたものを渡すことになる点。

export const PurchaseButton = dynamic(
  Promise.resolve((props: Props) => {
    // コンポーネントの実装
  }),
  { ssr: false }
)

さいごに

next/dynamic の想定しているユースケースが、「ファイル分割」と「SSR対象からの除外」の2通りあるように見えるが、公式ドキュメントの例では「ファイル分割」を前提とした例のみだったので「SSR対象からの除外」のみを対象とした利用方法を記事として公開してみました。

この記事の記載内容に誤り等あればご指摘いただけると助かります。

Discussion

snakasnaka

ドキュメントが探せないが noSSR というものがこの用途で利用できるかも?

export declare function noSSR<P = {}>(LoadableInitializer: LoadableFn<P>, loadableOptions: DynamicOptions<P>): React.ComponentType<P>;