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

Nov 25 2022
Jak naprawić błędy nawodnienia w NextJS 13 z interfejsem WAGMI Wallet Connection
Problem Jeśli niedawno zainstalowałeś WAGMI ze swoją nową wersją beta aplikacji NextJS i próbowałeś wykonać podstawowe połączenie z portfelem, być może napotkałeś błąd, który pokazuje 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.
NextJS 13 Błąd nawodnienia

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.

Po lewej: wyłączony JavaScript — Po prawej: włączony JavaScript

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ć.

NextJS 13 Błąd nawodnienia

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

http://localhost:300 Prosty interfejs użytkownika

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

NextJS z Tailwind

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 useStatei useEffectktó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>
    );
};

Połączenie z portfelem NextJS — brak połączenia

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

Połączenie z portfelem NextJS — Połączono

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

Połączenie z portfelem NextJS — 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.tsxtylko przy isConnectedzmiennej potrzebujemy spraw, którymi powinien zająć się klient. Wszystko inne może być renderowane przez serwer.

Przeprowadźmy refaktoryzację, page.tsxaby usunąć use clienti 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>
    );
};

Połączenie z portfelem NextJS — JavaScript wyłączony

Teraz włączmy ponownie JavaScript i stwórzmy nasz nowy ConnectWalletkomponent.

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

Połączenie z portfelem NextJS — błąd nawodnienia

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 useStatewraz z useEffect. Niestety nie można użyć useRefdo ś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>
    );
};

Połączenie z portfelem NextJS — rozwiązanie 1 Naprawiono błąd nawadniania

Zoptymalizowane rozwiązanie

Jednak dodawanie hasMounteddo 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 .