Closed turingmachine closed 1 year ago
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: https://github.com/astoilkov/use-local-storage-state/issues/54#issuecomment-1261800452.
I might add this to the documentation as it's creating a lot of confusion.
I'm having this issue with version 18 as well, using react 18 without StrictMode
. Any ideas how to solve?
Hmm. You are right. I think I actually misread the issue. I will take a look at this in the next few days.
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!
@turingmachine Can you join in? Did you figure your issue out?
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]);
@dalazx Hey, is this again with React 18?
Can you do a reproduction of this? This will help me a lot!
Thanks.
@astoilkov here you go https://github.com/dalazx/use-local-storage-state-demo
hope this helps
overall I think the culprit is useSyncExternalState
. I tried to debug it and saw that getSnapshot
is not getting called during hydration.
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.
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.
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).
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:
defaultValue
during SSRdefaultValue
during the first renderdefaultValue
during the second render (since there is no value in LS) or get the actual value during the second renderSo 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.
defaultValue
and isServerRender
is TRUEdefaultValue
and isServerRender
is TRUEdefaultValue
or actual value and isServerRender
is FALSEBut 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 emptyserverDefaultValue
used during 1st render and SSR (this can be optional with fallback to defaultValue
)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.
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?