Comprendre les erreurs d'hydratation dans NextJS 13 avec une connexion de portefeuille Web3
Le problème
Si vous avez récemment installé WAGMI avec votre application NextJS nouvellement bêta et que vous avez essayé de vous connecter à un portefeuille de base, vous avez peut-être rencontré une erreur indiquant des erreurs d'hydratation. Il convient de noter qu'il ne s'agit pas uniquement d'un problème lié à NextJS 13, mais nous expliquerons comment NextJS 13 modifie certaines conventions pour aider à isoler et à résoudre ce problème.
Qu'est-ce que l'hydratation ?
L'hydratation est le processus d'utilisation de JavaScript côté client pour ajouter l'état de l'application et l'interactivité au code HTML rendu par le serveur. C'est une fonctionnalité de React, l'un des outils sous-jacents qui font le framework Gatsby. Gatsby utilise l'hydratation pour transformer le HTML statique créé au moment de la construction en une application React.
- Comprendre React Hydratation
Que se passe-t-il?
Le problème est que lorsque nous utilisons des cadres React SSR (Server-Side Rendered) comme NextJS, il rend techniquement la page d'une manière spécifique, puis lorsque le client (le navigateur) rend les choses, il s'attend à ce que l'état rendu par le serveur correspond à ce qui se trouve du côté client pour s'assurer qu'il sait comment gérer son état.
Si l'état côté serveur et l'état client ne correspondent pas, vous obtenez une erreur d'hydratation.
Une excellente façon de voir cela est de désactiver JavaScrip sur votre navigateur et de voir la différence entre les deux DOM.
Si vous voulez en savoir plus sur l'hydratation, je vous recommande vivement de jeter un œil à ce billet de blog de Josh Comeau sur Les périls de la réhydratation .
Quelle est la solution ?
La solution est que nous devons diviser ce qui doit être géré par le serveur et ce qui doit être géré côté client. Avec certains des nouveaux ajustements apportés à NextJS 13, dans une partie de la documentation, il montre une séparation claire des fichiers entre le serveur et le client . Bien que ce ne soit qu'une idée, c'est quelque chose que nous pouvons démontrer en montrant comment résoudre la solution.
Conditions
Avant de commencer, assurez-vous que les éléments suivants sont installés sur votre ordinateur pour suivre les étapes suivantes.
- NVM ou nœud v18.12.1
- pnpm v7.15.0
Nous allons recréer le problème qui a été montré ci-dessus, puis passer en revue quelques solutions possibles pour le résoudre.
Prenons notre configuration initiale pour reproduire l'erreur.
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;
Nous utiliserons certains des nouveaux documents Beta NextJS 13 pour configurer notre application NextJS.
Dossier: ./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>
)
};
Dossier: ./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>
);
};
Configuration vent arrière (facultatif)
Cette prochaine étape est facultative, mais j'aime quand les choses s'améliorent lors de la démonstration de l'interface utilisateur, et pour cela, nous utiliserons Tailwind .
# FROM ./next13-wagmi-hydration
pnpm add -D tailwindcss postcss autoprefixer;
pnpx tailwindcss init -p;
Dossier: ./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: [],
}
Dossier: ./next13-wagmi-hyration/styles/global.css
@tailwind base;
@tailwind components;
@tailwind utilities;
Dossier: ./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>
);
};
Configuration WAGMI
Passons ensuite à la configuration de WAGMI pour permettre les interactions avec le portefeuille.
# 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;
Dossier: ./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;
Dossier: ./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;
Dossier: ./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>
)
};
Dossier: ./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').
Consultez leurs documents bêta sur NextJS Migrating Pages .
Afin de résoudre ce problème, nous devons expliquer comment deux fichiers doivent être gérés pour le client en utilisant un commentaire en haut de chaque fichier avec use client;
.
Il y a deux endroits où nous en avons besoin. Le premier est le notre provider.tsx
parce que nous savons que la plupart des fournisseurs tireront parti des crochets comme useState
et useEffect
qui sont principalement utilisés du côté client. La deuxième place est notre page.tsx
, mais ce sera temporaire et expliquez pourquoi.
Dossier: ./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>
);
};
Maintenant, si nous essayons de connecter le site, nous ne devrions toujours pas voir de problème.
Enfin, actualisons la page et voyons cette erreur d'hydratation.
Super! Maintenant que nous avons le problème, passons à la solution.
La solution
Je vais passer en revue quelques idées sur la façon d'organiser un peu mieux les choses et également montrer quelques solutions avec une méthode optimisée.
Tout d'abord, séparons ce qui est nécessaire pour notre client et ce qui est nécessaire pour notre serveur. Si nous regardons le page.tsx
nous remarquerons que ce n'est qu'au niveau de la isConnected
variable que nous avons vraiment besoin que les choses soient gérées par le client. Tout le reste peut être rendu par le serveur.
Refactorisons page.tsx
pour supprimer use client
et résumer l'interaction du portefeuille à son propre composant.
Dossier: ./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>
);
};
Maintenant, réactivons JavaScript et créons notre nouveau ConnectWallet
composant.
# FROM ./next13-wagmi-hydration
touch ./app/wallet.tsx
Dossier: ./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>
);
};
Crédit de solution
Il est à noter que des solutions originales ont été créées par Josh Comeau sur son blog The Perils of Rehydration . Un grand merci à Josh.
Première solution
Travaillons sur la première solution, où nous vérifierons si le composant a été monté en premier, et s'il n'est pas monté, alors ne chargeons pas le composant.
Pour garder une trace de cela, vous pouvez useState
avec useEffect
. Malheureusement, vous ne pouvez pas utiliser useRef
pour savoir si le composant est monté.
Dossier: ./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>
);
};
Solution optimisée
Il devient cependant un peu répétitif d'ajouter hasMounted
à chaque composant, nous pouvons donc aller plus loin en extrayant cette fonctionnalité dans son propre composant.
# 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>
);
};
Dossier: ./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
Pour voir le code complet fonctionner, consultez ce référentiel github.
Et après?
Recherchez un autre article sur une implémentation initiale de Sign-In With Ethereum fonctionnant avec NextJS à venir.
Si vous en avez tiré profit, veuillez également me suivre sur Twitter (où je suis assez actif) @codingwithmanny et instagram sur @codingwithmanny .