Comprensione degli errori di idratazione in NextJS 13 con una connessione al portafoglio Web3
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.
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.
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>
);
};
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>
);
};
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.tsx
perché sappiamo che la maggior parte dei provider trarrà vantaggio da hook simili useState
e useEffect
che 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>
);
};
Ora, se proviamo a connettere il sito, non dovremmo comunque riscontrare alcun problema.
Infine, aggiorniamo la pagina e vediamo l'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.tsx
noteremo che è solo nella isConnected
variabile che abbiamo veramente bisogno che le cose vengano gestite dal cliente. Tutto il resto può essere reso dal server.
Eseguiamo il refactoring page.tsx
per rimuovere use client
e 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>
);
};
Ora abilitiamo nuovamente JavaScript e creiamo il nostro nuovo ConnectWallet
componente.
# 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>
);
};
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 useState
affiancare a useEffect
. Sfortunatamente non puoi usare useRef
per 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>
);
};
Soluzione ottimizzata
Diventa però un po' ripetitivo aggiungere hasMounted
a 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 .