Compreendendo os erros de hidratação no NextJS 13 com uma conexão Web3 Wallet

Nov 25 2022
Como corrigir erros de hidratação no NextJS 13 com uma conexão de carteira WAGMI frontend
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.
PróximoJS 13 Erro de hidratação

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.

Esquerda: JavaScript desativado — Direita: JavaScript ativado

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.

PróximoJS 13 Erro de hidratação

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

http://localhost:300 interface do usuário simples

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

NextJS com Tailwind

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.tsxporque sabemos que a maioria dos provedores aproveitará ganchos como useStatee useEffectque 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>
    );
};

NextJS Wallet Connection — Não conectado

Agora, se tentarmos conectar o site, ainda não veremos nenhum problema.

NextJS Wallet Connection — Conectado

Por fim, vamos atualizar a página e ver o erro de hidratação.

NextJS Wallet Connection — 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.tsxvamos perceber que é só na isConnectedvariável que realmente precisamos que as coisas sejam tratadas pelo cliente. Todo o resto pode ser renderizado pelo servidor.

Vamos refatorar page.tsxpara remover use cliente 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>
    );
};

NextJS Wallet Connection — JavaScript desabilitado

Agora vamos ativar o JavaScript novamente e criar nosso novo ConnectWalletcomponente.

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

NextJS Wallet Connection — Erro de hidratação

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 useStatejunto com useEffect. Infelizmente, você não pode usar useRefpara 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>
    );
};

NextJS Wallet Connection — Solução 1 Erro de hidratação corrigido

Solução Otimizada

No entanto, torna-se um pouco repetitivo adicionar hasMounteda 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 .