Web3 ウォレット接続を使用した NextJS 13 の Hydration エラーについて

問題
最近、新しいベータ版 NextJS アプリケーションでWAGMIをインストールし、基本的なウォレット接続を試みた場合、Hydration エラーを示すエラーに遭遇した可能性があります。これは NextJS 13 だけの問題ではありませんが、NextJS 13 がこれを特定して解決するためにいくつかの規則を変更する方法について説明します。
ハイドレーションとは?
ハイドレーションは、クライアント側の JavaScript を使用して、サーバーでレンダリングされた HTML にアプリケーションの状態と対話性を追加するプロセスです。これは、Gatsby フレームワークを作成する基本ツールの 1 つである React の機能です。Gatsby はハイドレーションを使用して、ビルド時に作成された静的 HTML を React アプリケーションに変換します。
- React Hydration を理解する
何が起こっている?
問題は、NextJS のような SSR (Server-Side Rendered) React フレームワークを使用している場合、技術的にはページを特定の方法でレンダリングし、クライアント (ブラウザー) がレンダリングするときに、レンダリングされた状態が期待されることです。サーバーによって、クライアント側にあるものと一致して、その状態を管理する方法を確実に認識します。
サーバー側の状態とクライアントの状態が一致しない場合、ハイドレーション エラーが発生します。
これを確認するには、ブラウザで JavaScrip を無効にして、2 つの DOM の違いを確認してください。

水分補給について詳しく知りたい場合は、Josh Comeau によるThe Perils of Rehydrationに関するこのブログ記事を参照することをお勧めします。
解決策は何ですか?
解決策は、サーバーで処理する必要があるものとクライアント側で処理する必要があるものを分ける必要があるということです。NextJS 13 に対するいくつかの新しい調整により、一部のドキュメントでは、server と client 間のファイルの明確な分離が示されています。これは単なるアイデアですが、ソリューションを解決する方法を示して示すことができるものです。
要件
開始する前に、次の手順を実行するために、コンピューターに次のものがインストールされていることを確認してください。
- NVM またはノード v18.12.1
- pnpm v7.15.0
上に示した問題を再現し、それを修正する方法に関するいくつかの可能な解決策について説明します。

エラーを再現するための初期設定を取得しましょう。
pnpm create next-app --typescript next13-wagmi-hydration;
# Expected Prompts:
#? Would you like to use ESLint with this project? › No / Yes
# Creating a new Next.js app in /path/to/next13-wagmi-hydration.
#
# Using pnpm.
#
# Installing dependencies:
# - react
# - react-dom
# - next
# - typescript
# - @types/react
# - @types/node
# - @types/react-dom
# - eslint
# - eslint-config-next
pnpm add wagmi ethers;
新しいBeta NextJS 13 Docsのいくつかを使用して、NextJS アプリを構成します。
ファイル: ./next13-wagmi-hydration/next.config.js
/** @type {import('next').NextConfig} */
const nextConfig = {
reactStrictMode: true,
swcMinify: true,
experimental: {
appDir: true,
},
};
module.exports = nextConfig;
# FROM ./next13-wagmi-hydration
mkdir ./app;
# FROM ./next13-wagmi-hydration
mv ./pages/index.tsx app/page.tsx
rm -rf pages;
# FROM ./next13-wagmi-hydration
touch ./pages/layout.text
// Imports
// ========================================================
import '../styles/globals.css';
// Layout
// ========================================================
export default function RootLayout({
children,
}: {
children: React.ReactNode
}) {
return (
<html>
<head />
<body>
{children}
</body>
</html>
)
};
ファイル: ./next13-wagmi-hyration/app/page.tsx
// Imports
// ========================================================
import React from 'react';
// Page
// ========================================================
export default function Home() {
// Render
return (
<div>
<h1 className="text-2xl text-white font-medium mb-6">Wallet Connection</h1>
</div>
);
};

Tailwind 構成 (オプション)
この次のステップはオプションですが、UI のデモを行うときに状況が良くなったときに気に入っています。そのためにTailwindを使用します。
# FROM ./next13-wagmi-hydration
pnpm add -D tailwindcss postcss autoprefixer;
pnpx tailwindcss init -p;
ファイル: ./next13-wagmi-hyration/tailwind.config.js
/** @type {import('tailwindcss').Config} */
module.exports = {
content: [
"./app/**/*.{js,ts,jsx,tsx}",
"./components/**/*.{js,ts,jsx,tsx}",
],
theme: {
extend: {},
},
plugins: [],
}
ファイル: ./next13-wagmi-hyration/styles/global.css
@tailwind base;
@tailwind components;
@tailwind utilities;
ファイル: ./next13-wagmi-hyration/app/layout.tsx
// Imports
// ========================================================
import '../styles/globals.css';
// Layout
// ========================================================
export default function RootLayout({
children,
}: {
children: React.ReactNode
}) {
return (
<html>
<head />
<body className="bg-zinc-900">
{children}
</body>
</html>
)
};
// Imports
// ========================================================
import React from 'react';
// Page
// ========================================================
export default function Home() {
// Render
return (
<div className="p-8">
<h1 className="text-2xl text-white font-medium mb-6">Wallet Connection</h1>
</div>
);
};

WAGMI 構成
次に、WAGMI をセットアップして、ウォレットとのやり取りを可能にします。
# FROM ./next13-wagmi-hydration
pnpm add wagmi ethers;
# FROM ./next13-wagmi-hydration
mkdir ./providers;
mkdir ./providers/wagmi;
touch ./providers/wagmi/index.tsx;
touch ./app/providers.tsx;
ファイル: ./next13-wagmi-hyration/providers/wagmi/index.tsx
// Imports
// ========================================================
import React from 'react';
import { WagmiConfig, createClient } from "wagmi";
import { getDefaultProvider } from 'ethers';
// Config
// ========================================================
const client = createClient({
autoConnect: true,
provider: getDefaultProvider()
});
// Provider
// ========================================================
const WagmiProvider = ({ children }: { children: React.ReactNode }) => {
return <WagmiConfig client={client}>{children}</WagmiConfig>
};
// Exports
// ========================================================
export default WagmiProvider;
ファイル: ./next13-wagmi-hyration/app/providers.tsx
// Imports
// ========================================================
import React from 'react';
import WagmiProvider from "../providers/wagmi";
// Root Provider
// ========================================================
const RootProvider = ({ children }: { children: React.ReactNode }) => {
return <div>
<WagmiProvider>
{children}
</WagmiProvider>
</div>
};
// Exports
// ========================================================
export default RootProvider;
ファイル: ./next13-wagmi-hyration/app/layout.tsx
// Imports
// ========================================================
import RootProvider from "./providers";
import '../styles/globals.css';
// Layout
// ========================================================
export default function RootLayout({
children,
}: {
children: React.ReactNode
}) {
return (
<html>
<head />
<body className="bg-zinc-900">
<RootProvider>
{children}
</RootProvider>
</body>
</html>
)
};
ファイル: ./next13-wagmi-hyration/app/page.tsx
// Imports
// ========================================================
import React from 'react';
import { useAccount, useConnect, useDisconnect } from "wagmi";
import { InjectedConnector } from "wagmi/connectors/injected";
// Page
// ========================================================
export default function Home() {
// State / Props
const { address, isConnected } = useAccount();
const { connect } = useConnect({
connector: new InjectedConnector(),
});
const { disconnect } = useDisconnect()
// Render
return (
<div className="p-8">
<h1 className="text-2xl text-white font-medium mb-6">Wallet Connection</h1>
{!isConnected
? <div>
<button className="h-10 bg-blue-600 text-white px-6 rounded-full hover:bg-blue-800 transition-colors ease-in-out duration-200" onClick={() => connect()}>Connect Wallet</button>
</div>
: <div>
<label className="text-zinc-400 block mb-2">Wallet Address Connected</label>
<code className="bg-zinc-700 text-zinc-200 p-4 rounded block mb-4"><pre>{address}</pre></code>
<button className="h-10 bg-red-600 text-white px-6 rounded-full hover:bg-red-800 transition-colors ease-in-out duration-200" onClick={() => disconnect()}>Disconnect Wallet</button>
</div>}
</div>
);
};
# when running pnpm run dev
wait - compiling...
error - ./node_modules/.pnpm/@[email protected]_fsy4krnncv4idvr4txy3aqiuqm/node_modules/@tanstack/react-query-persist-client/build/lib/PersistQueryClientProvider.mjs
Attempted import error: 'useState' is not exported from 'react' (imported as 'React').
NextJS Migrating Pagesでベータ版のドキュメントを確認してください。
これを修正するには、クライアントに対して 2 つのファイルをどのように処理するかを明示する必要がありますuse client;
。
これが必要な場所が 2 つあります。1 つ目は、ほとんどのプロバイダーがクライアント側で主に使用されるおよびprovider.tsx
のようなフックを利用することを知っているためです。2 番目は私たちの ですが、これは一時的なものであり、その理由を説明します。useState
useEffect
page.tsx
ファイル: ./next13-wagmi-hyration/app/providers.tsx
'use client';
// Imports
// ========================================================
import React from 'react';
import WagmiProvider from "../providers/wagmi";
// Root Provider
// ========================================================
const RootProvider = ({ children }: { children: React.ReactNode }) => {
return <div>
<WagmiProvider>
{children}
</WagmiProvider>
</div>
};
// Exports
// ========================================================
export default RootProvider;
'use client';
// Imports
// ========================================================
import React from 'react';
import { useAccount, useConnect, useDisconnect } from "wagmi";
import { InjectedConnector } from "wagmi/connectors/injected";
// Page
// ========================================================
export default function Home() {
// State / Props
const { address, isConnected } = useAccount();
const { connect } = useConnect({
connector: new InjectedConnector(),
});
const { disconnect } = useDisconnect()
// Render
return (
<div className="p-8">
<h1 className="text-2xl text-white font-medium mb-6">Wallet Connection</h1>
{!isConnected
? <div>
<button className="h-10 bg-blue-600 text-white px-6 rounded-full hover:bg-blue-800 transition-colors ease-in-out duration-200" onClick={() => connect()}>Connect Wallet</button>
</div>
: <div>
<label className="text-zinc-400 block mb-2">Wallet Address Connected</label>
<code className="bg-zinc-700 text-zinc-200 p-4 rounded block mb-4"><pre>{address}</pre></code>
<button className="h-10 bg-red-600 text-white px-6 rounded-full hover:bg-red-800 transition-colors ease-in-out duration-200" onClick={() => disconnect()}>Disconnect Wallet</button>
</div>}
</div>
);
};

サイトを接続しようとしても、問題は発生しません。

最後に、ページを更新してハイドレーション エラーを確認しましょう。

すごい!これで問題が解決しました。解決策に移りましょう。
ソリューション
物事をもう少しうまく整理する方法についていくつかのアイデアを説明し、最適化された方法でいくつかの解決策を示します。
まず、クライアントに必要なものとサーバーに必要なものを分けましょう。を見てみると、本当にクライアントが処理する必要があるのは変数page.tsx
だけであることがわかります。isConnected
それ以外はすべてサーバーでレンダリングできます。
page.tsx
リファクタリングしてウォレットのやり取りを削除use client
し、独自のコンポーネントに抽象化しましょう。
ファイル: ./next13-wagmi-hyration/app/page.tsx
// Imports
// ========================================================
import React from 'react';
import ConnectWallet from './wallet';
// Page
// ========================================================
export default function Home() {
// Render
return (
<div className="p-8">
<h1 className="text-2xl text-white font-medium mb-6">Wallet Connection</h1>
<ConnectWallet />
</div>
);
};

JavaScript を再び有効にして、新しいConnectWallet
コンポーネントを作成しましょう。
# FROM ./next13-wagmi-hydration
touch ./app/wallet.tsx
ファイル: ./next13-wagmi-hyration/app/wallet.tsx
'use client';
// Imports
// ========================================================
import React from 'react';
import { useAccount, useConnect, useDisconnect } from "wagmi";
import { InjectedConnector } from "wagmi/connectors/injected";
// Page
// ========================================================
export default function Home() {
// State / Props
const { address, isConnected } = useAccount();
const { connect } = useConnect({
connector: new InjectedConnector(),
});
const { disconnect } = useDisconnect()
// Render
return (
<div>
{!isConnected
? <div>
<button className="h-10 bg-blue-600 text-white px-6 rounded-full hover:bg-blue-800 transition-colors ease-in-out duration-200" onClick={() => connect()}>Connect Wallet</button>
</div>
: <div>
<label className="text-zinc-400 block mb-2">Wallet Address Connected</label>
<code className="bg-zinc-700 text-zinc-200 p-4 rounded block mb-4"><pre>{address}</pre></code>
<button className="h-10 bg-red-600 text-white px-6 rounded-full hover:bg-red-800 transition-colors ease-in-out duration-200" onClick={() => disconnect()}>Connect Wallet</button>
</div>}
</div>
);
};

ソリューション クレジット
解決策は Josh Comeau がThe Perils of Rehydration のブログで独自に作成したものであることに注意してください。ジョシュに感謝します。
最初の解決策
コンポーネントが最初にマウントされているかどうかを確認し、マウントされていない場合はコンポーネントをロードしない最初のソリューションに取り組みましょう。
これを追跡するには、useState
と一緒にできますuseEffect
。useRef
残念ながら、コンポーネントがマウントされているかどうかを追跡するために使用することはできません。
ファイル: ./next13-wagmi-hyration/app/wallet.tsx
'use client';
// Imports
// ========================================================
import React, { useState, useEffect } from 'react';
import { useAccount, useConnect, useDisconnect } from "wagmi";
import { InjectedConnector } from "wagmi/connectors/injected";
// Page
// ========================================================
export default function Wallet() {
// State / Props
const [hasMounted, setHasMounted] = useState(false);
const { address, isConnected } = useAccount();
const { connect } = useConnect({
connector: new InjectedConnector(),
});
const { disconnect } = useDisconnect()
// Hooks
useEffect(() => {
setHasMounted(true);
}, [])
// Render
if (!hasMounted) return null;
return (
<div>
{!isConnected
? <div>
<button className="h-10 bg-blue-600 text-white px-6 rounded-full hover:bg-blue-800 transition-colors ease-in-out duration-200" onClick={() => connect()}>Connect Wallet</button>
</div>
: <div>
<label className="text-zinc-400 block mb-2">Wallet Address Connected</label>
<code className="bg-zinc-700 text-zinc-200 p-4 rounded block mb-4"><pre>{address}</pre></code>
<button className="h-10 bg-red-600 text-white px-6 rounded-full hover:bg-red-800 transition-colors ease-in-out duration-200" onClick={() => disconnect()}>Connect Wallet</button>
</div>}
</div>
);
};

最適化されたソリューション
ただし、すべてのコンポーネントに追加するのは少し繰り返しになるhasMounted
ので、その機能を独自のコンポーネントに抽象化することで、さらに一歩進めることができます。
# FROM ./next13-wagmi-hydration
touch ./app/clientOnly.tsx;
'use client';
// Imports
// ========================================================
import React, { useState, useEffect } from 'react';
// Page
// ========================================================
export default function ClientOnly({ children }: { children: React.ReactNode }) {
// State / Props
const [hasMounted, setHasMounted] = useState(false);
// Hooks
useEffect(() => {
setHasMounted(true);
}, [])
// Render
if (!hasMounted) return null;
return (
<div>
{children}
</div>
);
};
ファイル: ./next13-wagmi-hyration/app/wallet.tsx
'use client';
// Imports
// ========================================================
import React from 'react';
import ClientOnly from './clientOnly';
import { useAccount, useConnect, useDisconnect } from "wagmi";
import { InjectedConnector } from "wagmi/connectors/injected";
// Page
// ========================================================
export default function Wallet() {
// State / Props
const { address, isConnected } = useAccount();
const { connect } = useConnect({
connector: new InjectedConnector(),
});
const { disconnect } = useDisconnect()
// Render
return (
<div>
<ClientOnly>
{!isConnected
? <div>
<button className="h-10 bg-blue-600 text-white px-6 rounded-full hover:bg-blue-800 transition-colors ease-in-out duration-200" onClick={() => connect()}>Connect Wallet</button>
</div>
: <div>
<label className="text-zinc-400 block mb-2">Wallet Address Connected</label>
<code className="bg-zinc-700 text-zinc-200 p-4 rounded block mb-4"><pre>{address}</pre></code>
<button className="h-10 bg-red-600 text-white px-6 rounded-full hover:bg-red-800 transition-colors ease-in-out duration-200" onClick={() => disconnect()}>Connect Wallet</button>
</div>}
</ClientOnly>
</div>
);
};
NextJS Wallet Connection — Optimized Solution For Hydration Error Fixed
完全なコードの動作を確認するには、この github リポジトリをチェックしてください。
次は何ですか?
NextJS と連動する Sign-In With Ethereum の初期実装に関する別の記事をご覧ください。
このことから価値が得られた場合は、Twitter (私は非常に活発に活動しています) @codingwithmannyと instagram ( @codingwithmanny ) もフォローしてください。
