dayhaysoos/use-shopping-cart

Usage of cartCount causes an SSR mismatch error in Next.js

thorsten-stripe opened this issue · 14 comments

I'm not sure why this is happening:
image

cartCount renders correctly in the <p> tag but for setting the disabled value on the button it doesn't: https://github.com/thorsten-stripe/next.js/blob/thor/stripe/add-use-shopping-cart-example/examples/with-stripe-typescript/components/CartSummary.tsx#L45

I did some digging and traced it down to the react-storage-hooks module: soyguijarro/react-storage-hooks#17 . Maybe they have seen that before and have an idea.

So @thorsten-stripe the official way to fix it would be to modify react-storage-hooks to not load from LocalStorage on the initial client-side render, but instead right afterward right?

Oh nvm looks like you're really on top of all this and you've already put out a PR for this. Much appreciated.

So @thorsten-stripe the official way to fix it would be to modify react-storage-hooks to not load from LocalStorage on the initial client-side render, but instead right afterward right?

@ChrisBrownie55 Yes, I believe that is the only way to avoid this discrepancy, however it's not great, because you have an initial render with the default state and then an immediate re-render with the localStorage state, which can result in a flicker when the page loads.

One potential solution would be to initially return null until we had the chance to check the localStorage, that way you can employ conditional rendering to avoid the "flicker", but none of this feels great for non-SSR applications :(

@thorsten-stripe it looks like this has been fixed here:

soyguijarro/react-storage-hooks#8

You think you can try testing the upgraded version of react-storage-hooks and see if the bug is gone?

Unfortunately it didn't fix the hydration warning issue, see soyguijarro/react-storage-hooks#18 (comment)

So what I'm seeing here is that we need to display the default (a.k.a. empty) values for data stored in local storage and provide a status indicator for the data as well.

I think this could look something like the following:

export function useLocalStorageReducer(key, reducer, initialState) {
  const [state, dispatch] = useReducer((state, action) => {
    switch (action.type) {
      case 'value updated':
        return { status: 'loaded', value: action.storageValue }
      case 'loading started':
        return { status: 'loading', value: action.initialState }
      default:
        return state
    }
  })

  const dummyStorage = {/* ... */}
  const storageValue = useStorageReducer(/* same stuff but make initial state: */ null)
  
  useEffect(() => {
    if (storageValue === null)
      dispatch({ type: 'loading started', initialState })
    else
      dispatch({ type: 'value updated', storageValue })
  }, [initialState, storageValue])
}

This would also make it so that when isClient is false it will always be status: 'loading'. Although, I don't think this accounts for window.localStorage being inaccessible, like if it's disabled, as we currently aren't handling that anyway.

@thorsten-stripe was this taken care of? Do we need to update this lib?

It looks like setting suppressHydrationWarning is the only option until soyguijarro/react-storage-hooks#20 is merged. Once it is, we'll need to update our dependency. For now, I'll just tag this as "On Hold".

Fwiw I've been running into this whilst using material-ui badges too - things like

{cartCount > 0 ?
   <IconButton href="/cart">
      <Badge badgeContent={cartCount || 0} color="secondary" suppressHydrationWarning>
         <ShoppingCartOutlined />
      </Badge>
   </IconButton>
: <></>
}

which not only triggers the hydration warning, but also a mismatched className on the Badge being invisible (server side) or not (client side).

My only solution, for now, has been to go with the premise that the shopping cart interaction will be client-side only, and wrap any use of cartCount in <NoSSR>

I'd say that that's a fair assumption as all of the cart information has to be loaded from either LocalStorage or with loadCart. However, @benwwalker, I feel like the hydration warning is triggered in your above example because you're still conditionally rendering different values to the DOM based on cartCount. You might try something like:

// "hidden" would be like "display: none" or something
<IconButton href="/cart" className={cartCount > 0 ? '' : 'hidden'} suppressHydrationWarning>
  <Badge badgeContent={cartCount || 0} color="secondary">
    <ShoppingCartOutlined />
  </Badge>
</IconButton>

If you still have the same issue with suppressHydrationWarning then <NoSSR> might be the only way at the time.

Other than that, we're still just waiting on soyguijarro/react-storage-hooks#20 to be merged to definitively solve this issue.

Don't think this is an issue anymore, let me know if I'm wrong tho