Bir Web3 Cüzdan Bağlantısıyla NextJS 13'te Hidrasyon Hatalarını Anlama
Sorun
Yakın zamanda yeni beta NextJS uygulamanızla WAGMI'yi yüklediyseniz ve bazı temel cüzdan bağlantılarını yapmaya çalıştıysanız Hidrasyon hatalarını gösteren bir hatayla karşılaşmış olabilirsiniz. Bunun yalnızca bir NextJS 13 sorunu olmadığı unutulmamalıdır, ancak bunu izole etmeye ve çözmeye yardımcı olmak için NextJS 13'ün bazı kuralları nasıl değiştirdiğini ele alacağız.
Hidrasyon Nedir?
Hidrasyon, sunucu tarafından oluşturulan HTML'ye uygulama durumu ve etkileşim eklemek için istemci tarafı JavaScript'i kullanma işlemidir. Bu, Gatsby çerçevesini oluşturan temel araçlardan biri olan React'in bir özelliğidir. Gatsby, derleme zamanında oluşturulan statik HTML'yi bir React uygulamasına dönüştürmek için hidrasyon kullanır.
- React Hydration'ı Anlamak
Ne oluyor?
Sorun şu ki, NextJS gibi SSR (Sunucu Tarafında Oluşturulan) React Frameworks kullandığımızda, teknik olarak sayfayı belirli bir şekilde oluşturur ve ardından müşteri (tarayıcı) bir şeyler oluşturduğunda, durumun olmasını bekler. durumunu nasıl yöneteceğini bildiğinden emin olmak için istemci tarafında olanlarla eşleşir.
Sunucu tarafı durumu ile istemci durumu eşleşmiyorsa, bir hidrasyon hatası alırsınız.
Bunu görmenin harika bir yolu, tarayıcınızda JavaScrip'i devre dışı bırakmanız ve iki DOM arasındaki farkı görmenizdir.
Hidrasyon hakkında daha fazla bilgi edinmek istiyorsanız, kesinlikle Josh Comeau'nun Rehidrasyonun Tehlikeleri hakkındaki bu blog gönderisine bir göz atmanızı tavsiye ederim .
Çözüm Nedir?
Çözüm, nelerin sunucu tarafından ele alınması gerektiği ve nelerin istemci tarafında ele alınması gerektiği konusunda bölünmemiz gerektiğidir. NextJS 13'teki bazı yeni ayarlamalarla, bazı belgelerde sunucu ve istemci arasında dosyaların net bir şekilde ayrıldığını gösterir . Bu sadece bir fikir olmasına rağmen, çözümün nasıl çözüleceğini göstermek için gösterebileceğimiz bir şeydir.
Gereksinimler
Başlamadan önce, sonraki adımları takip etmek için aşağıdakilerin bilgisayarınızda yüklü olduğundan emin olun.
- NVM veya düğüm v18.12.1
- pnpm v7.15.0
Yukarıda gösterilen sorunu yeniden oluşturacağız ve ardından nasıl düzeltileceğine dair bazı olası çözümler üzerinde yol göstereceğiz.
Hatayı yeniden oluşturmak için ilk kurulumumuzu yapalım.
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;
NextJS uygulamamızı yapılandırmak için daha yeni Beta NextJS 13 Docs'tan bazılarını kullanacağız.
Dosya: ./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>
)
};
Dosya: ./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 Yapılandırması (İsteğe Bağlı)
Bu sonraki adım isteğe bağlıdır, ancak kullanıcı arayüzünün demosunu yaparken işlerin daha iyi görünmesini seviyorum ve bunun için Tailwind kullanacağız .
# FROM ./next13-wagmi-hydration
pnpm add -D tailwindcss postcss autoprefixer;
pnpx tailwindcss init -p;
Dosya: ./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: [],
}
Dosya: ./next13-wagmi-hyration/styles/global.css
@tailwind base;
@tailwind components;
@tailwind utilities;
Dosya: ./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 Yapılandırması
Ardından, cüzdan etkileşimlerine izin vermek için WAGMI kurulumunu yapalım.
# 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;
Dosya: ./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;
Dosya: ./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;
Dosya: ./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>
)
};
Dosya: ./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'daki beta belgelerine göz atın .
Bunu düzeltmek için, her dosyanın başında use client;
.
Buna ihtiyacımız olan iki yer var. Birincisi , çoğu sağlayıcının çoğunlukla istemci tarafında kullanılan kancalardan provider.tsx
yararlanacağını bildiğimiz için . İkinci sıra bizim , ama bu geçici olacak ve nedenini açıklayacak.useState
useEffect
page.tsx
Dosya: ./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>
);
};
Şimdi siteyi bağlamayı denersek, yine de bir sorun görmemeliyiz.
Son olarak sayfayı yenileyelim ve hidrasyon hatasını görelim.
Harika! Şimdi sorunumuz var, çözüme geçelim.
Çözüm
Bazı şeyleri nasıl daha iyi organize edeceğimize dair bazı fikirlerin üzerinden geçeceğim ve ayrıca optimize edilmiş bir yöntemle birkaç çözüm göstereceğim.
Öncelikle clientimiz için gerekli olanlar ile serverimiz için gerekli olanları ayıralım. ' a bakarsak , müşteri tarafından ele alınması gereken şeylerin page.tsx
yalnızca değişkende olduğunu fark ederiz. isConnected
Geri kalan her şey sunucu tarafından oluşturulabilir.
page.tsx
M-cüzdan etkileşimini kaldırmak use client
ve kendi bileşenine soyutlamak için yeniden düzenleme yapalım .
Dosya: ./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>
);
};
Şimdi JavaScript'i tekrar etkinleştirelim ve yeni ConnectWallet
bileşenimizi oluşturalım.
# FROM ./next13-wagmi-hydration
touch ./app/wallet.tsx
Dosya: ./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>
);
};
Çözüm Kredisi
Çözümlerin orijinal olarak Josh Comeau tarafından The Perils of Rehydration hakkındaki blogunda yaratıldığı belirtilmelidir . Josh'a çok teşekkürler.
İlk Çözüm
Önce bileşenin monte edilip edilmediğini kontrol edeceğimiz ve monte edilmemişse bileşeni yüklemeyeceğimiz ilk çözüm üzerinde çalışalım.
Bunu takip etmek useState
için useEffect
. useRef
Ne yazık ki bileşenin monte edilip edilmediğini takip etmek için kullanamazsınız .
Dosya: ./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>
);
};
Optimize Edilmiş Çözüm
Her bileşene eklemek biraz tekrarlı olsa da hasMounted
, bu işlevselliği kendi bileşenine soyutlayarak bir adım daha ileri götürebiliriz.
# 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>
);
};
Dosya: ./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
Kodun tamamının çalıştığını görmek için bu github deposuna göz atın.
Sıradaki ne?
Yakında gelecek olan NextJS ile çalışan Ethereum ile Oturum Açmanın ilk uygulaması hakkında başka bir makale bekleyin.
Bundan değer aldıysanız, lütfen beni twitter'da (oldukça aktif olduğum yer) @codingwithmanny'de ve @codingwithmanny'de instagramda takip edin .