react-use icon indicating copy to clipboard operation
react-use copied to clipboard

useLocalStorage mismatches server render

Open saiichihashimoto opened this issue 6 years ago • 15 comments

Any server rendering solution won't have access to a browser's local storage, so it will default to the initialValue. Since useLocalStorage will use the actual local storage value on first render, this will immediately end up in a react hydration mismatch anytime the local storage value is not equal to the initialValue.

saiichihashimoto avatar Oct 24 '19 00:10 saiichihashimoto

What would be possible solutions?

streamich avatar Oct 26 '19 18:10 streamich

Have the state be some sane default (maybe specified by the user, maybe null) and then fill it with local storage in a useEffect, which only happens on the browser. It would be up to the user how to handle the default.

saiichihashimoto avatar Oct 26 '19 18:10 saiichihashimoto

I believe currently it returns a default value (undefined by default) on the server: https://github.com/streamich/react-use/blob/master/src/useLocalStorage.ts#L9

And sets that value in useEffect hook on the browser: https://github.com/streamich/react-use/blob/master/src/useLocalStorage.ts#L32

Do you mean it should return undefined on the browser, too?

streamich avatar Oct 26 '19 20:10 streamich

At least on the first render, yeah. If you console output the value, you can see what I mean. In the server, it'll be the initial value, but on the browser the first render will be whatever is in local storage, which means the hydration won't match.

saiichihashimoto avatar Oct 27 '19 00:10 saiichihashimoto

Maybe we can add an option, if true, that would on the browser return the default value first, and only then re-render with a real value?

streamich avatar Oct 27 '19 17:10 streamich

That would solve my issues!

saiichihashimoto avatar Oct 27 '19 20:10 saiichihashimoto

You can also try disabling SSR for the component that is using local storage state.

In Next.js, for instance, you could do this:

import dynamic from 'next/dynamic'

const DynamicComponentWithNoSSR = dynamic(
  () => import('../components/hello3'),
  { ssr: false }
)

https://nextjs.org/docs/advanced-features/dynamic-import#with-no-ssr

dlbnco avatar Aug 04 '20 22:08 dlbnco

@dlbnco this avoids the problem by removing all the benefits of SSR for that component, which seems like a regression rather than a progression of features. The solution that doesn't disable SSR benefits to use the hook seems like the way to go.

saiichihashimoto avatar Aug 04 '20 23:08 saiichihashimoto

@saiichihashimoto that is true, although you could also use the loading option to load the same component (without state from localstorage) as a placeholder on SSR. Local storage is not available on the server anyway, so I think it would not cause any regression.

https://nextjs.org/docs/advanced-features/dynamic-import#with-custom-loading-component

dlbnco avatar Aug 05 '20 17:08 dlbnco

@dlbnco that's one way to solve it. What I'm claiming is that the hook's issue can be isolated to the hook, rather than requiring the entire component be blocked out via SSR. This lets the implementer decide how they want to handle the situation where the value isn't loaded on the frontend rather than the blanket solution where they don't show the component, since the component itself has no issues showing.

saiichihashimoto avatar Aug 05 '20 18:08 saiichihashimoto

SSR is well supported in upcoming hook of @react-hookz/web react-hookz/web#43

xobotyi avatar May 15 '21 15:05 xobotyi

Quick update: SSR is now fully supported by the useLocalStorageValue and useSessionStorageValue hooks included in @react-hookz/web! For reference, here are the docs on useLocalStorageValue and useSessionStorageValue.

If you are thinking of migrating, we have created a handy react-use to @react-hookz/web migration guide. Hope it helps!

JoeDuncko avatar Sep 02 '21 00:09 JoeDuncko

This works for me.

import { useRendersCount, useLocalStorage } from 'react-use'

const defaultValue = 'default'

const ShowValue = () => {
  const [value, setValue] = useLocalStorage('value', defaultValue)
  const isFirst = useRendersCount() === 1;

  return (
    <>
      <button type="button" onClick={() => setValue('new value')}>Set value </button>
      {isFirst ? defaultValue : value}
    </>
  )
}

alnorris avatar Nov 11 '22 17:11 alnorris

I use useLocalStorage in nextjs 13, and it still throws error

medmin avatar Dec 02 '22 22:12 medmin

@streamich Would you like a PR on this one ? It is indeed quite annoying, I am using Next 13 too and it creates big errors

lechinoix avatar Apr 17 '23 15:04 lechinoix