Skip to main content

Command Palette

Search for a command to run...

Enforcing Ethereum Mainnet in a Wagmi + Next.js App 🎛️

Creating dApps restricted to Mainnet using Wagmi and Next.js

Updated
•3 min read
Enforcing Ethereum Mainnet in a Wagmi + Next.js App 🎛️
S

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 useChainId from Wagmi.

  • Offers safe switching: Uses useSwitchChain to 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)

  1. 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 mainnet in getDefaultConfig({ 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 chainChanged update into useChainId. 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 useChainId updates 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
      }),
    );
  1. Hydration flicker caused by SSR vs CSR mismatch: The first attempt rendered network-dependent UI immediately. In Next.js, useChainId isn’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 isMounted flag.
  • 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) ✨