ทำความเข้าใจเกี่ยวกับข้อผิดพลาดในการไฮเดรชันใน NextJS 13 ด้วยการเชื่อมต่อ Web3 Wallet
ปัญหา
หากคุณเพิ่งติดตั้งWAGMIด้วยแอปพลิเคชัน NextJS รุ่นเบต้าใหม่ของคุณ และพยายามเชื่อมต่อกระเป๋าเงินพื้นฐาน คุณอาจพบข้อผิดพลาดที่แสดงข้อผิดพลาด Hydration โปรดทราบว่านี่ไม่ได้เป็นเพียงปัญหาของ NextJS 13 แต่เราจะอธิบายว่า NextJS 13 เปลี่ยนแปลงข้อตกลงบางอย่างเพื่อช่วยแยกและแก้ไขปัญหานี้อย่างไร
ไฮเดรชั่นคืออะไร?
ไฮเดรชันเป็นกระบวนการของการใช้ JavaScript ฝั่งไคลเอ็นต์เพื่อเพิ่มสถานะแอปพลิเคชันและการโต้ตอบให้กับ HTML ที่แสดงผลโดยเซิร์ฟเวอร์ เป็นคุณลักษณะของ React ซึ่งเป็นหนึ่งในเครื่องมือพื้นฐานที่สร้างเฟรมเวิร์ก Gatsby Gatsby ใช้ไฮเดรชั่นเพื่อแปลง HTML แบบสแตติกที่สร้างขึ้น ณ เวลาบิลด์เป็นแอปพลิเคชัน React
- ทำความเข้าใจปฏิกิริยาไฮเดรชั่น
เกิดอะไรขึ้น?
ปัญหาคือว่าเมื่อเราใช้ SSR (ฝั่งเซิร์ฟเวอร์ Rendered) React Frameworks เช่น NextJS ในทางเทคนิคแล้ว จะแสดงผลหน้าเว็บด้วยวิธีเฉพาะ จากนั้นเมื่อไคลเอ็นต์ (เบราว์เซอร์) แสดงผลสิ่งต่างๆ ก็คาดหวังว่าสถานะจะแสดงผล โดยเซิร์ฟเวอร์จะจับคู่สิ่งที่อยู่ในฝั่งไคลเอ็นต์เพื่อให้แน่ใจว่าทราบวิธีจัดการสถานะของตน
หากสถานะฝั่งเซิร์ฟเวอร์และสถานะไคลเอ็นต์ไม่ตรงกัน คุณจะได้รับข้อผิดพลาดในการไฮเดรชัน
วิธีที่ดีในการดูสิ่งนี้คือถ้าคุณปิดการใช้งาน JavaScrip บนเบราว์เซอร์ของคุณและเห็นความแตกต่างระหว่าง DOM ทั้งสอง
หากคุณต้องการเรียนรู้เพิ่มเติมเกี่ยวกับการให้ความชุ่มชื้น ฉันขอแนะนำให้ดูโพสต์บล็อกนี้โดย Josh Comeau เกี่ยวกับอันตรายของการขาดน้ำ
ทางออกคืออะไร?
วิธีแก้ไขคือเราต้องแยกแยะว่าอะไรควรจัดการโดยเซิร์ฟเวอร์และอะไรควรจัดการในฝั่งไคลเอนต์ ด้วยการปรับเปลี่ยนใหม่บางส่วนสำหรับ NextJS 13 ในเอกสารประกอบบางรายการ จะแสดงการแยกไฟล์ที่ชัดเจนระหว่างเซิร์ฟเวอร์และไคลเอนต์ แม้ว่าจะเป็นเพียงแนวคิด แต่เป็นสิ่งที่เราสามารถแสดงให้เห็นในการแก้ปัญหาได้
ความต้องการ
ก่อนที่เราจะเริ่ม ตรวจสอบให้แน่ใจว่าได้ติดตั้งสิ่งต่อไปนี้บนคอมพิวเตอร์ของคุณเพื่อทำตามขั้นตอนต่อไป
- 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
ในการแก้ไขปัญหานี้ เราจำเป็นต้องระบุวิธีจัดการไฟล์สองไฟล์สำหรับลูกค้าอย่างชัดเจนโดยใช้ความคิดเห็นที่ด้านบนสุดของแต่ละไฟล์ด้วย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>
);
};
ตอนนี้ถ้าเราลองเชื่อมต่อกับไซต์ เราก็ยังไม่ควรพบปัญหา
สุดท้าย รีเฟรชหน้านี้และดูข้อผิดพลาดเรื่องความชุ่มชื้น
ยอดเยี่ยม! ตอนนี้เรามีปัญหาแล้ว มาดูแนวทางแก้ไขกัน
การแก้ไขปัญหา
ฉันจะแนะนำแนวคิดบางอย่างเกี่ยวกับวิธีจัดระเบียบสิ่งต่างๆ ให้ดียิ่งขึ้น และแสดงวิธีแก้ไขสองสามวิธีด้วยวิธีการที่เหมาะสมที่สุด
ขั้นแรก เรามาแยกสิ่งที่จำเป็นสำหรับไคลเอ็นต์และสิ่งที่จำเป็นสำหรับเซิร์ฟเวอร์ของเราออกจากกัน หากเราดู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 นี้
อะไรต่อไป?
คอยติดตามบทความอื่นเกี่ยวกับการเริ่มใช้ Sign-In With Ethereum ที่ทำงานร่วมกับ NextJS ในเร็วๆ นี้
หากคุณได้รับประโยชน์จากสิ่งนี้ โปรดติดตามฉันบน Twitter (ที่ฉันใช้งานอยู่) @codingwithmannyและ instagram ที่@codingwithmanny