ทำความเข้าใจเกี่ยวกับข้อผิดพลาดในการไฮเดรชันใน NextJS 13 ด้วยการเชื่อมต่อ Web3 Wallet

Nov 25 2022
วิธีแก้ไขข้อผิดพลาดในการไฮเดรชันใน NextJS 13 ด้วยการเชื่อมต่อกระเป๋าเงิน WAGMI ส่วนหน้า
ปัญหา หากคุณเพิ่งติดตั้ง WAGMI ด้วยแอปพลิเคชัน NextJS รุ่นเบต้าใหม่ของคุณ และพยายามเชื่อมต่อกระเป๋าเงินพื้นฐาน คุณอาจพบข้อผิดพลาดที่แสดงข้อผิดพลาด Hydration โปรดทราบว่านี่ไม่ได้เป็นเพียงปัญหาของ NextJS 13 แต่เราจะอธิบายว่า NextJS 13 เปลี่ยนแปลงข้อตกลงบางอย่างเพื่อช่วยแยกและแก้ไขปัญหานี้อย่างไร
ถัดไปJS 13 ข้อผิดพลาดในการไฮเดรชัน

ปัญหา

หากคุณเพิ่งติดตั้งWAGMIด้วยแอปพลิเคชัน NextJS รุ่นเบต้าใหม่ของคุณ และพยายามเชื่อมต่อกระเป๋าเงินพื้นฐาน คุณอาจพบข้อผิดพลาดที่แสดงข้อผิดพลาด Hydration โปรดทราบว่านี่ไม่ได้เป็นเพียงปัญหาของ NextJS 13 แต่เราจะอธิบายว่า NextJS 13 เปลี่ยนแปลงข้อตกลงบางอย่างเพื่อช่วยแยกและแก้ไขปัญหานี้อย่างไร

ไฮเดรชั่นคืออะไร?

ไฮเดรชันเป็นกระบวนการของการใช้ JavaScript ฝั่งไคลเอ็นต์เพื่อเพิ่มสถานะแอปพลิเคชันและการโต้ตอบให้กับ HTML ที่แสดงผลโดยเซิร์ฟเวอร์ เป็นคุณลักษณะของ React ซึ่งเป็นหนึ่งในเครื่องมือพื้นฐานที่สร้างเฟรมเวิร์ก Gatsby Gatsby ใช้ไฮเดรชั่นเพื่อแปลง HTML แบบสแตติกที่สร้างขึ้น ณ เวลาบิลด์เป็นแอปพลิเคชัน React
- ทำความเข้าใจปฏิกิริยาไฮเดรชั่น

เกิดอะไรขึ้น?

ปัญหาคือว่าเมื่อเราใช้ SSR (ฝั่งเซิร์ฟเวอร์ Rendered) React Frameworks เช่น NextJS ในทางเทคนิคแล้ว จะแสดงผลหน้าเว็บด้วยวิธีเฉพาะ จากนั้นเมื่อไคลเอ็นต์ (เบราว์เซอร์) แสดงผลสิ่งต่างๆ ก็คาดหวังว่าสถานะจะแสดงผล โดยเซิร์ฟเวอร์จะจับคู่สิ่งที่อยู่ในฝั่งไคลเอ็นต์เพื่อให้แน่ใจว่าทราบวิธีจัดการสถานะของตน

หากสถานะฝั่งเซิร์ฟเวอร์และสถานะไคลเอ็นต์ไม่ตรงกัน คุณจะได้รับข้อผิดพลาดในการไฮเดรชัน

วิธีที่ดีในการดูสิ่งนี้คือถ้าคุณปิดการใช้งาน JavaScrip บนเบราว์เซอร์ของคุณและเห็นความแตกต่างระหว่าง DOM ทั้งสอง

ซ้าย: JavaScript ที่ปิดใช้งาน — ขวา: JavaScript ที่เปิดใช้งาน

หากคุณต้องการเรียนรู้เพิ่มเติมเกี่ยวกับการให้ความชุ่มชื้น ฉันขอแนะนำให้ดูโพสต์บล็อกนี้โดย Josh Comeau เกี่ยวกับอันตรายของการขาดน้ำ

ทางออกคืออะไร?

วิธีแก้ไขคือเราต้องแยกแยะว่าอะไรควรจัดการโดยเซิร์ฟเวอร์และอะไรควรจัดการในฝั่งไคลเอนต์ ด้วยการปรับเปลี่ยนใหม่บางส่วนสำหรับ NextJS 13 ในเอกสารประกอบบางรายการ จะแสดงการแยกไฟล์ที่ชัดเจนระหว่างเซิร์ฟเวอร์และไคลเอนต์ แม้ว่าจะเป็นเพียงแนวคิด แต่เป็นสิ่งที่เราสามารถแสดงให้เห็นในการแก้ปัญหาได้

ความต้องการ

ก่อนที่เราจะเริ่ม ตรวจสอบให้แน่ใจว่าได้ติดตั้งสิ่งต่อไปนี้บนคอมพิวเตอร์ของคุณเพื่อทำตามขั้นตอนต่อไป

  • NVM หรือโหนด v18.12.1
  • pnpm v7.15.0

เราจะสร้างปัญหาที่แสดงด้านบนขึ้นมาใหม่ จากนั้นแนะนำวิธีแก้ไขที่เป็นไปได้บางส่วนเกี่ยวกับวิธีแก้ไข

ถัดไปJS 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>
    );
};

NextJS กับ Tailwind

การกำหนดค่า 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

ในการแก้ไขปัญหานี้ เราจำเป็นต้องระบุวิธีจัดการไฟล์สองไฟล์สำหรับลูกค้าอย่างชัดเจนโดยใช้ความคิดเห็นที่ด้านบนสุดของแต่ละไฟล์ด้วยuse client;.

มีสองแห่งที่เราต้องการสิ่งนี้ อย่างแรกคือของเราprovider.tsxเพราะเรารู้ว่าผู้ให้บริการส่วนใหญ่จะใช้ประโยชน์จาก hooks 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>
    );
};

NextJS Wallet Connection — ไม่ได้เชื่อมต่อ

ตอนนี้ถ้าเราลองเชื่อมต่อกับไซต์ เราก็ยังไม่ควรพบปัญหา

NextJS Wallet Connection — เชื่อมต่อแล้ว

สุดท้าย รีเฟรชหน้านี้และดูข้อผิดพลาดเรื่องความชุ่มชื้น

NextJS Wallet Connection — ข้อผิดพลาดในการไฮเดรชัน

ยอดเยี่ยม! ตอนนี้เรามีปัญหาแล้ว มาดูแนวทางแก้ไขกัน

การแก้ไขปัญหา

ฉันจะแนะนำแนวคิดบางอย่างเกี่ยวกับวิธีจัดระเบียบสิ่งต่างๆ ให้ดียิ่งขึ้น และแสดงวิธีแก้ไขสองสามวิธีด้วยวิธีการที่เหมาะสมที่สุด

ขั้นแรก เรามาแยกสิ่งที่จำเป็นสำหรับไคลเอ็นต์และสิ่งที่จำเป็นสำหรับเซิร์ฟเวอร์ของเราออกจากกัน หากเราดู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 Wallet Connection — ปิดใช้งาน 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 Wallet Connection — ข้อผิดพลาดในการไฮเดรชัน

เครดิตโซลูชัน

ควรสังเกตว่าโซลูชันนั้นสร้างสรรค์โดย 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>
    );
};

NextJS Wallet Connection — โซลูชันที่ 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 นี้

อะไรต่อไป?

คอยติดตามบทความอื่นเกี่ยวกับการเริ่มใช้ Sign-In With Ethereum ที่ทำงานร่วมกับ NextJS ในเร็วๆ นี้

หากคุณได้รับประโยชน์จากสิ่งนี้ โปรดติดตามฉันบน Twitter (ที่ฉันใช้งานอยู่) @codingwithmannyและ instagram ที่@codingwithmanny