Enforcing Ethereum Mainnet in a Wagmi + Next.js App 🎛️
Creating dApps restricted to Mainnet using Wagmi and Next.js

I'm Stephen, a Senior Frontend Engineer with over 5 years of experience, passionate about crafting innovative and user-centric web applications. Proficient in HTML, CSS, JavaScript, TypeScript, Vue.js, and React, I lead cross-functional teams to seamlessly integrate frontend solutions with robust backend infrastructures. Committed to continuous learning and problem-solving, I hold a B.Tech in Computer Science from Federal University of Technology, Owerri and thrive in dynamic environments. Connect with me on linkedin or explore my github for a closer look at my projects and contributions.
Earlier this year, while I was building out AreaDotClub, I wanted a simple guarantee: to enforce Ethereum Mainnet on all pages, such that if a user switches chain from their wallet extension, it shows a popup or a page, alerting them that Area requires only Ethereum Mainnet. Not “warn later,” not “silently fail”—just a clear, friendly gate with a one-click fix. That led me to write MainnetEnforcer, a thin guard wrapped around children that checks the current chain and steers users back to Mainnet when they’re not there.
What the component does
Detects current chain: Uses
useChainIdfrom Wagmi.Offers safe switching: Uses
useSwitchChainto move the wallet to Mainnet with one click.Prevents hydration issues: Gates UI with isMounted to avoid SSR/CSR mismatches in Next.js.
Delivers clean UX: Shows a friendly message and a loading state while switching.
What I thought would be easy (and wasn’t)
Setting up WAGMI config for this specific feature: The first step to implementing this feature is to configure chains in the wagmi config.
import { mainnet } from "wagmi/chains"; import { createConfig, http } from "wagmi"; import { getDefaultConfig } from "connectkit"; const config = createConfig( getDefaultConfig({ chains: [mainnet], transports: { [mainnet.id]: http(""), // rpc url here }, // ...other configs }), );The problem with this setup, though, is that no matter what chain a user switches to, it won’t detect that change. You must be wondering why this happens, well the reasons are;
When you configure only
mainnetingetDefaultConfig({ chains }), Wagmi sets the initial chain to the first configured chain and treats any other wallet network as “unsupported.”For unsupported chains, Wagmi does not propagate the
chainChangedupdate intouseChainId. It keeps returning the initial chain (Mainnet), so our<MainnetEnforcer />thinks we are still on Mainnet and doesn’t react.With multiple chains configured, the wallet’s new chain is recognised and
useChainIdupdates accordingly, so the enforcer sees the mismatch.
I was able to fix this by importing all chains from wagmi/chains and filtering out mainnet from the imported chains before appending it to the chains array within the config.
import { mainnet } from "wagmi/chains";
import * as chains from "wagmi/chains";
import { createConfig, http } from "wagmi";
import { getDefaultConfig } from "connectkit";
const allChains = Object.values(chains)
const config = createConfig(
getDefaultConfig({
chains: [mainnet, ...allChains.filter(chain => chain.id !== mainnet.id)],
transports: {
[mainnet.id]: http(""), // rpc url here
},
// ...other configs
}),
);
- Hydration flicker caused by SSR vs CSR mismatch: The first attempt rendered network-dependent UI immediately. In Next.js,
useChainIdisn’t consistent server-side vs client-side, so the page would first render without a wallet context, then “flip” after hydration. That caused a jarring flicker and occasional hydration warnings.
I fixed this with a tiny “mount gate”: render nothing chain-dependent until the component is mounted.
useEffect(() => {
setIsMounted(true)
},[])
if (isMounted && (chainId !== mainnet.id)) {
// content to render here...
}
The shape of the solution
After bypassing the two issues i faced earlier, the only thing left was to hook up a descriptive UI with a button that allows users to switch back to mainnet.
Why this approach works in production
- It avoids SSR pitfalls with a single
isMountedflag.
- It de-risks the UX with clear copy and a deterministic call-to-action.
- It respects wallet state (don’t show WalletEnforcer content when there’s nothing to switch).
Wrapping it up
What started as “just show Mainnet” turned into a small but meaningful tour through dapp ergonomics: avoiding hydration flicker, respecting wallet state, and surfacing honest feedback while switching. The last wrinkle—Wagmi treating non-configured networks as unsupported—was the quiet culprit behind “why doesn’t useChainId change?” I fixed it by listing more chains purely for detection, then letting the guard enforce Mainnet at the UX layer.
If you want to see how this works in production, you can check out Area (use code: GENESIS) ✨
