Comprensione degli errori di idratazione in NextJS 13 con una connessione al portafoglio Web3

Nov 25 2022
Come correggere gli errori di idratazione in NextJS 13 con una connessione al portafoglio WAGMI front-end
Il problema Se di recente hai installato WAGMI con la tua nuova applicazione beta NextJS e hai provato a eseguire una connessione di base al portafoglio, potresti aver riscontrato un errore che mostra errori di idratazione. Va notato che questo non è solo un problema di NextJS 13, ma tratteremo come NextJS 13 modifica alcune convenzioni per isolare e risolvere questo problema.
NextJS 13 Errore di idratazione

Il problema

Se di recente hai installato WAGMI con la tua nuova applicazione beta NextJS e hai provato a eseguire una connessione di base al portafoglio, potresti aver riscontrato un errore che mostra errori di idratazione. Va notato che questo non è solo un problema di NextJS 13, ma tratteremo come NextJS 13 modifica alcune convenzioni per isolare e risolvere questo problema.

Cos'è l'idratazione?

L'idratazione è il processo di utilizzo di JavaScript lato client per aggiungere lo stato dell'applicazione e l'interattività all'HTML reso dal server. È una funzionalità di React, uno degli strumenti alla base del framework Gatsby. Gatsby utilizza l'idratazione per trasformare l'HTML statico creato in fase di compilazione in un'applicazione React.
- Comprendere React Hydration

Cosa sta succedendo?

Il problema è che quando utilizziamo framework React SSR (Server-Side Rendered) come NextJS, tecnicamente esegue il rendering della pagina in un modo specifico, quindi quando il client (il browser) esegue il rendering delle cose, si aspetta che lo stato sia stato reso dal server corrisponda a ciò che è sul lato client per assicurarsi che sappia come gestire il suo stato.

Se lo stato lato server e lo stato client non corrispondono, viene visualizzato un errore di idratazione.

Un ottimo modo per vederlo è se disabiliti JavaScrip sul tuo browser e vedi la differenza tra i due DOM.

A sinistra: JavaScript disabilitato — A destra: JavaScript abilitato

Se vuoi saperne di più sull'idratazione, ti consiglio vivamente di dare un'occhiata a questo post sul blog di Josh Comeau su The Perils of Rehydration .

Qual è la soluzione?

La soluzione è che dobbiamo dividere su cosa dovrebbe essere gestito dal server e cosa dovrebbe essere gestito dal lato client. Con alcune delle nuove modifiche a NextJS 13, in parte della documentazione mostra una netta separazione dei file tra server e client . Sebbene sia solo un'idea, è qualcosa che possiamo dimostrare mostrando come risolvere la soluzione.

Requisiti

Prima di iniziare, assicurati di aver installato quanto segue sul tuo computer per seguire i passaggi successivi.

  • NVM o nodo v18.12.1
  • pnpm v7.15.0

Ricreeremo il problema mostrato sopra e poi esamineremo alcune possibili soluzioni su come risolverlo.

NextJS 13 Errore di idratazione

Otteniamo la nostra configurazione iniziale per riprodurre l'errore.

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;

Utilizzeremo alcuni dei più recenti documenti Beta NextJS 13 per configurare la nostra app NextJS.

File: ./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>
  )
};

File: ./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 Interfaccia utente semplice

Configurazione del vento in coda (opzionale)

Questo passaggio successivo è facoltativo, ma mi piace quando le cose sembrano migliori durante la demo dell'interfaccia utente e per questo utilizzeremo Tailwind .

# FROM ./next13-wagmi-hydration
pnpm add -D tailwindcss postcss autoprefixer;
pnpx tailwindcss init -p;

File: ./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: [],
}

File: ./next13-wagmi-hyration/styles/global.css

@tailwind base;
@tailwind components;
@tailwind utilities;

File: ./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 con Tailwind

Configurazione WAGMI

Quindi impostiamo WAGMI per consentire le interazioni con il portafoglio.

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

File: ./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;

File: ./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;

File: ./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>
  )
};

File: ./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').

Dai un'occhiata ai loro documenti beta su NextJS Migrating Pages .

Per risolvere questo problema, dobbiamo esplicitare come devono essere gestiti due file per il client utilizzando un commento nella parte superiore di ciascun file con use client;.

Ci sono due posti in cui abbiamo bisogno di questo. Il primo è il nostro provider.tsxperché sappiamo che la maggior parte dei provider trarrà vantaggio da hook simili useStatee useEffectche sono utilizzati principalmente dal lato client. Il secondo posto è il nostro page.tsx, ma questo sarà temporaneo e spiegherà perché.

File: ./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 Connection — Non connesso

Ora, se proviamo a connettere il sito, non dovremmo comunque riscontrare alcun problema.

NextJS Wallet Connection — Connesso

Infine, aggiorniamo la pagina e vediamo l'errore di idratazione.

NextJS Wallet Connection — Errore di idratazione

Grande! Ora che abbiamo il problema, passiamo alla soluzione.

La soluzione

Esaminerò alcune idee su come organizzare un po' meglio le cose e mostrerò anche alcune soluzioni con un metodo ottimizzato.

Innanzitutto, separiamo ciò che è necessario per il nostro client e ciò che è necessario per il nostro server. Se guardiamo page.tsxnoteremo che è solo nella isConnectedvariabile che abbiamo veramente bisogno che le cose vengano gestite dal cliente. Tutto il resto può essere reso dal server.

Eseguiamo il refactoring page.tsxper rimuovere use cliente astrarre l'interazione del portafoglio al proprio componente.

File: ./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 Connection - JavaScript disabilitato

Ora abilitiamo nuovamente JavaScript e creiamo il nostro nuovo ConnectWalletcomponente.

# FROM ./next13-wagmi-hydration
touch ./app/wallet.tsx

File: ./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 — Errore di idratazione

Credito di soluzione

Va notato che le soluzioni originali sono state create da Josh Comeau sul suo blog su The Perils of Rehydration . Molte grazie a Josh.

Prima soluzione

Lavoriamo sulla prima soluzione, dove verificheremo se il componente è stato montato per primo, e se non lo è, allora non caricare il componente.

Per tenerne traccia puoi useStateaffiancare a useEffect. Sfortunatamente non puoi usare useRefper tenere traccia se il componente è stato montato.

File: ./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 — Soluzione 1 Errore di idratazione risolto

Soluzione ottimizzata

Diventa però un po' ripetitivo aggiungere hasMounteda ogni componente, quindi possiamo fare un ulteriore passo avanti astraendo quella funzionalità nel proprio componente.

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

File: ./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

Per vedere il codice completo funzionante, dai un'occhiata a questo repository github.

Qual è il prossimo?

Cerca un altro articolo su un'implementazione iniziale di Sign-In With Ethereum che funziona con NextJS in arrivo.

Se hai tratto valore da questo, per favore seguimi anche su Twitter (dove sono abbastanza attivo) @codingwithmanny e instagram su @codingwithmanny .