-
|
What is the best way to avoid a Hydration Mismatch on the first page load using makePersisted? Ideally, makePersisted would wait until the dom isHyrated before it changes its default to the value stored in storage. This renders fine on the first page load: const [getUsers, setUsers] = makePersisted(
createSignal<string[]>([]),
{storage: globalThis.sessionStorage, name: 'users'}
)<button onClick={()=> {setUsers(['user1'])}}>Add</button>
<For each={getUsers()}>
{user=> <>
<button class="btn join-item">
{user}
</button>
</>}
</For>Now, refresh the browser and the server will render no users, however, the 1st render on the client will render one user which causes the Hydration Mismatch. I'll make a reproduction if you think I'm in error, I'm summarizing code. |
Beta Was this translation helpful? Give feedback.
Replies: 5 comments 2 replies
-
|
I' am afraid this is failure by design. The hydration expects the same element to render on the server and the client, but the client has the users from session storage faster than the server, which leads to a mismatch. Our primitive behaves exactly as specified, but that becomes an issue with hydration. How to solve this? In theory, you could block the rendering until hydration is done. Let me think about how to do this. As a workaround, you could use a memo to return an empty array until hydration. We have a nifty little lifecycle primitive you might find helpful. I might add an option where you can add an accessor that will let us block updates until truthy. Update: I added the option. Once the PR is merged, you can use isHydrated from lifecycle inside the options to avoid hydration errors. |
Beta Was this translation helpful? Give feedback.
-
|
Hey, guys! This thing seems to move forward quite slowly, so I thought I'd share my solution. It might help some people who land here serching for a solution to our common headache. So basically it consists of 2 basic parts:
This probably won't help with improving // safe-persisted.mts
import { createSignal } from "solid-js";
import { makePersisted, AsyncStorage } from "@solid-primitives/storage";
// Just so TS in Codesandbox doesn't complain
const ls = (typeof localStorage !== "undefined" && localStorage != null) ? localStorage : null;
// The async storage object that exists everywhere
const asyncLocalStorage = {
getItem(key: string): Promise<string | null> {
return Promise.resolve().then(() => ls?.getItem(key) ?? null);
},
setItem(key: string, value: string): Promise<void> {
ls?.setItem(key, value);
return Promise.resolve();
},
removeItem(key: string): Promise<void> {
ls?.removeItem(key);
return Promise.resolve();
},
} as AsyncStorage;
// Use this async storage with makePersisted
export default function makeSafePersisted(initVal: any, options?: object) {
return makePersisted(createSignal(initVal), {
...options,
storage: asyncLocalStorage,
});
}
I made a test/demonstration codesandbox: 🖥️ ......... https://codesandbox.io/p/devbox/jovial-blackwell-yh27tq You can check out the complete code. |
Beta Was this translation helpful? Give feedback.
-
|
It seems we only need to delay initialization to after hydration. That's something that we can do without making everything async. |
Beta Was this translation helpful? Give feedback.
-
|
Yes, that would be ideal. However, I'm not sure if it should be the default behavior. In this case the signal would render and hydrate with the initial value, and then would receive an update. This is how I do it manually with localStorage. But there might be a case when somebody needs it to render immediately with the persisted data in a client-only component. So I think this should be an option. |
Beta Was this translation helpful? Give feedback.
-
|
I also want to point out a potential race condition I ran into while working on the localStorage persistence. Consider a version of const [signal, setSignal] = makePersisted(createSignal(), {
storage: localStorage,
initAfterHydration: true
});The problem is that setSignal() can be called:
If that happens, the persisted value in localStorage may be overwritten or cleared before it’s ever read. by some other code that also runs after hydration or during render. In this case the persisted data in the storage ( In fact, this is the case when using In my case I solved it by ignoring any I don't know if this is too much an edge case, but I think it's not. Any delayed initialization introduces a time window where writes can happen before reads, and that can lead to unexpected data loss. I think this should also be considered when creating an option for delayed initialization. |
Beta Was this translation helpful? Give feedback.
I' am afraid this is failure by design. The hydration expects the same element to render on the server and the client, but the client has the users from session storage faster than the server, which leads to a mismatch. Our primitive behaves exactly as specified, but that becomes an issue with hydration.
How to solve this? In theory, you could block the rendering until hydration is done. Let me think about how to do this. As a workaround, you could use a memo to return an empty array until hydration. We have a nifty little lifecycle primitive you might find helpful. I might add an option where you can add an accessor that will let us block updates until truthy.
Update: I added the option.…