solid-primitives icon indicating copy to clipboard operation
solid-primitives copied to clipboard

storage: solve hydration issue

Open atk opened this issue 11 months ago • 14 comments

In order to simplify avoiding hydration issues, makePersisted now accepts isHydrated from the lifecycle package as an option to delay the initialization from storage until the parent is hydrated; since this way both server and client will have the same default on initial rendering, this will make them render the same, thus no longer causing hydration mismatch errors.

Another solution would be to sync the storage with the server and use makePersisted on the server, too.

This addresses https://github.com/solidjs-community/solid-primitives/discussions/737

atk avatar Jan 15 '25 12:01 atk

🦋 Changeset detected

Latest commit: b3a785b32cd70cbb5e63d99530f648441fda94f8

The changes in this PR will be included in the next version bump.

This PR includes changesets to release 1 package
Name Type
@solid-primitives/storage Minor

Not sure what this means? Click here to learn what changesets are.

Click here if you're a maintainer who wants to add another changeset to this PR

changeset-bot[bot] avatar Jan 15 '25 12:01 changeset-bot[bot]

Why not use isHydrated (or it's implementation directly since it's just some "is hydrating" checks and onMount) in the primitive by default? The primitives should be usable with ssr without needing to enable that support.

if (!isServer && sharedConfig.context) {/*delay init*/}
else {/*init immediately*/}

This is what all the primitives what read something from the dom like createWindowSize do. Even though window.innerWidth can be read immediately, it is delayed when hydrating.

thetarnav avatar Jan 15 '25 19:01 thetarnav

We do not know if the server and the client will have access to the same data on their storage. You could also using the sync API or cookieStorage to keep them in sync, so I do not want to make it a default, because then the delayed initialization on the client could introduce hydration mismatches if the fallback is hydrated.

atk avatar Jan 16 '25 07:01 atk

Is there a way to ensure consistency automatically? I don't mean sync. Just a way for the server to "tell the client" to use a fallback or not? Like this seems like something that the primitive should handle on it's own. Maybe by using resources? Which also have a onHydrated callback option that could be used to handle hydration.

thetarnav avatar Jan 16 '25 21:01 thetarnav

Is there a way to ensure consistency automatically?

If this is out of scope of this pr, and you just want to introduce a temporary fix, then I don't mind. But the api could be less convoluted imo. For example as a option flag:

makePersisted(init, {useInitDuringHydration: true})

As opposed to telling users to pull another dependency. It's not like isHydrated can have multiple implementations to allow for an interface.

thetarnav avatar Jan 16 '25 21:01 thetarnav

There is no way for us to know on either server or client what data the other side will have unless we either sync them manually or deliberately let them be out of sync.

It's not like isHydrated can have multiple implementations to allow for an interface

No, but one could also choose this to block initialization until any moment of your choice.

atk avatar Jan 17 '25 05:01 atk

OK, I switched to your suggested solution.

atk avatar Jan 19 '25 12:01 atk

looks good although there are some “isHydrated” leftovers in the comments and readme also if you

No, but one could also choose this to block initialization until any moment of your choice.

If you think this valuable, delayInit could accept a function matching onMount type ({delayInit: onMount}), which would give the flexibility while keeping the api simple. But yeah, no idea if that’s necessary.

thetarnav avatar Jan 19 '25 14:01 thetarnav

The new delayInit name is great. I was thinking defer, but "delay" helps make this unique. Doesn't isHydrated happen before onMount though? If it isHyraded v. onMount makes any real difference, esp. re-rendering, I suggest to choose well if one always works or go back to the original PR with a function passed in but with the name changed to delayInit.

And fyi, good idea in your example @thetarnav, but I don't think onMount returns a value (I'm not on the latest version though). We might want to keep isHydrated in the docs at least.. I'm making some assumptions here though I don't know all the details about the difference in onMount and isHydrated.

function onMount(fn) {
  createEffect(() => untrack(fn));
}

jcalfee avatar Jan 19 '25 15:01 jcalfee

@jcalfee

Doesn't isHydrated happen before onMount though?

No, isHydrated is pretty much onMount as a signal with a hydration check: https://github.com/solidjs-community/solid-primitives/blob/main/packages/lifecycle/src/index.ts#L35

good idea in your example @thetarnav, but I don't think onMount returns a value

why would it return a value?

currently the logic is:

const initialize = () => {...}
if (options.deferInit) {
    onMount(initialize)
} else {
    initialize()
}

and my idea would look something like this

const initialize = () => {...}
if (options.deferInit) {
    options.deferInit(initialize)
} else {
    initialize()
}

thetarnav avatar Jan 19 '25 16:01 thetarnav

I was referring to this:

If you think this valuable, delayInit could accept a function matching onMount type ({delayInit: onMount}),

If onMount did return a value that would be useful.

jcalfee avatar Jan 19 '25 16:01 jcalfee

options.deferInit(initialize) is somewhat of a foot gun IMO. If you don't read the instructions carefully, you'll easily break things. I think a boolean i option s fine for now. We can iterate if necessary.

atk avatar Jan 19 '25 17:01 atk

any update about this?

aguilera51284 avatar Jul 27 '25 00:07 aguilera51284

There's still a pending review.

atk avatar Jul 27 '25 17:07 atk