Hydratationsfehler in NextJS 13 mit einer Web3-Wallet-Verbindung verstehen
Das Problem
Wenn Sie kürzlich WAGMI mit Ihrer neuen Beta-NextJS-Anwendung installiert und versucht haben, eine einfache Wallet-Verbindung herzustellen, sind Sie möglicherweise auf einen Fehler gestoßen, der Hydration-Fehler anzeigt. Es sollte beachtet werden, dass dies nicht nur ein Problem von NextJS 13 ist, aber wir werden behandeln, wie NextJS 13 einige Konventionen ändert, um dies zu isolieren und zu lösen.
Was ist Hydratation?
Hydration ist der Prozess der Verwendung von clientseitigem JavaScript, um Anwendungsstatus und Interaktivität zu servergerendertem HTML hinzuzufügen. Es ist eine Funktion von React, einem der zugrunde liegenden Tools, die das Gatsby-Framework erstellen. Gatsby verwendet Hydratation, um das statische HTML, das zur Erstellungszeit erstellt wurde, in eine React-Anwendung umzuwandeln.
- React Hydratation verstehen
Was ist los?
Das Problem ist, dass bei der Verwendung von SSR (Server-Side Rendered) React Frameworks wie NextJS die Seite technisch auf eine bestimmte Weise gerendert wird und dann, wenn der Client (der Browser) Dinge rendert, erwartet wird, dass der Zustand gerendert wird vom Server mit dem auf der Clientseite übereinstimmt, um sicherzustellen, dass er weiß, wie er seinen Status verwalten soll.
Wenn der serverseitige Status und der Clientstatus nicht übereinstimmen, erhalten Sie einen Hydratationsfehler.
Eine gute Möglichkeit, dies zu sehen, besteht darin, JavaScrip in Ihrem Browser zu deaktivieren und den Unterschied zwischen den beiden DOMs zu sehen.
Wenn Sie mehr über Flüssigkeitszufuhr erfahren möchten, empfehle ich Ihnen auf jeden Fall einen Blick auf diesen Blog-Beitrag von Josh Comeau zu The Perils of Rehydratation .
Was ist die Lösung?
Die Lösung besteht darin, dass wir darüber streiten müssen, was vom Server und was auf der Client-Seite behandelt werden soll. Bei einigen der neuen Anpassungen an NextJS 13 zeigt sich in einigen Dokumentationen eine klare Trennung von Dateien zwischen Server und Client . Obwohl es nur eine Idee ist, können wir zeigen, wie die Lösung gelöst werden kann.
Anforderungen
Bevor wir beginnen, vergewissern Sie sich, dass Folgendes auf Ihrem Computer installiert ist, um die nächsten Schritte auszuführen.
- NVM oder Knoten v18.12.1
- pnpm v7.15.0
Wir werden das oben gezeigte Problem neu erstellen und dann einige mögliche Lösungen zur Behebung des Problems durchgehen.
Lassen Sie uns unsere anfängliche Einrichtung vornehmen, um den Fehler zu reproduzieren.
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;
Wir werden einige der neueren Beta NextJS 13-Dokumente verwenden, um unsere NextJS-App zu konfigurieren.
Datei: ./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>
)
};
Datei: ./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>
);
};
Rückenwindkonfiguration (optional)
Dieser nächste Schritt ist optional, aber ich mag es, wenn die Dinge beim Demontieren der Benutzeroberfläche besser aussehen, und dafür verwenden wir Tailwind .
# FROM ./next13-wagmi-hydration
pnpm add -D tailwindcss postcss autoprefixer;
pnpx tailwindcss init -p;
Datei: ./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: [],
}
Datei: ./next13-wagmi-hyration/styles/global.css
@tailwind base;
@tailwind components;
@tailwind utilities;
Datei: ./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-Konfiguration
Lassen Sie uns als Nächstes das WAGMI-Setup einrichten, um Wallet-Interaktionen zu ermöglichen.
# 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;
Datei: ./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;
Datei: ./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;
Datei: ./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>
)
};
Datei: ./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').
Sehen Sie sich ihre Beta-Dokumentation auf NextJS Migrating Pages an .
Um dies zu beheben, müssen wir angeben, wie zwei Dateien für den Client behandelt werden sollen, indem wir einen Kommentar am Anfang jeder Datei mit verwenden use client;
.
Es gibt zwei Stellen, an denen wir das brauchen. Der erste ist unser provider.tsx
, weil wir wissen, dass die meisten Anbieter Hooks wie useState
und nutzen useEffect
werden, die hauptsächlich auf der Client-Seite verwendet werden. Der zweite Platz ist unser page.tsx
, aber das wird vorübergehend sein und erklären, warum.
Datei: ./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>
);
};
Wenn wir jetzt versuchen, die Site zu verbinden, sollten wir immer noch kein Problem sehen.
Zuletzt aktualisieren wir die Seite und sehen diesen Hydratationsfehler.
Groß! Jetzt haben wir das Problem, kommen wir zur Lösung.
Die Lösung
Ich werde einige Ideen durchgehen, wie man die Dinge ein bisschen besser organisieren kann, und auch ein paar Lösungen mit einer optimierten Methode zeigen.
Lassen Sie uns zunächst trennen, was für unseren Client und was für unseren Server benötigt wird. Wenn wir uns das ansehen, page.tsx
werden wir feststellen, isConnected
dass wir nur bei der Variable wirklich Dinge brauchen, die vom Client gehandhabt werden müssen. Alles andere kann vom Server gerendert werden.
Lassen Sie uns umgestalten , um die Wallet-Interaktion zu page.tsx
entfernen use client
und in eine eigene Komponente zu abstrahieren.
Datei: ./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>
);
};
Lassen Sie uns jetzt JavaScript wieder aktivieren und unsere neue ConnectWallet
Komponente erstellen.
# FROM ./next13-wagmi-hydration
touch ./app/wallet.tsx
Datei: ./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>
);
};
Lösungskredit
Es sei darauf hingewiesen, dass die Lösungen ursprünglich von Josh Comeau in seinem Blog „ The Perils of Rehydratation “ erstellt wurden . Vielen Dank an Josh.
Erste Lösung
Lassen Sie uns an der ersten Lösung arbeiten, bei der wir prüfen, ob die Komponente zuerst gemountet wurde, und wenn sie nicht gemountet ist, dann laden Sie die Komponente nicht.
Um dies zu verfolgen, können Sie useState
neben mit useEffect
. Leider kann man damit nicht useRef
verfolgen, ob die Komponente montiert ist.
Datei: ./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>
);
};
Optimierte Lösung
Es wird jedoch etwas repetitiv, jeder Komponente etwas hinzuzufügen hasMounted
, also können wir noch einen Schritt weiter gehen, indem wir diese Funktionalität in eine eigene Komponente abstrahieren.
# 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>
);
};
Datei: ./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
Um zu sehen, wie der vollständige Code funktioniert, sehen Sie sich dieses Github-Repository an.
Was kommt als nächstes?
Halten Sie Ausschau nach einem weiteren Artikel über eine erste Implementierung von Sign-In With Ethereum in Zusammenarbeit mit NextJS, der in Kürze erscheinen wird.
Wenn Sie davon profitieren, folgen Sie mir bitte auch auf Twitter (wo ich ziemlich aktiv bin) @codingwithmanny und auf Instagram unter @codingwithmanny .