wagmi: [bug] using WAGMI with SSR (Next JS) is causing styling issues

Is there an existing issue for this?

  • I have searched the existing issues

Package Version

0.3.5

Current Behavior

(copying from a discussion we previously created)

After bumping wagmi to 0.3.5 in our project, we started to face a number of styling issues (here’s an example 😆) due to a mismatch between the server-rendered HTML and the first render on the client side.

After some investigation, I discovered that this was due to hooks like useAccount returning isLoading as true during SSR, but as false on the client. Here’s an example of the return value of useAccount during SSR and on the client:

Before we upgraded to 0.3, the SSR and client output was consistent on first render. In this case it returned:

[
  {
    "loading": false
  },
  null
]

A few questions:

  • Why is loading true on the server side. In my tests, it’s also true when autoConnect is set to false?
  • Is there a recommended pattern for handling SSR in wagmi? Currently we’re manually patching this issue in many places, but I would prefer to help with a fix in wagmi 😄

I would guess that anyone using Next.js + WAGMI + Stitches will face a similar issue to us.

Expected Behavior

No response

Steps To Reproduce

Styling bug isn’t visible in the repo to reproduce, but mis-match between client/server output is highlighted in a console error.

Link to Minimal Reproducible Example (CodeSandbox, StackBlitz, etc.)

https://github.com/smhutch/wagmi-sandbox/blob/main/README.md

Anything else?

No response

About this issue

  • Original URL
  • State: closed
  • Created 2 years ago
  • Reactions: 4
  • Comments: 49 (23 by maintainers)

Commits related to this issue

Most upvoted comments

Hey!

Sorry, I completely missed the discussion thread, thanks for opening an issue here (definitely easier to keep track).

Yeah, there are a couple of nuances of server/client hydration in wagmi. The main culprit of these hydration issues is that wagmi now caches responses and persists them to local storage (which is obviously not accessible on the server). This is not just a wagmi issue, but also for any library that persists state to local/session storage. Currently, there is no first-class solution for SSR in wagmi (but this is something that is on the roadmap – perhaps using cookies).

There are a couple of workarounds to resolve these hydration issues right now that have their trade-offs:

  1. You can use a useIsMounted Hook and render your content after hydration. This can be seen in our examples. The trade-off here is content nested within the isMounted variable is not visible on the server.
function Page() {
  const isMounted = useIsMounted()

  if (!isMounted) return null
  return (
    ...
  )
}
  1. Otherwise, you can guard wagmi data in an isMounted variable. The trade-off here is that it’s a bit annoying to have isMounted variables floating everywhere.
function Example() {
  const isMounted = useIsMounted();
  const { data } = useAccount();
  
  return (
    ...
    { isMounted && <div>{data?.address}</div> }
    ...
  )
}

3 (NOT RECOMMENDED). You can turn off local storage persistance completely, which will resolve everything, but comes with the trade-off that you lose data persistence on page load (and will consequently see FOUW – flash of unconnected wallet, balances, contract data, etc)

const client = createClient({
  ...
  persister: null
})

Why is loading true on the server side. In my tests, it’s also true when autoConnect is set to false?

This is probably a bug on the wagmi side - I’ll take a look!

I think it’s okay to modify it like this.

import { useEffect } from 'react';
import { createClient } from 'wagmi';

const client = createClient({
  autoConnect: false,
  connectors,
  provider
})

export default function App({ Component, pageProps }: AppPropsWithLayout) {
  useEffect(() => {
    client.autoConnect();
  }, [])

  return (
    ...
  )
}

Filling the codebase with the isMounted is really annoying for me. And I think I found the workaround solution that just works.

For anyone who is facing the “Hydration failed” problem, maybe you could give it a try.

const client = createClient({
  autoConnect: false,
  provider,
  webSocketProvider,
})
  const { connectAsync, connectors } = useConnect()
  const client = useClient()

  const [isAutoConnecting, setIsAutoConnecting] = useState(false)

  useEffect(() => {
    if (isAutoConnecting) return
    if (isConnected) return

    setIsAutoConnecting(true)

    const autoConnect = async () => {
      const lastUsedConnector = client.storage?.getItem("wallet")

      const sorted = lastUsedConnector ?
        [...connectors].sort((x) =>
          x.id === lastUsedConnector ? -1 : 1,
        )
        : connectors

      for (const connector of sorted) {
        if (!connector.ready || !connector.isAuthorized) continue
        const isAuthorized = await connector.isAuthorized()
        if (!isAuthorized) continue

        await connectAsync({ connector })
        break
      }
    }

    autoConnect()
  }, [])

I found that the autoConnect feature is the root cause, so I handle that myself. Ref: interal autoconnect function

Also having this problem, I read that you wanted to explore this when next13 arrived, and since it is now in beta, I was wondering if you have an ETA on this feature? Kinda sucks to have to use workarounds in my apps, especially now since web3modal changed to the wagmi hooks in v2 (the previous hooks didn’t face this problem) and a lot of extra users will be facing these problems now.

Thanks in advance! @jxom

This is not fixed, the issue should be kept open

Hey @tmm 👋

Any chance we can get this re-opened? This issue isn’t fixed quite yet.

This is not a wagmi issue that needs fixing. You should follow SSR best practices for your site. Once the Next.js app directory is out of beta, we will look into adding a first-class integration in wagmi to make this easier.

An easy way to save developers from having to do isMounted manually from every callsite (i.e. “SSR hygiene”) could be to add a third non-boolean option for autoconnect, e.g.

export const client = createClient({
  autoConnect: "onlyAfterHydration",
  connectors,
  provider,
})

In the meantime, developers will probably have to put wrapper hooks around useAccount, etc that return disconnected during hydration

(FWIW, autoconnect has always happened to be slow enough for me that I’ve not run into this issue)

For the isMounted fix, I found that neither the useIsMounted hook from usehooks-ts nor the useMountedState from react-use worked as both use a ref internally that is only set to true inside a useEffect, which runs after the rest of the content has finished rendering on the client. And since it’s a ref, updating it doesn’t trigger a rerender itself, so nothing shows up.

To get it to work I had to write a simple useIsMounted hook that uses setState instead so that a rerender is triggered:

import { useState, useEffect } from 'react'

export function useIsMounted(): boolean {
    let [isMounted, setIsMounted] = useState(false)

    useEffect(() => {
        setIsMounted(true)
    }, [])

    return isMounted
}

I was also not able to fix the hydration error with "wagmi": "^0.6.6" using either isMounted or persister: null.

What is currently working for me is useEffect, like so:

const { address, connector, isConnected } = useAccount()

const [connected, setConnected] = useState(false)

useEffect(() => {
  setConnected(isConnected);
}, [isConnected]);

then

My address is: {connected && address}

Shout out to @ottodevs for this workaround. Hope that helps someone!

While the overall issue is “rendering a component that uses web storage to change its appearance fails hydration errors” which is not wagmi-specific, the part of wagmi that is causing the issue (caching data) I think could be extended to allow a new option for this situation:

There’s already a cacheTime parameter for most wagmi hooks, but the documentation says it defaults to zero. So I presume the internal logic is that on initial call, it serves any stored data it has, then because the stored data is stale (past zero milliseconds old) fetches new, and replaces the stored data with the new value (possibly the same value, just new cache time). However I’m not seeing the status nor isLoading/isFetching values update after page load…?

A solution to this scenario could be a new parameter to specify always render with null data once. Namely, if that parameter is set, the first time the hook is called, return data of null, with a status of loading. Then look up to see if there’s cached data. If there is, update data to it (and status of success), triggering a second rendering of the component.

Most wagmi hooks have enabled input parameters, and specify refetch return values that I think could be used for another workaround style, but I don’t see documentation on them (or what the difference is between isLoading and isFetching, and between isFetched and isFetchedAfterMount); updated documentation and examples on those could possibly help developers code appropriately for this situation using those tools.

Providing additional documentation on caching I pulled out into a separate request: https://github.com/wagmi-dev/wagmi/discussions/3017

I think it’s okay to modify it like this.

import { useEffect } from 'react';
import { createClient } from 'wagmi';

const client = createClient({
  autoConnect: false,
  connectors,
  provider
})

export default function App({ Component, pageProps }: AppPropsWithLayout) {
  useEffect(() => {
    client.autoConnect();
  }, [])

  return (
    ...
  )
}

this is nasty and i love it

Following the updated example nextjs app https://github.com/wagmi-dev/wagmi/blob/main/examples/_dev/src/pages/index.tsx, this would cause a brief flash of a blank page when navigating between routes. Does anyone have a workaround/fix for this that doesn’t cause the flash?

You need to make the workaround more precise. For example, instead of hiding the entire app on initial render, just wrap the return values of hooks.

Could you give an example of wrapping return value of a hook? For example if I’m using:

const { address } = useAccount()
const { data } = useContractRead({
    address: CONTRACT_ADDRESS,
    abi: CONTRACT_ABI,
    functionName: "getBalance",
    args: [address],
    enabled: isAuthenticated && Boolean(address),
  });
const _address = useAccount().address
const address = isMounted ? _address : undefined
const { data } = useContractRead({
    address: isMounted ? CONTRACT_ADDRESS : undefined,
    abi: CONTRACT_ABI,
    functionName: "getBalance",
    args: [address],
    enabled: isAuthenticated && Boolean(address),
  });

FWIW I suspect that the inconsistent values on initial render is related to the usage of a global config variable, which will be gone in wagmi@2

@apecollector – we can update the examples.

Update: Done. (https://github.com/wagmi-dev/wagmi/pull/1040)

@imornar first-class SSR support is on the roadmap, but we are holding until we see the Next.js Layouts RFC progress a little more (lots of folks use wagmi and Next.js together and we want to make sure wagmi’s SSR API design is compatible with the future of Next).

In the meantime, you’re welcome to take some of the ideas from https://github.com/wagmi-dev/wagmi/pull/689 — pass server-compatible storage to client and hydrate client state using client.setState — if you are in need of an immediate solution.