Zrozumienie błędów nawodnienia w NextJS 13 z połączeniem portfela Web3

Problem
Jeśli niedawno zainstalowałeś WAGMI z nową wersją beta aplikacji NextJS i próbowałeś wykonać podstawowe połączenie z portfelem, być może napotkałeś błąd pokazujący błędy Hydration. Należy zauważyć, że nie jest to wyłącznie problem NextJS 13, ale omówimy, w jaki sposób NextJS 13 zmienia niektóre konwencje, aby pomóc wyizolować i rozwiązać ten problem.
Co to jest nawodnienie?
Nawadnianie to proces wykorzystujący JavaScript po stronie klienta w celu dodania stanu aplikacji i interaktywności do kodu HTML renderowanego przez serwer. Jest to funkcja React, jednego z podstawowych narzędzi tworzących framework Gatsby. Gatsby używa hydratacji do przekształcenia statycznego kodu HTML utworzonego w czasie kompilacji w aplikację React.
- Zrozumienie reakcji nawodnienia
Co się dzieje?
Problem polega na tym, że kiedy używamy SSR (Server-Side Rendered) React Frameworks, takich jak NextJS, technicznie renderuje on stronę w określony sposób, a następnie, gdy klient (przeglądarka) renderuje rzeczy, oczekuje, że stan renderowany przez serwer pasuje do tego, co jest po stronie klienta, aby upewnić się, że wie, jak zarządzać swoim stanem.
Jeśli stan po stronie serwera i stan klienta nie są zgodne, pojawia się błąd nawodnienia.
Świetnym sposobem, aby to zobaczyć, jest wyłączenie JavaScrip w przeglądarce i zobaczenie różnicy między dwoma DOM.

Jeśli chcesz dowiedzieć się więcej o nawodnieniu, zdecydowanie polecam zajrzeć do tego wpisu Josha Comeau na blogu The Perils of Rehydration .
Jakie jest rozwiązanie?
Rozwiązanie polega na tym, że musimy dokonać podziału na to, co powinno być obsługiwane przez serwer, a co powinno być obsługiwane po stronie klienta. Z niektórymi nowymi dostosowaniami do NextJS 13, w niektórych dokumentacjach widać wyraźne oddzielenie plików między serwerem a klientem . Chociaż to tylko pomysł, możemy to zademonstrować, pokazując, jak rozwiązać rozwiązanie.
Wymagania
Zanim zaczniemy, upewnij się, że na komputerze są zainstalowane następujące elementy, aby wykonać kolejne kroki.
- NVM lub węzeł v18.12.1
- pnpm v7.15.0
Odtworzymy problem pokazany powyżej, a następnie przedstawimy kilka możliwych rozwiązań, jak go naprawić.

Przejdźmy do naszej początkowej konfiguracji odtwarzania błędu.
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;
Będziemy używać niektórych nowszych dokumentów Beta NextJS 13 do konfigurowania naszej aplikacji NextJS.
Plik: ./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>
)
};
Plik: ./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>
);
};

Konfiguracja tylnego wiatru (opcjonalnie)
Ten następny krok jest opcjonalny, ale lubię, gdy rzeczy wyglądają lepiej podczas demonstracji interfejsu użytkownika, a do tego celu użyjemy Tailwind .
# FROM ./next13-wagmi-hydration
pnpm add -D tailwindcss postcss autoprefixer;
pnpx tailwindcss init -p;
Plik: ./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: [],
}
Plik: ./next13-wagmi-hyration/styles/global.css
@tailwind base;
@tailwind components;
@tailwind utilities;
Plik: ./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>
);
};

Konfiguracja WAGMI
Następnie skonfigurujmy WAGMI, aby umożliwić interakcje z portfelem.
# 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;
Plik: ./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;
Plik: ./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;
Plik: ./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>
)
};
Plik: ./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').
Sprawdź ich dokumentację beta na stronach migracji NextJS .
Aby to naprawić, musimy wyraźnie określić, w jaki sposób dwa pliki mają być obsługiwane przez klienta, wykorzystując komentarz na górze każdego pliku z rozszerzeniem use client;
.
Potrzebujemy tego w dwóch miejscach. Pierwszy jest nasz provider.tsx
, ponieważ wiemy, że większość dostawców będzie korzystać z haków podobnych useState
i useEffect
które są najczęściej używane po stronie klienta. Drugie miejsce jest nasze page.tsx
, ale to będzie tymczasowe i wyjaśnij dlaczego.
Plik: ./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>
);
};

Teraz, jeśli spróbujemy połączyć się z witryną, nadal nie powinniśmy widzieć problemu.

Na koniec odświeżmy stronę i zobaczmy ten błąd nawodnienia.

Świetny! Skoro mamy już problem, przejdźmy do rozwiązania.
Rozwiązanie
Zamierzam przedstawić kilka pomysłów, jak lepiej zorganizować rzeczy, a także pokazać kilka rozwiązań z zoptymalizowaną metodą.
Najpierw oddzielmy to, co jest potrzebne naszemu klientowi i co jest potrzebne naszemu serwerowi. Jeśli spojrzymy na zmienną, zauważymy, że tak naprawdę page.tsx
tylko przy isConnected
zmiennej potrzebujemy spraw, którymi powinien zająć się klient. Wszystko inne może być renderowane przez serwer.
Przeprowadźmy refaktoryzację, page.tsx
aby usunąć use client
i wyodrębnić interakcję portfela z jego własnym komponentem.
Plik: ./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>
);
};

Teraz włączmy ponownie JavaScript i stwórzmy nasz nowy ConnectWallet
komponent.
# FROM ./next13-wagmi-hydration
touch ./app/wallet.tsx
Plik: ./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>
);
};

Kredyt rozwiązania
Należy zauważyć, że oryginalne rozwiązania zostały stworzone przez Josha Comeau na jego blogu The Perils of Rehydration . Wielkie dzięki dla Josha.
Pierwsze rozwiązanie
Popracujmy nad pierwszym rozwiązaniem, w którym sprawdzimy, czy komponent został zamontowany jako pierwszy, a jeśli nie, to nie ładuj komponentu.
Aby to śledzić, możesz useState
wraz z useEffect
. Niestety nie można użyć useRef
do śledzenia, czy komponent został zamontowany.
Plik: ./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>
);
};

Zoptymalizowane rozwiązanie
Jednak dodawanie hasMounted
do każdego komponentu staje się nieco powtarzalne, więc możemy pójść o krok dalej, wyodrębniając tę funkcjonalność do własnego komponentu.
# 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>
);
};
Plik: ./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
Aby zobaczyć działający pełny kod, sprawdź to repozytorium github.
Co dalej?
Wkrótce pojawi się kolejny artykuł na temat wstępnej implementacji logowania za pomocą Ethereum współpracującego z NextJS.
Jeśli masz z tego wartość, śledź mnie również na Twitterze (gdzie jestem dość aktywny) @codingwithmanny i instagramie @codingwithmanny .
