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

Эта проблема
Если вы недавно установили WAGMI со своим новым бета-приложением NextJS и попытались выполнить базовое подключение к кошельку, вы могли столкнуться с ошибкой, которая показывает ошибки гидратации. Следует отметить, что это проблема не только NextJS 13, но мы рассмотрим, как NextJS 13 изменяет некоторые соглашения, чтобы помочь изолировать и решить эту проблему.
Что такое увлажнение?
Гидратация — это процесс использования клиентского JavaScript для добавления состояния приложения и интерактивности в HTML, отображаемый на сервере. Это функция React, одного из основных инструментов, составляющих структуру Gatsby. Гэтсби использует гидратацию для преобразования статического HTML, созданного во время сборки, в приложение React.
- Понимание гидратации React
Что здесь происходит?
Проблема в том, что когда мы используем SSR (Server-Side Rendered) React Frameworks, такие как NextJS, он технически отображает страницу определенным образом, а затем, когда клиент (браузер) отображает что-то, он ожидает, что состояние отображается сервером соответствует тому, что находится на стороне клиента, чтобы гарантировать, что он знает, как управлять своим состоянием.
Если состояние на стороне сервера и состояние клиента не совпадают, возникает ошибка гидратации.
Отличный способ убедиться в этом — отключить JavaScrip в браузере и увидеть разницу между двумя DOM.

Если вы хотите узнать больше о гидратации, я определенно рекомендую взглянуть на этот пост в блоге Джоша Комо «Опасности регидратации» .
Какое решение?
Решение состоит в том, что нам нужно разделить то, что должно обрабатываться сервером и что должно обрабатываться на стороне клиента. С некоторыми новыми настройками NextJS 13 в части документации показано четкое разделение файлов между сервером и клиентом . Хотя это всего лишь идея, это то, что мы можем продемонстрировать в шоу, как найти решение.
Требования
Прежде чем мы начнем, убедитесь, что на вашем компьютере установлено следующее, чтобы выполнить следующие шаги.
- NVM или узел v18.12.1
- пнм v7.15.0
Мы собираемся воссоздать проблему, показанную выше, а затем рассмотрим некоторые возможные решения, как ее исправить.

Давайте получим нашу первоначальную настройку для воспроизведения ошибки.
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>
);
};

Конфигурация попутного ветра (опционально)
Этот следующий шаг не является обязательным, но мне нравится, когда при демонстрации пользовательского интерфейса все выглядит лучше, и для этого мы будем использовать 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>
);
};

Конфигурация ВАГМИ
Далее давайте настроим 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>
);
};

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

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

Большой! Теперь у нас есть проблема, давайте перейдем к решению.
Решение
Я собираюсь рассмотреть некоторые идеи о том, как лучше организовать вещи, а также показать несколько решений с оптимизированным методом.
Во-первых, давайте разделим, что нужно для нашего клиента и что нужно для нашего сервера. Если мы посмотрим на , 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>
);
};

Теперь давайте снова включим 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>
);
};

Кредит на решение
Следует отметить, что оригинальные растворы были созданы Джошем Комо в его блоге «Опасности регидратации» . Большое спасибо Джошу.
Первое решение
Давайте поработаем над первым решением, где мы сначала проверим, был ли смонтирован компонент, и если он не смонтирован, то не загружаем компонент.
Чтобы отслеживать это, вы можете 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>
);
};

Оптимизированное решение
Однако добавление 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 .
