Compreendendo os erros de hidratação no NextJS 13 com uma conexão Web3 Wallet
O problema
Se você instalou recentemente o WAGMI com seu aplicativo NextJS recém-beta e tentou fazer alguma conexão básica de carteira, pode ter encontrado um erro que mostra erros de hidratação. Deve-se observar que este não é apenas um problema do NextJS 13, mas abordaremos como o NextJS 13 altera algumas convenções para ajudar a isolar e resolver isso.
O que é hidratação?
Hidratação é o processo de usar JavaScript do lado do cliente para adicionar o estado do aplicativo e a interatividade ao HTML renderizado pelo servidor. É um recurso do React, uma das ferramentas subjacentes que compõem o framework Gatsby. O Gatsby usa a hidratação para transformar o HTML estático criado no momento da compilação em um aplicativo React.
- Entendendo a Hidratação Reativa
O que está acontecendo?
O problema é que quando estamos usando SSR (Server-Side Rendered) React Frameworks como NextJS, ele renderiza tecnicamente a página de uma maneira específica e, quando o cliente (o navegador) renderiza as coisas, ele espera que o estado renderizado pelo servidor corresponde ao que está no lado do cliente para garantir que ele saiba como gerenciar seu estado.
Se o estado do lado do servidor e o estado do cliente não corresponderem, você receberá um erro de hidratação.
Uma ótima maneira de ver isso é desabilitar o JavaScrip em seu navegador e ver a diferença entre os dois DOM.
Se você quiser aprender mais sobre hidratação, definitivamente recomendo dar uma olhada neste post de blog de Josh Comeau sobre os perigos da reidratação .
Qual é a solução?
A solução é que precisamos dividir sobre o que deve ser tratado pelo servidor e o que deve ser tratado no lado do cliente. Com alguns dos novos ajustes do NextJS 13, em algumas documentações mostra uma clara separação de arquivos entre servidor e cliente . Embora seja apenas uma ideia, é algo que podemos demonstrar mostrando como resolver a solução.
Requisitos
Antes de começar, certifique-se de ter o seguinte instalado em seu computador para seguir as próximas etapas.
- NVM ou nó v18.12.1
- pnpm v7.15.0
Vamos recriar o problema mostrado acima e, em seguida, dar uma olhada em algumas soluções possíveis para corrigi-lo.
Vamos obter nossa configuração inicial para reproduzir o erro.
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 alguns dos documentos Beta NextJS 13 mais recentes para configurar nosso aplicativo NextJS.
Arquivo: ./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>
)
};
Arquivo: ./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>
);
};
Configuração do Tailwind (opcional)
Esta próxima etapa é opcional, mas gosto quando as coisas ficam melhores ao demonstrar a interface do usuário e, para isso, usaremos o Tailwind .
# FROM ./next13-wagmi-hydration
pnpm add -D tailwindcss postcss autoprefixer;
pnpx tailwindcss init -p;
Arquivo: ./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: [],
}
Arquivo: ./next13-wagmi-hyration/styles/global.css
@tailwind base;
@tailwind components;
@tailwind utilities;
Arquivo: ./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>
);
};
Configuração WAGMI
Em seguida, vamos configurar o WAGMI para permitir interações de carteira.
# 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;
Arquivo: ./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;
Arquivo: ./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;
Arquivo: ./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>
)
};
Arquivo: ./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').
Confira seus documentos beta em NextJS Migrating Pages .
Para corrigir isso, precisamos explicitar como dois arquivos devem ser manipulados para o cliente, utilizando um comentário na parte superior de cada arquivo com a extensão use client;
.
Há dois lugares onde precisamos disso. A primeira é nossa provider.tsx
porque sabemos que a maioria dos provedores aproveitará ganchos como useState
e useEffect
que são usados principalmente no lado do cliente. O segundo lugar é nosso page.tsx
, mas isso será temporário e explicaremos o porquê.
Arquivo: ./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>
);
};
Agora, se tentarmos conectar o site, ainda não veremos nenhum problema.
Por fim, vamos atualizar a página e ver o erro de hidratação.
Excelente! Agora que temos o problema, vamos para a solução.
A solução
Vou mostrar algumas ideias de como organizar as coisas um pouco melhor e também mostrar algumas soluções com um método otimizado.
Primeiro, vamos separar o que é necessário para o nosso cliente e o que é necessário para o nosso servidor. Se olharmos para o page.tsx
vamos perceber que é só na isConnected
variável que realmente precisamos que as coisas sejam tratadas pelo cliente. Todo o resto pode ser renderizado pelo servidor.
Vamos refatorar page.tsx
para remover use client
e abstrair a interação da carteira para seu próprio componente.
Arquivo: ./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>
);
};
Agora vamos ativar o JavaScript novamente e criar nosso novo ConnectWallet
componente.
# FROM ./next13-wagmi-hydration
touch ./app/wallet.tsx
Arquivo: ./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 da solução
Deve-se notar que as soluções originais foram criadas por Josh Comeau em seu blog sobre os perigos da reidratação . Muito obrigado a Josh.
Primeira Solução
Vamos trabalhar na primeira solução, onde verificaremos se o componente foi montado primeiro e, se não estiver montado, não carregue o componente.
Para acompanhar isso, você pode useState
junto com useEffect
. Infelizmente, você não pode usar useRef
para acompanhar se o componente foi montado.
Arquivo: ./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>
);
};
Solução Otimizada
No entanto, torna-se um pouco repetitivo adicionar hasMounted
a cada componente, para que possamos dar um passo adiante, abstraindo essa funcionalidade em seu próprio 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>
);
};
Arquivo: ./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 o código completo funcionando, confira este repositório github.
Qual é o próximo?
Fique atento para outro artigo sobre uma implementação inicial do Sign-In With Ethereum trabalhando com o NextJS em breve.
Se você obteve valor com isso, siga-me também no twitter (onde sou bastante ativo) @codingwithmanny e instagram em @codingwithmanny .