Понимание ошибок гидратации в NextJS 13 с подключением к кошельку Web3

Nov 25 2022
Как исправить ошибки гидратации в NextJS 13 с подключением внешнего кошелька WAGMI
Проблема Если вы недавно установили WAGMI со своим новым бета-приложением NextJS и попытались выполнить базовое подключение к кошельку, вы могли столкнуться с ошибкой, которая показывает ошибки гидратации. Следует отметить, что это проблема не только NextJS 13, но мы рассмотрим, как NextJS 13 изменяет некоторые соглашения, чтобы помочь изолировать и решить эту проблему.
NextJS 13 Ошибка гидратации

Эта проблема

Если вы недавно установили WAGMI со своим новым бета-приложением NextJS и попытались выполнить базовое подключение к кошельку, вы могли столкнуться с ошибкой, которая показывает ошибки гидратации. Следует отметить, что это проблема не только NextJS 13, но мы рассмотрим, как NextJS 13 изменяет некоторые соглашения, чтобы помочь изолировать и решить эту проблему.

Что такое увлажнение?

Гидратация — это процесс использования клиентского JavaScript для добавления состояния приложения и интерактивности в HTML, отображаемый на сервере. Это функция React, одного из основных инструментов, составляющих структуру Gatsby. Гэтсби использует гидратацию для преобразования статического HTML, созданного во время сборки, в приложение React.
- Понимание гидратации React

Что здесь происходит?

Проблема в том, что когда мы используем SSR (Server-Side Rendered) React Frameworks, такие как NextJS, он технически отображает страницу определенным образом, а затем, когда клиент (браузер) отображает что-то, он ожидает, что состояние отображается сервером соответствует тому, что находится на стороне клиента, чтобы гарантировать, что он знает, как управлять своим состоянием.

Если состояние на стороне сервера и состояние клиента не совпадают, возникает ошибка гидратации.

Отличный способ убедиться в этом — отключить JavaScrip в браузере и увидеть разницу между двумя DOM.

Слева: отключен JavaScript — справа: включен JavaScript

Если вы хотите узнать больше о гидратации, я определенно рекомендую взглянуть на этот пост в блоге Джоша Комо «Опасности регидратации» .

Какое решение?

Решение состоит в том, что нам нужно разделить то, что должно обрабатываться сервером и что должно обрабатываться на стороне клиента. С некоторыми новыми настройками NextJS 13 в части документации показано четкое разделение файлов между сервером и клиентом . Хотя это всего лишь идея, это то, что мы можем продемонстрировать в шоу, как найти решение.

Требования

Прежде чем мы начнем, убедитесь, что на вашем компьютере установлено следующее, чтобы выполнить следующие шаги.

  • NVM или узел v18.12.1
  • пнм v7.15.0

Мы собираемся воссоздать проблему, показанную выше, а затем рассмотрим некоторые возможные решения, как ее исправить.

NextJS 13 Ошибка гидратации

Давайте получим нашу первоначальную настройку для воспроизведения ошибки.

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;

Мы будем использовать некоторые из новых документов Beta NextJS 13 для настройки нашего приложения NextJS.

Файл: ./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>
  )
};

Файл: ./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 Простой пользовательский интерфейс

Конфигурация попутного ветра (опционально)

Этот следующий шаг не является обязательным, но мне нравится, когда при демонстрации пользовательского интерфейса все выглядит лучше, и для этого мы будем использовать Tailwind .

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

Файл: ./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: [],
}

Файл: ./next13-wagmi-hyration/styles/global.css

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

Файл: ./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 с Tailwind

Конфигурация ВАГМИ

Далее давайте настроим WAGMI, чтобы разрешить взаимодействие с кошельком.

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

Файл: ./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;

Файл: ./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;

Файл: ./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>
  )
};

Файл: ./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').

Ознакомьтесь с их документацией по бета-тестированию на NextJS Migration Pages .

Чтобы исправить это, нам нужно явно указать, как клиент должен обрабатывать два файла, используя комментарий в верхней части каждого файла с расширением use client;.

Нам это нужно в двух местах. Первый — наш, provider.tsxпотому что мы знаем, что большинство провайдеров будут использовать хуки, подобные useStateи useEffectкоторые в основном используются на стороне клиента. Второе место за нами page.tsx, но это будет временно и объясню почему.

Файл: ./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 — не подключено

Теперь, если мы попытаемся подключить сайт, мы все равно не увидим проблемы.

Подключение кошелька NextJS — подключено

Наконец, давайте обновим страницу и увидим эту ошибку гидратации.

Подключение к кошельку NextJS — ошибка гидратации

Большой! Теперь у нас есть проблема, давайте перейдем к решению.

Решение

Я собираюсь рассмотреть некоторые идеи о том, как лучше организовать вещи, а также показать несколько решений с оптимизированным методом.

Во-первых, давайте разделим, что нужно для нашего клиента и что нужно для нашего сервера. Если мы посмотрим на , page.tsxто заметим, что только isConnectedпеременная нам действительно нужна для обработки клиентом. Все остальное может быть обработано сервером.

Давайте проведем рефакторинг, page.tsxчтобы удалить use clientи абстрагировать взаимодействие с кошельком в отдельный компонент.

Файл: ./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 — JavaScript отключен

Теперь давайте снова включим JavaScript и создадим наш новый ConnectWalletкомпонент.

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

Файл: ./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 — ошибка гидратации

Кредит на решение

Следует отметить, что оригинальные растворы были созданы Джошем Комо в его блоге «Опасности регидратации» . Большое спасибо Джошу.

Первое решение

Давайте поработаем над первым решением, где мы сначала проверим, был ли смонтирован компонент, и если он не смонтирован, то не загружаем компонент.

Чтобы отслеживать это, вы можете useStateвместе с useEffect. К сожалению, вы не можете использовать useRefдля отслеживания, смонтирован ли компонент.

Файл: ./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 — Решение 1 Исправлена ​​ошибка гидратации

Оптимизированное решение

Однако добавление hasMountedк каждому компоненту становится немного повторяющимся, поэтому мы можем сделать еще один шаг, абстрагировав эту функциональность в отдельный компонент.

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

Файл: ./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

Чтобы увидеть, как работает полный код, загляните в этот репозиторий github.

Что дальше?

Скоро появится еще одна статья о начальной реализации Sign-In With Ethereum, работающей с NextJS.

Если вы получили от этого пользу, подписывайтесь на меня в твиттере (где я довольно активен) @codingwithmanny и в инстаграме @codingwithmanny .