Next.jsでSuspense利用時に発生したError: Hydration failed because the initial UI does not match what was rendered on the server.の対処法


# 前提

以下2つの記事を参考にNext.jsにてSuspenseの挙動を試していた際にエラーが発生したので、備忘録として記事を記載します。
- [Suspenseの挙動を観察しよう｜ReactのSuspense対応非同期処理を手書きするハンズオン](https://zenn.dev/uhyo/books/react-concurrent-handson/viewer/observe-suspense)
- [Next.js でConcurrent Rendering(Suspense)を試してみた | DevelopersIO](https://dev.classmethod.jp/articles/nextjs-suspense-sample/)

## 環境

- React v18.1.0
- TypeScript v4.5.3
- Next.js v12.1.6

## ソース

### フォルダー構成

```
├─ src
|  └─ features/sample/fetch/index.tsx
│  └─ pages/sample/fetch/index.tsx
└─ package.jsonなど
```

### ソース内容

```typescript:features/sample/fetch/index.tsx
import React, { VFC, Suspense, useState, useEffect, SVGProps } from 'react'
import { Box, Typography, Stack, Button, CircularProgress } from '@mui/material'
import { Formik, Form } from 'formik'
import useSWR, { useSWRConfig, mutate } from 'swr'

function sleep(ms: number) {
  return new Promise((resolve) => setTimeout(resolve, ms))
}

const AlwaysSuspend: React.VFC = () => {
  console.log('AlwaysSuspend is rendered')

  // eslint-disable-next-line @typescript-eslint/no-throw-literal
  throw sleep(1000)
}

export const SometimesSuspend: React.VFC = () => {
  if (Math.random() < 0.5) {
    // eslint-disable-next-line @typescript-eslint/no-throw-literal
    throw sleep(1000)
  }
  return <p>Hello, world!</p>
}

export type SampleFetchProps = { image?: string }

const SampleFetch: VFC<SampleFetchProps> = (props) => {


  return (
    <Box>
      <Suspense fallback={<div>loading</div>}>
        <Typography variant="h6">Fetch</Typography>
        {/* <AlwaysSuspend /> */}
        <SometimesSuspend />
        <Formik
          initialValues={{ firstName: '', lastName: '' }}
          onSubmit={() => {
            console.log('submit')
          }}
        >
          <Form>
            <Stack spacing={2}>
              <Box p={2} display="flex" justifyContent="flex-end">
                <Button type="submit" variant="contained">
                  Get Submit
                </Button>
              </Box>
            </Stack>
          </Form>
        </Formik>
      </Suspense>
    </Box>
  )
}

export default SampleFetch
```

```typescript:pages/sample/fetch/index.tsx
import React, { VFC } from 'react'
import SampleFetch from '~/features/sample/fetch'

const NextPage: VFC = () => {
  return <SampleFetch />
}

export default NextPage
```

# エラー内容

`next dev`実行し、`http://localhost:3000/sample/fetch`へアクセスすると、以下のエラーが発生します

```:エラーメッセージ
Unhandled Runtime Error
Error: Hydration failed because the initial UI does not match what was rendered on the server.
```

# 対応

SampleFetchコンポーネントをクライアントでのみレンダリングするよう、実装しました。

## 対応詳細

`pages/sample/fetch/index.tsx`にて、以下の通り修正を実施しました。

```typescript:pages/sample/fetch/index.tsx
import React, { VFC } from 'react'
import dynamic from 'next/dynamic'

// import SampleFetch from '~/features/sample/fetch'

// 追加
const SampleFetch = dynamic(() => import('~/features/sample/fetch'), {
  ssr: false,
})

const NextPage: VFC = () => {
  return <SampleFetch />
}

export default NextPage
```

実装内容は公式ドキュメントを参考にしました。
- [Advanced Features: Dynamic Import | Next.js](https://nextjs.org/docs/advanced-features/dynamic-import#with-no-ssr)

## 調査内容

以下のGitHubのDiscussionにて、似た問題が発生していました。

- [Hydration failed because the initial UI does not match what was rendered on the server · Discussion #35773 · vercel/next.js](https://github.com/vercel/next.js/discussions/35773)

Discussionには、以下2点の対応方法が示されていました。

### 対応① HTMLを適切な構造に修正する

React（v18）では、サーバーとクライアントで出力されているコンポーネントが異なっている場合（適切なHTMLの構造ではない場合）、エラーとなるようです。（Reactのv17までは、Warningメッセージのみだったようです。）

さらに適切なHTML構造出なかった場合、その旨を示すWarningメッセージが表示されるようです。しかし、私のプロジェクトでは表示されなかったため、当対応は実施しませんでした。

- [参考Discussion](https://github.com/vercel/next.js/discussions/35773#discussioncomment-2514856)

### 対応②　コンポーネントをクライアントのみでレンダリングする

当記事で実施した対応内容です。

- [参考Discussion](https://github.com/vercel/next.js/discussions/35773#discussioncomment-2485078)

# 最後に

対象のコンポーネントをクライアントでのみレンダリングするよう対応することで、当エラーを回避しました。  
しかし、非同期処理を行うすべてのコンポーネントに、当対応を実施することは、冗長なように感じます。

もっとスマートな対応があればご教授いただきたく思います。

閲覧いただきありがとうございました。