Skip to content

Make useStore compatible with client-side hydration of SSR components#10

Merged
ai merged 6 commits intonanostores:mainfrom
jmurty:fix-hydration-with-ssr
Mar 14, 2026
Merged

Make useStore compatible with client-side hydration of SSR components#10
ai merged 6 commits intonanostores:mainfrom
jmurty:fix-hydration-with-ssr

Conversation

@jmurty
Copy link
Contributor

@jmurty jmurty commented Mar 9, 2026

Hi, please consider improving support for projects that use SSR / ISR pages with client-side navigation, which can currently get hydration errors without a work-around like this.

Add initial option to return same value as used for SSR until client-side hydration is complete

When a value is provided for the new optional initial option, that value is returned by useStore instead of the store's current value until hydration is completed on the client, as detected by completion of the first-run useEffect.

This initial option avoids hydration errors on projects that use Server Side Rendering (SSR) combined with client-side navigation and rendering, where the client-side store value can get out of sync with the value in the server-rendered page especially if the server page is cached in any way (ISR).

jmurty added 3 commits March 9, 2026 01:04
…t-side hydration is complete

When a value is provided for the new optional `initial` option, that
value is returned by `useStore` instead of the store's current value
until hydration is completed on the client, as detected by completion
of the first-run `useEffect`.

This `initial` option avoids hydration errors on projects that use
Server Side Rendering (SSR) combined with client-side navigation and
rendering, where the client-side store value can get out of sync
with the value in the server-rendered page especially if the server
page is cached in any way (ISR).
@jmurty
Copy link
Contributor Author

jmurty commented Mar 9, 2026

This change currently increases the package size by 30 bytes to 952, which it might be possible to reduce?

And if this approach of adding an initial option is acceptable, I can make a similar PR on https://github.com/nanostores/react as well. I'm starting with the Preact version because that's what our website uses, so I can test it more easily with a real use-case.

@ai
Copy link
Member

ai commented Mar 9, 2026

30 bytes is not a so big problem. I do not like the idea of duplicating initial value, it looks like a hack which lead to duplication.

Also, as I know React has the same issue. Maybe we can add Store#init with initial value?

@jmurty
Copy link
Contributor Author

jmurty commented Mar 9, 2026

Thanks for the feedback. And yes, I agree having to pass in initial value – which for correctness should always exactly match the store's initial value – is pretty hacky.

Keeping the initial value in the store itself then referring to it in the useStore hook would be much cleaner. I can make a PR for this over on https://github.com/nanostores
Would Store#init be your preferred approach, or something like Store#getInitial() so the API is closer to Store#get()?

My only concern with using a Store#init value or similar from the store is that the useStore hook would (presumably) always use it to delay return of the current store value until after hydration, not only when the user opts into that behaviour. This would be a change in behaviour that benefits projects using SSR but might be a regression for other kinds of projects?

jmurty added a commit to jmurty/nanostores that referenced this pull request Mar 9, 2026
…al value

Make the store's initial value available via the `init` attribute.

This is intended mainly for internal use by integrations like React
and Preact that need to return the initial value during the rendering
lifecycle.

See:
- nanostores/preact#10
- nanostores/react#38
ai pushed a commit to nanostores/nanostores that referenced this pull request Mar 10, 2026
…al value (#390)

* Add `Store#init` low-level attribute to always return a store's initial value

Make the store's initial value available via the `init` attribute.

This is intended mainly for internal use by integrations like React
and Preact that need to return the initial value during the rendering
lifecycle.

See:
- nanostores/preact#10
- nanostores/react#38

* Add tests for `Store#init` attribute, and increase size limit to 270b / 804b for Atom / Popular respectively

* Improve tests for map to ensure `setKey` doesn't change value of `init`
jmurty added 2 commits March 13, 2026 00:46
This requires an upcoming release of nanostores including the new
`Store#init` attribute from PR:
nanostores/nanostores#390
@jmurty
Copy link
Contributor Author

jmurty commented Mar 12, 2026

NOTE: Tests are expected to fail for recent updates to this PR until Store#init is available.

@jmurty jmurty changed the title Add initial option to fix hydration errors with SSR pages Make useStore compatible with client-side hydration of SSR components Mar 12, 2026
I haven't found a way to detect hydration errors when a component
with updated state data is hydrated over "server" rendered markup,
but this change brings the tests closer to the related tests in
nanostores/react which better demonstrate the original problem and
the fact this PR solves it.
@ai ai merged commit 4062d4f into nanostores:main Mar 14, 2026
@ai
Copy link
Member

ai commented Mar 14, 2026

@jmurty can you provide some arguments why we always should use initial data on the server?

On React we follow their official API. But here seems like we are adding extra layer of unexpected complexity.

For instance, I can assume that some users can use @nanostores/preact to do the simple server rendering (without hydration and other magic, just like a server-side template). For them the usage will be completely broken.

Also, am I right that on the client-side first it will be rendered with initial value, then onEffect will be called and only then component will be re-rendered with actual value (if store was changed before component render)?

@jmurty
Copy link
Contributor Author

jmurty commented Mar 14, 2026

Hi @ai you're right that always returning the initial data is a change in behaviour and potentially a breaking change. It's following the React way of doing things, but this isn't necessarily the also right way for Preact. I worried about this in an earlier comment, but not clearly.

I think we shouldn't always use initial data for the Preact hook, on server-side or on client-side. And I hadn't even thought of the case for server-only rendering where useEffect will never fire and the value will never get updated.

We should probably make users opt-in to the new SSR behaviour. Perhaps add an ssr: true option to turn on the new behaviour, and leave it off otherwise?

@jmurty
Copy link
Contributor Author

jmurty commented Mar 15, 2026

@ai I'll take another go at this, with explicit opt-in required to enable SSR support (and its quirks) via a new ssr option: #12

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants