Hiểu lỗi hydrat hóa trong NextJS 13 với kết nối ví Web3
![](https://post.nghiatu.com/assets/images/m/max/724/1*1o1Blnfw28h64fAyOPdZQA.png)
Vấn đề
Nếu gần đây bạn đã cài đặt WAGMI với ứng dụng NextJS beta mới của mình và cố gắng thực hiện một số kết nối ví cơ bản, bạn có thể đã gặp lỗi hiển thị lỗi Hydration. Cần lưu ý rằng đây không chỉ là vấn đề của NextJS 13 mà chúng tôi sẽ đề cập đến cách NextJS 13 thay đổi một số quy ước để giúp cô lập và giải quyết vấn đề này.
Hydrat hóa là gì?
Hydrat hóa là quá trình sử dụng JavaScript phía máy khách để thêm trạng thái ứng dụng và tính tương tác vào HTML do máy chủ kết xuất. Đó là một tính năng của React, một trong những công cụ cơ bản tạo nên khung Gatsby. Gatsby sử dụng quá trình hydrat hóa để chuyển đổi HTML tĩnh được tạo tại thời điểm xây dựng thành ứng dụng React.
- Hiểu phản ứng hydrat hóa
Điều gì đang xảy ra?
Vấn đề là khi chúng ta đang sử dụng SSR (Server-Side Rendered) React Frameworks như NextJS, về mặt kỹ thuật, nó sẽ hiển thị trang theo một cách cụ thể và sau đó khi ứng dụng khách (trình duyệt) hiển thị mọi thứ, nó mong rằng trạng thái được hiển thị bởi máy chủ khớp với những gì ở phía máy khách để đảm bảo nó biết cách quản lý trạng thái của mình.
Nếu trạng thái phía máy chủ và trạng thái máy khách không khớp, thì bạn sẽ gặp lỗi hydrat hóa.
Một cách tuyệt vời để thấy điều này là nếu bạn tắt JavaScrip trên trình duyệt của mình và thấy sự khác biệt giữa hai DOM.
![](https://post.nghiatu.com/assets/images/m/max/724/1*0Nvv2qvOTzMcEPma_4ztUA.png)
Nếu bạn muốn tìm hiểu thêm về hydrat hóa, tôi chắc chắn khuyên bạn nên xem bài đăng trên blog này của Josh Comeau về Nguy cơ mất nước .
Giải pháp là gì?
Giải pháp là chúng ta cần phân chia những gì nên được xử lý bởi máy chủ và những gì nên được xử lý ở phía máy khách. Với một số điều chỉnh mới đối với NextJS 13, trong một số tài liệu, nó cho thấy sự phân tách rõ ràng các tệp giữa máy chủ và máy khách . Mặc dù đó chỉ là một ý tưởng, nhưng đó là điều chúng tôi có thể chứng minh để chỉ ra cách giải quyết giải pháp.
Yêu cầu
Trước khi bắt đầu, hãy đảm bảo đã cài đặt phần mềm sau trên máy tính của bạn để thực hiện các bước tiếp theo.
- NVM hoặc nút v18.12.1
- pnpm v7.15.0
Chúng tôi sẽ tạo lại sự cố đã hiển thị ở trên, sau đó hướng dẫn một số giải pháp khả thi về cách khắc phục sự cố.
![](https://post.nghiatu.com/assets/images/m/max/724/1*TjoKEDUdhpbChEIrKwAdIA.png)
Hãy thiết lập ban đầu để tái tạo lỗi.
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;
Chúng tôi sẽ sử dụng một số Tài liệu Beta NextJS 13 mới hơn để định cấu hình ứng dụng NextJS của chúng tôi.
Tập tin: ./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>
)
};
Tập tin: ./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>
);
};
![](https://post.nghiatu.com/assets/images/m/max/724/1*AfnJGFzmTc-78o3jO7Pytg.png)
Cấu hình Tailwind (Tùy chọn)
Bước tiếp theo này là tùy chọn, nhưng tôi thích khi mọi thứ trông đẹp hơn khi demo giao diện người dùng và để làm được điều này, chúng tôi sẽ sử dụng Tailwind .
# FROM ./next13-wagmi-hydration
pnpm add -D tailwindcss postcss autoprefixer;
pnpx tailwindcss init -p;
Tập tin: ./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: [],
}
Tập tin: ./next13-wagmi-hyration/styles/global.css
@tailwind base;
@tailwind components;
@tailwind utilities;
Tập tin: ./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>
);
};
![](https://post.nghiatu.com/assets/images/m/max/724/1*yUf12WeK3XTyZ8dycKnbRg.png)
Cấu hình WAGMI
Tiếp theo, hãy thiết lập WAGMI để cho phép tương tác với ví.
# 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;
Tập tin: ./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;
Tập tin: ./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;
Tập tin: ./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>
)
};
Tập tin: ./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').
Kiểm tra tài liệu beta của họ trên NextJS Migrating Pages .
Để khắc phục điều này, chúng ta cần làm rõ cách xử lý hai tệp cho máy khách bằng cách sử dụng nhận xét ở đầu mỗi tệp có phần mở rộng use client;
.
Có hai nơi chúng ta cần điều này. Đầu tiên là của chúng tôi provider.tsx
vì chúng tôi biết rằng hầu hết các nhà cung cấp sẽ tận dụng lợi thế của các hook như useState
và useEffect
phần lớn được sử dụng ở phía khách hàng. Vị trí thứ hai là của chúng tôi page.tsx
, nhưng đây sẽ là tạm thời và giải thích lý do tại sao.
Tập tin: ./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>
);
};
![](https://post.nghiatu.com/assets/images/m/max/724/1*_SJvwf66V2FT1f0PVyrUJg.png)
Bây giờ nếu chúng tôi thử và kết nối trang web, chúng tôi vẫn không thấy vấn đề gì.
![](https://post.nghiatu.com/assets/images/m/max/724/1*zVqjya2iWJ5Dx_YT0ER4yA.png)
Cuối cùng, hãy làm mới trang và xem lỗi hydrat hóa đó.
![](https://post.nghiatu.com/assets/images/m/max/724/1*TjoKEDUdhpbChEIrKwAdIA.png)
Tuyệt quá! Bây giờ chúng tôi có vấn đề, hãy chuyển sang giải pháp.
Giải pháp
Tôi sẽ xem qua một số ý tưởng về cách tổ chức mọi thứ tốt hơn một chút và cũng đưa ra một số giải pháp với phương pháp tối ưu hóa.
Trước tiên, hãy tách biệt những gì cần thiết cho khách hàng của chúng tôi và những gì cần thiết cho máy chủ của chúng tôi. Nếu chúng ta nhìn vào biến, page.tsx
chúng ta sẽ nhận thấy rằng chỉ ở isConnected
biến mà chúng ta thực sự cần những thứ được xử lý bởi khách hàng. Mọi thứ khác có thể được hiển thị bởi máy chủ.
Hãy cấu trúc lại page.tsx
để loại bỏ use client
và trừu tượng hóa tương tác ví với thành phần của chính nó.
Tập tin: ./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>
);
};
![](https://post.nghiatu.com/assets/images/m/max/724/1*6TzPWykZVYTv-mdyAx264Q.png)
Bây giờ, hãy bật lại JavaScript và tạo ConnectWallet
thành phần mới của chúng ta.
# FROM ./next13-wagmi-hydration
touch ./app/wallet.tsx
Tập tin: ./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>
);
};
![](https://post.nghiatu.com/assets/images/m/max/724/1*31aWHdbKGKIX9l-er-sMGQ.png)
tín dụng giải pháp
Cần lưu ý rằng các giải pháp ban đầu được tạo ra bởi Josh Comeau trên blog của anh ấy trên The Perils of Rehydration . Rất cám ơn Josh.
Giải pháp đầu tiên
Hãy làm việc với giải pháp đầu tiên, nơi chúng tôi sẽ kiểm tra xem thành phần đã được gắn trước chưa và nếu nó chưa được gắn, thì đừng tải thành phần đó.
Để theo dõi điều này, bạn có thể useState
cùng với useEffect
. Thật không may, bạn không thể sử dụng useRef
để theo dõi nếu thành phần đã được gắn kết.
Tập tin: ./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>
);
};
![](https://post.nghiatu.com/assets/images/m/max/724/1*CxJC6v_wLLviCqaGqGteIw.png)
Giải pháp tối ưu
Mặc dù việc thêm hasMounted
vào mọi thành phần sẽ trở nên hơi lặp đi lặp lại, vì vậy chúng ta có thể tiến thêm một bước nữa bằng cách trừu tượng hóa chức năng đó thành thành phần riêng của nó.
# 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>
);
};
Tập tin: ./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
Để xem toàn bộ mã hoạt động, hãy xem kho lưu trữ github này.
Cái gì tiếp theo?
Hãy xem một bài viết khác về triển khai ban đầu của Đăng nhập bằng Ethereum sẽ sớm hoạt động với NextJS.
Nếu bạn nhận được giá trị từ điều này, vui lòng theo dõi tôi trên twitter (nơi tôi hoạt động khá tích cực) @codingwithmanny và instagram tại @codingwithmanny .
![](https://post.nghiatu.com/assets/images/m/max/724/1*3G-o_2D3lJtdiE07w5inpQ.gif)