LoginSignup
8
2

More than 1 year has passed since last update.

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

Posted at

前提

以下2つの記事を参考にNext.jsにてSuspenseの挙動を試していた際にエラーが発生したので、備忘録として記事を記載します。

環境

  • 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など

ソース内容

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
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にて、以下の通り修正を実施しました。

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

実装内容は公式ドキュメントを参考にしました。

調査内容

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

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

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

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

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

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

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

最後に

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

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

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

8
2
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
8
2