Comprender los errores de hidratación en NextJS 13 con una conexión de billetera Web3

Nov 25 2022
Cómo corregir errores de hidratación en NextJS 13 con una conexión de billetera WAGMI de frontend
El problema Si recientemente instaló WAGMI con su aplicación NextJS recientemente beta e intentó hacer una conexión básica de billetera, es posible que haya encontrado un error que muestre errores de hidratación. Cabe señalar que este no es solo un problema de NextJS 13, sino que cubriremos cómo NextJS 13 cambia algunas convenciones para ayudar a aislar y resolver esto.
SiguienteJS 13 Error de hidratación

El problema

Si recientemente instaló WAGMI con su aplicación NextJS recientemente beta e intentó hacer una conexión básica de billetera, es posible que haya encontrado un error que muestre errores de hidratación. Cabe señalar que este no es solo un problema de NextJS 13, sino que cubriremos cómo NextJS 13 cambia algunas convenciones para ayudar a aislar y resolver esto.

¿Qué es la hidratación?

La hidratación es el proceso de usar JavaScript del lado del cliente para agregar el estado de la aplicación y la interactividad al HTML generado por el servidor. Es una característica de React, una de las herramientas subyacentes que forman el marco de Gatsby. Gatsby utiliza la hidratación para transformar el HTML estático creado en el momento de la compilación en una aplicación React.
- Comprender la hidratación de React

¿Que esta pasando?

El problema es que cuando usamos SSR (Server-Side Rendered) React Frameworks como NextJS, técnicamente representa la página de una manera específica, y luego, cuando el cliente (el navegador) representa las cosas, espera que el estado representado por el servidor coincide con lo que está en el lado del cliente para asegurarse de que sabe cómo administrar su estado.

Si el estado del lado del servidor y el estado del cliente no coinciden, obtendrá un error de hidratación.

Una excelente manera de ver esto es si deshabilita JavaScrip en su navegador y ve la diferencia entre los dos DOM.

Izquierda: JavaScript deshabilitado — Derecha: JavaScript habilitado

Si desea obtener más información sobre la hidratación, definitivamente le recomiendo que eche un vistazo a esta publicación de blog de Josh Comeau sobre Los peligros de la rehidratación .

¿Cual es la solución?

La solución es que debemos dividirnos sobre lo que debe manejar el servidor y lo que debe manejar el lado del cliente. Con algunos de los nuevos ajustes a NextJS 13, en parte de la documentación muestra una clara separación de archivos entre servidor y cliente . Aunque es solo una idea, es algo que podemos demostrar mostrando cómo resolver la solución.

Requisitos

Antes de comenzar, asegúrese de tener instalado lo siguiente en su computadora para seguir los siguientes pasos.

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

Vamos a recrear el problema que se mostró arriba y luego veremos algunas posibles soluciones sobre cómo solucionarlo.

SiguienteJS 13 Error de hidratación

Obtengamos nuestra configuración inicial para poder reproducir el error.

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;

Usaremos algunos de los documentos Beta NextJS 13 más nuevos para configurar nuestra aplicación NextJS.

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

Archivo: ./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 interfaz de usuario simple

Configuración de viento de cola (opcional)

El próximo paso es opcional, pero me gusta cuando las cosas se ven mejor al hacer una demostración de la interfaz de usuario, y para esto usaremos Tailwind .

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

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

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

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

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

SiguienteJS con Tailwind

Configuración de WAGMI

A continuación, configuremos WAGMI para permitir interacciones de billetera.

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

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

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

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

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

Consulte sus documentos beta en NextJS Migrating Pages .

Para solucionar esto, debemos explicar cómo se deben manejar dos archivos para el cliente utilizando un comentario en la parte superior de cada archivo con use client;.

Hay dos lugares donde necesitamos esto. La primera es nuestra provider.tsxporque sabemos que la mayoría de los proveedores se aprovecharán de ganchos similares useStatey useEffectque se utilizan principalmente en el lado del cliente. El segundo lugar es nuestro page.tsx, pero esto será temporal y explique por qué.

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

Conexión de billetera NextJS: no conectado

Ahora, si intentamos conectar el sitio, aún no deberíamos ver ningún problema.

Conexión de billetera NextJS — Conectado

Por último, actualicemos la página y veamos ese error de hidratación.

NextJS Wallet Connection — Error de hidratación

¡Estupendo! Ahora que tenemos el problema, pasemos a la solución.

La solución

Voy a repasar algunas ideas sobre cómo organizar las cosas un poco mejor y también mostraré algunas soluciones con un método optimizado.

Primero, separemos lo que se necesita para nuestro cliente y lo que se necesita para nuestro servidor. Si nos fijamos en el page.tsxnos daremos cuenta de que es sólo en la isConnectedvariable que realmente necesitamos que las cosas sean manejadas por el cliente. Todo lo demás puede ser renderizado por el servidor.

Refactoricemos page.tsxpara eliminar use clienty abstraer la interacción de la billetera a su propio componente.

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

Conexión de billetera NextJS: JavaScript deshabilitado

Ahora volvamos a habilitar JavaScript y creemos nuestro nuevo ConnectWalletcomponente.

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

Archivo: ./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 — Error de hidratación

Crédito de solución

Cabe señalar que las soluciones han sido creadas originalmente por Josh Comeau en su blog sobre Los peligros de la rehidratación . Muchas gracias a Jos.

Primera solución

Trabajemos en la primera solución, donde verificaremos si el componente se montó primero y, si no lo está, no cargue el componente.

Para realizar un seguimiento de esto, puede useStatejunto con useEffect. Desafortunadamente, no puede usar useRefpara realizar un seguimiento si el componente se ha montado.

Archivo: ./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 — Solución 1 Error de hidratación solucionado

Solución optimizada

Sin embargo, se vuelve un poco repetitivo agregar hasMounteda cada componente, por lo que podemos dar un paso más al abstraer esa funcionalidad en su propio 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>
    );
};

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

Para ver el código completo en funcionamiento, consulte este repositorio de github.

¿Que sigue?

Busque otro artículo sobre una implementación inicial de Sign-In With Ethereum trabajando con NextJS próximamente.

Si obtuviste valor de esto, sígueme también en Twitter (donde soy bastante activo) @codingwithmanny e Instagram en @codingwithmanny .