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

Manny
12 min readNov 25, 2022
NextJS 13 Hydration Error

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.

Left: Disabled JavaScript — Right: Enabled JavaScript

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.

NextJS 13 Hydration Error

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.

http://localhost:300 Simple UI

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.

NextJS with Tailwind

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.

NextJS Wallet Connection — Not Connected

Now if we try and connect the site, we still shouldn’t see a problem.

NextJS Wallet Connection — Connected

Lastly, let’s refresh the page and see that hydration error.

NextJS Wallet Connection — 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”.

NextJS Wallet Connection — JavaScript Disabled

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.

NextJS Wallet Connection — Hydration 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!

NextJS Wallet Connection — Solution 1 Hydration Error 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>
);
};
NextJS Wallet Connection — Optimized Solution For Hydration Error Fixed

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.

--

--

Manny

DevRel Engineer @ Berachain | Prev Polygon | Ankr & Web Application / Full Stack Developer