Understanding Hydration Errors In NextJS 13 With A Web3 Wallet Connection
How To Fix Hydration Errors In NextJS 13 With A Frontend WAGMI Wallet Connection
The Problem
If you have recently installed WAGMI with your newly beta NextJS application and tried to do some basic wallet connection, you might have come across an error that show Hydration errors. It should be noted that this is not solely a NextJS 13 issue but we’ll cover how NextJS 13 changes some conventions to help isolate and solve this.
What Is Hydration?
Hydration is the process of using client-side JavaScript to add application state and interactivity to server-rendered HTML. It’s a feature of React, one of the underlying tools that make the Gatsby framework. Gatsby uses hydration to transform the static HTML created at build time into a React application.
- Understanding React Hydration
What Is Going On?
The problem is that is that when we’re using SSR (Server-Side Rendered) React Frameworks like NextJS, it technically renders the page a specific way, and then when the client (the browser) renders things, it expects that the state rendered by the server matches what is on the client side to ensure it knows how to manage its state.
If the server-side state and the client state don’t match, then you get a hydration error.
A great way to see this is if you disable JavaScrip on your browser and see the difference between the two DOM.
If you want to learn more about hydration, I definitely recommend taking a look at this blog post by Josh Comeau on The Perils of Rehydration.
What’s The Solution?
The solution is that we need to divisive about what should be handled by the server and what should be handled on the client side. With some of the new adjustments to NextJS 13, in some of the documentation it shows a clear separation of files between server and client. Although it’s just an idea, it’s something we can demonstrate in show how to solve the solution.
Requirements
Before we start, make sure to have the following installed on your computer to follow the next steps.
- NVM or node v18.12.1
- pnpm v7.15.0
Building The Problem
We’re going to recreate the problem that was show above, and then walkthrough on some possible solutions on how to fix it.
Let’s get our initial setup of getting to reproduce the error.
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;
Initial NextJS 13 Configuration
We’ll be using some of the newer Beta NextJS 13 Docs to configure our NextJS app.
File: ./next13-wagmi-hydration/next.config.js
/** @type {import('next').NextConfig} */
const nextConfig = {
reactStrictMode: true,
swcMinify: true,
experimental: {
appDir: true,
},
};
module.exports = nextConfig;
To take advantage of it, we’ll need to create a new app
directory.
# FROM ./next13-wagmi-hydration
mkdir ./app;
Move all our pages from /pages
to the new /app
directory and then delete the /pages
directory, we won’t need it.
# FROM ./next13-wagmi-hydration
mv ./pages/index.tsx app/page.tsx
rm -rf pages;
We’ll also create a new layout.tsx
for our new /app
directory.
# FROM ./next13-wagmi-hydration
touch ./pages/layout.text
File: ./next13-wagmi-hyration/app/layout.tsx
// Imports
// ========================================================
import '../styles/globals.css';
// Layout
// ========================================================
export default function RootLayout({
children,
}: {
children: React.ReactNode
}) {
return (
<html>
<head />
<body>
{children}
</body>
</html>
)
};
We’re going to modify our page.tsx
with just some simple code to start.
File: ./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>
);
};
If we run our app, we should see the following.
Tailwind Configuration (Optional)
This next step is optional, but I like it when things look better when demoing UI, and for this we’ll be using Tailwind.
# FROM ./next13-wagmi-hydration
pnpm add -D tailwindcss postcss autoprefixer;
pnpx tailwindcss init -p;
Modify our newly generated tailwind.config.js
. Note that it says app
and not pages
.
File: ./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: [],
}
Modify our existing styles.
File: ./next13-wagmi-hyration/styles/global.css
@tailwind base;
@tailwind components;
@tailwind utilities;
Since we’re already importing global.css
into layout.tsx
we just just need to make a couple of styling changes.
File: ./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>
)
};
File: ./next13-wagmi-hyration/app/page.tsx
// 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>
);
};
We should see that our UI now has a darker look to it.
WAGMI Configuration
Next let’s get WAGMI setup to allow for wallet interactions.
# FROM ./next13-wagmi-hydration
pnpm add wagmi ethers;
To help with organization we’re going to create a new /providers
folder and a new providers.tsx
file to wrap all providers for out app.
# FROM ./next13-wagmi-hydration
mkdir ./providers;
mkdir ./providers/wagmi;
touch ./providers/wagmi/index.tsx;
touch ./app/providers.tsx;
Starting with our WAGMI provider.
File: ./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;
Next our root provider that will allow for multiple provider imports and then allow a single wrap on our layout.tsx
file so that all the functionality is available throughout the app.
File: ./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;
Lastly we’ll import that root provider into our layout.tsx
.
File: ./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>
)
};
Next let’s add the wallet functionality to our page.tsx
.
File: ./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>
);
};
If we try running our app, we might see this error show up in our terminal:
# when running pnpm run dev
wait - compiling...
error - ./node_modules/.pnpm/@tanstack+react-query-persist-client@4.16.1_fsy4krnncv4idvr4txy3aqiuqm/node_modules/@tanstack/react-query-persist-client/build/lib/PersistQueryClientProvider.mjs
Attempted import error: 'useState' is not exported from 'react' (imported as 'React').
This one of those new features with NextJS 13 where it does it’s best to make a clear distinction between server side pages and client side pages.
Check out their beta docs on NextJS Migrating Pages.
In order to fix this, we need to explicit with how two files are to be handled for the client by utilizing a comment at the top of each file with use client;
.
There are two places we need this. The first is our provider.tsx
because we know that most providers will be taking advantage of hooks like useState
and useEffect
which are mostly used on the client side. The second place is our page.tsx
, but this will be temporary and explain why.
File: ./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;
File: ./next13-wagmi-hyration/app/page.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 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>
);
};
If your MetaMask wallet is already connected to http://localhost:3000
we should see our UI working with no errors.
Now if we try and connect the site, we still shouldn’t see a problem.
Lastly, let’s refresh the page and see that hydration error.
Great! Now we have the issue, let’s move onto the solution.
The Solution
I’m going to walk through some ideas on how to organize things a bit better and also show a few solutions with an optimized method.
First, let’s make separate what is needed for our client and what is needed for our server. If we look at the page.tsx
we’ll notice that it’s only at the isConnected
variable that we really need things to be handled by the client. Everything else can be rendered by the server.
Let’s refactor page.tsx
to remove use client
and abstract the wallet interaction to its own component.
File: ./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>
);
};
What this does ensure that whatever is in this page will be rendered server side first. If we comment out the ConnectWallet
component, and disable JavaScript on our browser, we should still see the title “Wallet Connection”.
Now let’s enable JavaScript back and create our new ConnectWallet
component.
# FROM ./next13-wagmi-hydration
touch ./app/wallet.tsx
Let’s move all that functionality we had in page.tsx
into our wallet.tsx
and make sure we have 'use client’;
stated at the top of the file.
File: ./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>
);
};
We should still get our error.
Solution Credit
It should be noted that solutions have been original created by Josh Comeau on his blog on The Perils of Rehydration. Many thanks to Josh.
First Solution
Let’s work on the first solution, where we’ll check if the component has been mounted first, and if it’s not mounted, then don’t load the component.
To keep track of this you can useState
alongside with useEffect
. Unfortunately you can’t use useRef
to keep track if the component has mounted.
File: ./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>
);
};
Now if we try and connect our wallet and refresh our app, we should see that the hydration error is fixed!
Optimized Solution
It becomes a bit repetitive though to add hasMounted
to every component, so we can take it a step further by abstracting that functionality into its own component.
# FROM ./next13-wagmi-hydration
touch ./app/clientOnly.tsx;
File: ./next13-wagmi-hyration/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>
);
};
Now with this component, we can use this anywhere we need to be specific in using client side functionality to avoid hydration errors.
File: ./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>
);
};
Full Code
To see the full code working, check out this github repository.
What’s Next?
Look out for another article on an initial implementation of Sign-In With Ethereum working with NextJS coming soon.
If you got value from this, please also follow me on twitter (where I’m quite active) @codingwithmanny and instagram at @codingwithmanny.