Memahami Kesalahan Hidrasi Di NextJS 13 Dengan Koneksi Web3 Wallet

Masalah
Jika Anda baru saja menginstal WAGMI dengan aplikasi NextJS beta yang baru dan mencoba melakukan beberapa koneksi dompet dasar, Anda mungkin menemukan kesalahan yang menunjukkan kesalahan Hidrasi. Perlu dicatat bahwa ini bukan semata-mata masalah NextJS 13 tetapi kami akan membahas bagaimana NextJS 13 mengubah beberapa konvensi untuk membantu mengisolasi dan menyelesaikannya.
Apa itu Hidrasi?
Hidrasi adalah proses menggunakan JavaScript sisi klien untuk menambahkan status aplikasi dan interaktivitas ke HTML yang dirender oleh server. Ini adalah fitur React, salah satu alat dasar yang membuat framework Gatsby. Gatsby menggunakan hidrasi untuk mengubah HTML statis yang dibuat saat pembuatan menjadi aplikasi React.
- Memahami React Hydration
Apa yang sedang terjadi?
Masalahnya adalah ketika kita menggunakan SSR (Server-Side Rendered) React Frameworks seperti NextJS, secara teknis merender halaman dengan cara tertentu, dan kemudian ketika klien (browser) merender sesuatu, ia mengharapkan status dirender oleh server cocok dengan apa yang ada di sisi klien untuk memastikannya mengetahui cara mengelola statusnya.
Jika status sisi server dan status klien tidak cocok, Anda akan mendapatkan kesalahan hidrasi.
Cara terbaik untuk melihatnya adalah jika Anda menonaktifkan JavaScrip di browser Anda dan melihat perbedaan antara kedua DOM tersebut.

Jika Anda ingin mempelajari lebih lanjut tentang hidrasi, saya merekomendasikan untuk melihat posting blog ini oleh Josh Comeau di The Perils of Rehydration .
Apa Solusinya?
Solusinya adalah kita perlu memecah belah tentang apa yang harus ditangani oleh server dan apa yang harus ditangani di sisi klien. Dengan beberapa penyesuaian baru pada NextJS 13, dalam beberapa dokumentasi ini menunjukkan pemisahan file yang jelas antara server dan klien . Meskipun itu hanya sebuah ide, itu adalah sesuatu yang dapat kami tunjukkan untuk menunjukkan bagaimana memecahkan solusinya.
Persyaratan
Sebelum kita mulai, pastikan untuk menginstal yang berikut di komputer Anda untuk mengikuti langkah selanjutnya.
- NVM atau node v18.12.1
- pnpm v7.15.0
Kami akan membuat ulang masalah yang ditampilkan di atas, lalu menelusuri beberapa kemungkinan solusi tentang cara memperbaikinya.

Mari dapatkan pengaturan awal untuk mereproduksi kesalahan.
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;
Kami akan menggunakan beberapa Dokumen Beta NextJS 13 yang lebih baru untuk mengonfigurasi aplikasi NextJS kami.
Mengajukan: ./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>
)
};
Mengajukan: ./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>
);
};

Konfigurasi Tailwind (Opsional)
Langkah selanjutnya ini bersifat opsional, tetapi saya suka jika semuanya terlihat lebih baik saat mendemonstrasikan UI, dan untuk ini kita akan menggunakan Tailwind .
# FROM ./next13-wagmi-hydration
pnpm add -D tailwindcss postcss autoprefixer;
pnpx tailwindcss init -p;
Mengajukan: ./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: [],
}
Mengajukan: ./next13-wagmi-hyration/styles/global.css
@tailwind base;
@tailwind components;
@tailwind utilities;
Mengajukan: ./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>
);
};

Konfigurasi WAGMI
Selanjutnya, mari siapkan WAGMI untuk memungkinkan interaksi dompet.
# 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;
Mengajukan: ./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;
Mengajukan: ./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;
Mengajukan: ./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>
)
};
Mengajukan: ./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').
Lihat dokumen beta mereka di NextJS Migrating Pages .
Untuk memperbaikinya, kita perlu menjelaskan bagaimana dua file ditangani untuk klien dengan menggunakan komentar di bagian atas setiap file dengan use client;
.
Ada dua tempat yang kita butuhkan ini. Yang pertama adalah kami provider.tsx
karena kami tahu bahwa sebagian besar penyedia akan memanfaatkan pengait seperti useState
dan useEffect
yang sebagian besar digunakan di sisi klien. Tempat kedua adalah our page.tsx
, tetapi ini bersifat sementara dan jelaskan alasannya.
Mengajukan: ./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>
);
};

Sekarang jika kami mencoba dan menghubungkan situs tersebut, kami masih tidak akan melihat masalah.

Terakhir, mari segarkan halaman dan lihat kesalahan hidrasi itu.

Besar! Sekarang kita memiliki masalahnya, mari beralih ke solusinya.
Solusinya
Saya akan membahas beberapa ide tentang cara mengatur hal-hal sedikit lebih baik dan juga menunjukkan beberapa solusi dengan metode yang dioptimalkan.
Pertama, mari kita pisahkan apa yang dibutuhkan untuk klien kita dan apa yang dibutuhkan untuk server kita. Jika kita melihat page.tsx
kita akan melihat bahwa hanya pada isConnected
variabel kita benar-benar membutuhkan sesuatu untuk ditangani oleh klien. Segala sesuatu yang lain dapat diberikan oleh server.
Mari refactor page.tsx
untuk menghapus use client
dan mengabstraksi interaksi dompet ke komponennya sendiri.
Mengajukan: ./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>
);
};

Sekarang mari aktifkan kembali JavaScript dan buat ConnectWallet
komponen baru kita.
# FROM ./next13-wagmi-hydration
touch ./app/wallet.tsx
Mengajukan: ./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>
);
};

Kredit Solusi
Perlu dicatat bahwa solusi asli dibuat oleh Josh Comeau di blognya di The Perils of Rehydration . Banyak terima kasih kepada Josh.
Solusi Pertama
Mari kita kerjakan solusi pertama, di mana kita akan memeriksa apakah komponen sudah terpasang terlebih dahulu, dan jika belum terpasang, maka komponen tidak dimuat.
Untuk melacak ini, Anda dapat useState
bersama dengan useEffect
. Sayangnya Anda tidak dapat menggunakan useRef
untuk melacak jika komponen sudah terpasang.
Mengajukan: ./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>
);
};

Solusi yang Dioptimalkan
Itu menjadi sedikit berulang untuk ditambahkan hasMounted
ke setiap komponen, jadi kita bisa melangkah lebih jauh dengan mengabstraksi fungsionalitas itu ke dalam komponennya sendiri.
# 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>
);
};
Mengajukan: ./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
Untuk melihat kode lengkap berfungsi, lihat repositori github ini.
Apa berikutnya?
Nantikan artikel lain tentang implementasi awal Sign-In With Ethereum bekerja dengan NextJS segera hadir.
Jika Anda mendapat nilai dari ini, ikuti juga saya di twitter (di mana saya cukup aktif) @codingwithmanny dan instagram di @codingwithmanny .
