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

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.

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.

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

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

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.tsx
porque sabemos que la mayoría de los proveedores se aprovecharán de ganchos similares useState
y useEffect
que 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>
);
};

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

Por último, actualicemos la página y veamos ese 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.tsx
nos daremos cuenta de que es sólo en la isConnected
variable que realmente necesitamos que las cosas sean manejadas por el cliente. Todo lo demás puede ser renderizado por el servidor.
Refactoricemos page.tsx
para eliminar use client
y 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>
);
};

Ahora volvamos a habilitar JavaScript y creemos nuestro nuevo ConnectWallet
componente.
# 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>
);
};

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 useState
junto con useEffect
. Desafortunadamente, no puede usar useRef
para 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>
);
};

Solución optimizada
Sin embargo, se vuelve un poco repetitivo agregar hasMounted
a 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 .
