Skip to content

Enable SSR support in useStore without breaking non-SSR usage#12

Merged
ai merged 5 commits intonanostores:mainfrom
jmurty:fix-regression-for-ssr
Mar 17, 2026
Merged

Enable SSR support in useStore without breaking non-SSR usage#12
ai merged 5 commits intonanostores:mainfrom
jmurty:fix-regression-for-ssr

Conversation

@jmurty
Copy link
Contributor

@jmurty jmurty commented Mar 15, 2026

The fix for SSR usage of useStore in #10 may introduce breaking changes for all other use-cases.

This PR changes the approach, requiring the user to explicitly opt in to the SSR feature so that other use-cases will not be affected. This is necessary because we must let the user decide what state should be used at server render time, based on their use case.

Preact does not have the kind of low-level support for handling server vs client rendering provided by React's useSyncExternalStore

@jmurty
Copy link
Contributor Author

jmurty commented Mar 15, 2026

TODO: Add documentation for the new ssr option, if this turns out to be the right approach.

@jmurty
Copy link
Contributor Author

jmurty commented Mar 15, 2026

I'm realising this may be more complicated than I first thought.

For server-side rendering, it's possible to have a use-case with this sequence: create store with initial value, update store value (e.g. fetch from API), render HTML

Similarly on the client-side: create store with initial value (maybe from server, maybe hard-coded), update store value (e.g. from local storage), hydrate HTML

To do the right thing for SSR support on the client-side, the value returned from useStore at the hydrate HTML step must be the same value used by the server to render HTML. This may not be the init value of the store.

So perhaps instead of a simple ssr option to enable/disable SSR support, we really do need something like the initial option to let users provide the same "initial" value for the hydrate step on the client as was used at render time on the server?

@ai
Copy link
Member

ai commented Mar 15, 2026

I think ssr is not explain the behavior. Maybe it is better to create another use* function.

Do I understand correctly that we are here fixing hydration issue with specific SSR case, when server always return the initial state? For instance, there are SSR implementation when server do not return HTML until all AJAX tasks are finished (so user will not see loaders).

@jmurty
Copy link
Contributor Author

jmurty commented Mar 15, 2026

To begin: I'm sorry for the mess my SSR code contributions have become. I hadn't thought through all the implications and use-cases properly, and getting SSR and hydration right is more difficult than I realised. Please bear with me and I'm sure we can get this solved properly.

Yes, this approach so far only handles the specific SSR case where useStore on the server side always returns the store's initial state, not updated state after changes are made to the store server-side.

Unfortunately, I think the just released nanostores/react update might have the same problem – it solves the simple case where a default value for Store#init is enough, but will cause server-side code to always use the init value when rendering HTML instead of the latest store value.

To handle SSR properly we need to follow the approach of the React useSyncExternalStore() hook's getServerSnapshot option, which returns the "initial snapshot of the data in the store" during server rendering and at the hydration step of client-side rendering.

optional getServerSnapshot: A function that returns the initial snapshot of the data in the store. It will be used only during server rendering and during hydration of server-rendered content on the client. The server snapshot must be the same between the client and the server, and is usually serialized and passed from the server to the client. If you omit this argument, rendering the component on the server will throw an error.

Where I made a mistake is treating Store#init as the same thing as getServerSnapshot, when they need to be different in more complex use-cases.

Using Store#init as the shared data snapshot means that sharing any non-trivial, non-default data between server and client for SSR will require the user to derive the store's data before creating the store, so the up-to-date value can be set as Store#init then used for server render and client hydration. This is a backwards way of using a store.

To handle SSR properly and completely, I think we need two things:

  • The existing behaviour, where the Store#init value is sufficient to fix SSR hydration for simple cases where the store state isn't modified server-side
  • A way to provide an optional "server snapshot" override value for useStore to use instead of Store#init for client hydration, for cases where the store was modified server-side and its latest value is passed (somehow) from server to client for this purpose.

How exactly the up-to-date server-side store state can get passed to the client is a another complicated question.

jmurty added 2 commits March 17, 2026 23:08
…act`

Apply the improved API for `ssr` options, more complete tests, and
clearer documentation from the React version of `useStore`, adapted
to work for Preact (i.e. via a `useEffect` hook instead of
`useSyncExternalStore`)
@ai ai merged commit 3323275 into nanostores:main Mar 17, 2026
4 checks passed
@ai
Copy link
Member

ai commented Mar 17, 2026

Released in 1.1. Thanks!

@jmurty
Copy link
Contributor Author

jmurty commented Mar 17, 2026

Cheers, and thank you!

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