Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

React 18: value only is available on 2nd render cycle #56

Closed
turingmachine opened this issue Oct 20, 2022 · 13 comments
Closed

React 18: value only is available on 2nd render cycle #56

turingmachine opened this issue Oct 20, 2022 · 13 comments
Labels
question Further information is requested

Comments

@turingmachine
Copy link

I believe this is by design, but can you elaborate why the value is only available on the 2nd render, which is triggered by use-local-storage-state itself?

import { useState, useRef } from 'react'
import useLocalStorageState from 'use-local-storage-state'

const App = ({ Component, pageProps }) => {
  const [triggerRender, setTriggerRender] = useState('a')

  const renderCounter = useRef(0)
  renderCounter.current = renderCounter.current + 1

  const [test, setTest] = useLocalStorageState('test', {
    defaultValue: 'DEFAULT',
  })

  console.log('cycle:' + renderCounter.current)
  console.log('test value:' + test)

  return (
      <a onClick={() => setTest('TEST')}>set Localstorage state</a>
  )
}

export default App
@astoilkov
Copy link
Owner

Hi, yeah, it's by design. There is no other way to make it work when ssr: true. Take a look at this comment where I explain this: #54 (comment).

I might add this to the documentation as it's creating a lot of confusion.

@dherault
Copy link

I'm having this issue with version 18 as well, using react 18 without StrictMode. Any ideas how to solve?

@astoilkov
Copy link
Owner

Hmm. You are right. I think I actually misread the issue. I will take a look at this in the next few days.

@astoilkov
Copy link
Owner

Hmm. Sorry. I was testing on Code Sandbox and there StrictMode is logging twice in the console (I didn't know that). I now tested this without StrictMode and can't replicate it.

Can you make a reproducible example?

Thanks!

@astoilkov
Copy link
Owner

@turingmachine Can you join in? Did you figure your issue out?

@dalazx
Copy link

dalazx commented Nov 8, 2022

I am observing the same issue which seems to be caused by the hydration process, but in my case there are more than 1 renders before the value is finally returned. The real issue is that it is impossible to tell whether the value is not ready or the value is not there at all. So as a workaround I came up with:

export function useIsLocalStorageReady() {
  const [isReady, setIsReady] = useLocalStorageState("__ready");
  useEffect(() => {
    if (isReady) return;
    setIsReady(true);
  }, [isReady, setIsReady]);
  return !!isReady;
}
const [value, setValue] = useLocalStorageState("value");
const isLocalStorageReady = useIsLocalStorageReady();
useEffect(() => {
  if (!isLocalStorageReady) return;
  // now value can be read
}, [isLocalStorageReady, value]);

@astoilkov
Copy link
Owner

@dalazx Hey, is this again with React 18?

Can you do a reproduction of this? This will help me a lot!

Thanks.

@dalazx
Copy link

dalazx commented Nov 9, 2022

@astoilkov here you go
https://github.com/dalazx/use-local-storage-state-demo

hope this helps
image

overall I think the culprit is useSyncExternalState. I tried to debug it and saw that getSnapshot is not getting called during hydration.

@astoilkov
Copy link
Owner

You are right, the issue is useSyncExternalStore() and how it works internally. I didn't know that until now but TIL something. Here is the explanation.

If I change the code to this:

import {useSyncExternalStore} from "react";

function IndexPage() {
  const value = useSyncExternalStore(() => {
    return () => {}
  }, () => 'client value', () => 'server value')

  console.log(value)

  return (
    <>{value}</>
  );
}

export default IndexPage;

Where the important part is:

const value = useSyncExternalStore(() => {
  return () => {}
}, () => 'client value', () => 'server value')

console.log(value)

The console will log server value and then client value.

This seems like an internal React behavior. It first renders the server value and then the client value (only when it's different).

A more elegant solution to your problem would be:

function useIsServerRender() {
  const isServerRender = useSyncExternalStore(() => {
    return () => {}
  }, () => false, () => true)
  return isServerRender
}

I will update the documentation to clarify this behavior because it's really confusing.

@pitkes22
Copy link

pitkes22 commented Dec 5, 2022

It would be really nice if isServerRender would be returned in extra data (in the object that now has isPersistent ...) since now it is not possible to distinguish if the value is defaultValue because of the first render, or if the value is not set.

@astoilkov
Copy link
Owner

Yep, I agree. I wanted to avoid that (I would have preferred one less value to return) but it seems necessary.


Can people in the discussion share why they needed to know from where the value comes? What will you do with the value?

I'm asking because it's also dangerous to provide that value because the user shouldn't change the rendered HTML (React doesn't allow that).

@pitkes22
Copy link

pitkes22 commented Dec 6, 2022

My use case is that I need to run the side effect only when the user has no value set in localStorage.

So I have a useEffect where I check if the state is null (null is my default value), and if so I execute my side effect. Currently, it is not possible for me to achive this since:

  1. I get defaultValue during SSR
  2. I get defaultValue during the first render
  3. I get defaultValue during the second render (since there is no value in LS) or get the actual value during the second render

So for me there is no way to know if the value is defaultValue because there is no localStorage available or it is just empty.


I agree with you that it can be dangerous to provide this value to the user since the whole point of this two-pass rendering in strict mode is to achieve the same output between SSR and the first render.

So the output between SSR and the first render must always be the same as this.

  1. SSR: The value is defaultValue and isServerRender is TRUE
  2. 1st Render: The value is defaultValue and isServerRender is TRUE
  3. 2nd Render: The value is defaultValue or actual value and isServerRender is FALSE

But from what I can see when I use the hook that you suggested above useIsServerRender it actually works like this.


Alternatively, quite a nice solution can be to instead of providing a boolean value to the user with the value isServerRender the hook may accept two kinds of defaultValue

  • defaultValue used when localStorage is available and empty
  • serverDefaultValue used during 1st render and SSR (this can be optional with fallback to defaultValue)

@astoilkov
Copy link
Owner

I decided not to add a specific property for this use case. I might be wrong but it feels like an edge case.

What I did instead added an explanation for the issue and how to fix it in the readme that also points to this issue.

@astoilkov astoilkov added the question Further information is requested label Jun 20, 2024
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
question Further information is requested
Projects
None yet
Development

No branches or pull requests

5 participants