Hydratationsfehler in NextJS 13 mit einer Web3-Wallet-Verbindung verstehen

Nov 25 2022
So beheben Sie Hydratationsfehler in NextJS 13 mit einer Frontend-WAGMI-Wallet-Verbindung
Das Problem Wenn Sie kürzlich WAGMI mit Ihrer neuen Beta-NextJS-Anwendung installiert und versucht haben, eine grundlegende 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.
WeiterJS 13 Hydration Error

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.

Links: JavaScript deaktiviert — Rechts: JavaScript aktiviert

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.

WeiterJS 13 Hydration Error

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>
    );
};

http://localhost:300 Einfache Benutzeroberfläche

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>
    );
};

NextJS mit Rückenwind

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 useStateund nutzen useEffectwerden, 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>
    );
};

NextJS Wallet-Verbindung – Nicht verbunden

Wenn wir jetzt versuchen, die Site zu verbinden, sollten wir immer noch kein Problem sehen.

NextJS Wallet-Verbindung – Verbunden

Zuletzt aktualisieren wir die Seite und sehen diesen Hydratationsfehler.

NextJS Wallet Connection – Trinkfehler

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.tsxwerden wir feststellen, isConnecteddass 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.tsxentfernen use clientund 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>
    );
};

NextJS Wallet-Verbindung – JavaScript deaktiviert

Lassen Sie uns jetzt JavaScript wieder aktivieren und unsere neue ConnectWalletKomponente 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>
    );
};

NextJS Wallet Connection – Trinkfehler

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 useStateneben mit useEffect. Leider kann man damit nicht useRefverfolgen, 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>
    );
};

NextJS Wallet Connection – Lösung 1 Trinkfehler behoben

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 .