Comprendre les erreurs d'hydratation dans NextJS 13 avec une connexion de portefeuille Web3

Nov 25 2022
Comment réparer les erreurs d'hydratation dans NextJS 13 avec une connexion de portefeuille WAGMI frontale
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.
SuivantJS 13 Hydration Error

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.

Gauche : JavaScript désactivé — Droite : JavaScript activé

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.

SuivantJS 13 Hydration Error

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

http://localhost:300 Interface utilisateur simple

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

NextJS avec Tailwind

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.tsxparce que nous savons que la plupart des fournisseurs tireront parti des crochets comme useStateet useEffectqui 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>
    );
};

NextJS Wallet Connection — Non connecté

Maintenant, si nous essayons de connecter le site, nous ne devrions toujours pas voir de problème.

NextJS Wallet Connection — Connecté

Enfin, actualisons la page et voyons cette erreur d'hydratation.

NextJS Wallet Connection — 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.tsxnous remarquerons que ce n'est qu'au niveau de la isConnectedvariable 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.tsxpour supprimer use clientet 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>
    );
};

NextJS Wallet Connection — JavaScript désactivé

Maintenant, réactivons JavaScript et créons notre nouveau ConnectWalletcomposant.

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

NextJS Wallet Connection — Erreur d'hydratation

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 useStateavec useEffect. Malheureusement, vous ne pouvez pas utiliser useRefpour 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>
    );
};

NextJS Wallet Connection — Solution 1 Correction de l'erreur d'hydratation

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 .