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

Nov 25 2022
フロントエンド WAGMI ウォレット接続を使用して NextJS 13 の Hydration エラーを修正する方法
問題 最近、新しいベータ版 NextJS アプリケーションで WAGMI をインストールし、基本的なウォレット接続を試みた場合、Hydration エラーを示すエラーに遭遇した可能性があります。これは NextJS 13 だけの問題ではありませんが、NextJS 13 がこれを特定して解決するためにいくつかの規則を変更する方法について説明します。
NextJS 13 ハイドレーション エラー

問題

最近、新しいベータ版 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 の違いを確認してください。

左: JavaScript を無効化 — 右: JavaScript を有効化

水分補給について詳しく知りたい場合は、Josh Comeau によるThe Perils of Rehydrationに関するこのブログ記事を参照することをお勧めします。

解決策は何ですか?

解決策は、サーバーで処理する必要があるものとクライアント側で処理する必要があるものを分ける必要があるということです。NextJS 13 に対するいくつかの新しい調整により、一部のドキュメントでは、server と client 間のファイルの明確な分離が示されています。これは単なるアイデアですが、ソリューションを解決する方法を示して示すことができるものです。

要件

開始する前に、次の手順を実行するために、コンピューターに次のものがインストールされていることを確認してください。

  • NVM またはノード v18.12.1
  • pnpm v7.15.0

上に示した問題を再現し、それを修正する方法に関するいくつかの可能な解決策について説明します。

NextJS 13 ハイドレーション エラー

エラーを再現するための初期設定を取得しましょう。

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>
    );
};

http://localhost:300 シンプルな UI

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>
    );
};

Tailwind を使用した NextJS

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 番目は私たちの ですが、これは一時的なものであり、その理由を説明します。useStateuseEffectpage.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>
    );
};

NextJS ウォレット接続 — 未接続

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

NextJS ウォレット接続 — 接続済み

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

NextJS ウォレット接続 — ハイドレーション エラー

すごい!これで問題が解決しました。解決策に移りましょう。

ソリューション

物事をもう少しうまく整理する方法についていくつかのアイデアを説明し、最適化された方法でいくつかの解決策を示します。

まず、クライアントに必要なものとサーバーに必要なものを分けましょう。を見てみると、本当にクライアントが処理する必要があるのは変数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>
    );
};

NextJS ウォレット接続 — JavaScript 無効

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>
    );
};

NextJS ウォレット接続 — ハイドレーション エラー

ソリューション クレジット

解決策は Josh Comeau がThe Perils of Rehydration のブログで独自に作成したものであることに注意してください。ジョシュに感謝します。

最初の解決策

コンポーネントが最初にマウントされているかどうかを確認し、マウントされていない場合はコンポーネントをロードしない最初のソリューションに取り組みましょう。

これを追跡するには、useStateと一緒にできますuseEffectuseRef残念ながら、コンポーネントがマウントされているかどうかを追跡するために使用することはできません。

ファイル: ./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>
    );
};

NextJS ウォレット接続 — 解決策 1 ハイドレーション エラーの修正

最適化されたソリューション

ただし、すべてのコンポーネントに追加するのは少し繰り返しになる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 ) もフォローしてください。