前提
以下2つの記事を参考にNext.jsにてSuspenseの挙動を試していた際にエラーが発生したので、備忘録として記事を記載します。
- Suspenseの挙動を観察しよう|ReactのSuspense対応非同期処理を手書きするハンズオン
- Next.js でConcurrent Rendering(Suspense)を試してみた | DevelopersIO
環境
- 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など
ソース内容
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
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
にて、以下の通り修正を実施しました。
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メッセージが表示されるようです。しかし、私のプロジェクトでは表示されなかったため、当対応は実施しませんでした。
対応② コンポーネントをクライアントのみでレンダリングする
当記事で実施した対応内容です。
最後に
対象のコンポーネントをクライアントでのみレンダリングするよう対応することで、当エラーを回避しました。
しかし、非同期処理を行うすべてのコンポーネントに、当対応を実施することは、冗長なように感じます。
もっとスマートな対応があればご教授いただきたく思います。
閲覧いただきありがとうございました。