Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Nanostores in SSR #80

Open
AlexandrHoroshih opened this issue Oct 25, 2021 · 16 comments
Open

Nanostores in SSR #80

AlexandrHoroshih opened this issue Oct 25, 2021 · 16 comments

Comments

@AlexandrHoroshih
Copy link

AlexandrHoroshih commented Oct 25, 2021

Hi there!

I saw news on new release, saw allTasks API and decided to research, how nanostores is compatible with SSR

Loading of Node.js template in codesandbox takes forever for some reason, so i decided to create similiar reproduce with basic one:
https://codesandbox.io/s/loving-wilson-4vp4r?file=/src/index.js
(see index.js and console)

Idea is the same:

  1. Request happens
  2. Request-handler task is added to event-loop and starts some logic and web or db requests
  3. Once those web or db requests are over, state is recalculated with new data
  4. State injected in the app, which then rendered to string
  5. Resulting string sent as response

During all of these operations is important to isolate state that "belongs" to different requests from each other - otherwise data from one request will leak to the other

Nanostores have two issues there:

  1. States of nanostores are shared between requests, which then leads to data prepared for one request to show in another - in result rendered html-string is not correct.

  2. State of internal allTasks counter is also shared between all requests, which leads to weird bug: responses are not sent until all requests are handled, even if their data is ready.

Possible solutions:

  1. Many state-managers require creation of new instance of store/stores for every request, like this
const ssr = async (request) => {
 const stores = initStores()
 
 await stores.startLogic(request)
 
 return renderAppToString(stores)
}

This will effectively isolate requests from each other, because all of them will have their own instance of stores and all pending operations also will be bounded to this instance
Mobx example: https://github.com/vercel/next.js/tree/master/examples/with-mobx-state-tree

  1. Some other state-managers, like effector, allow to create separate instance of app state in special way, allowing to reuse already initialized stores and connections, like this:
const ssr = async (request) => {
 // all stores and connections already defined somewhere
 const scope = fork()
 
 await allSettled(startLogic, { scope, params: request })
 
 return renderAppToString(scope)
}

Example: https://github.com/GTOsss/ssr-effector-next-example/blob/effector-react-form-ssr/src/pages/ssr.tsx

@ai
Copy link
Member

ai commented Oct 25, 2021

Am I right that you are using the same Node.js context between different users during the async SSR?

I'm afraid that state manager isolation is a weak security strategy for this case. The data of one user can be leaked via many global structures like caches in npm libraries, etc.

Is it possible to create the new global context between these calls?

@ai
Copy link
Member

ai commented Oct 25, 2021

Short answer to the direct question:

  1. Many state-managers require creation of new instance of store/stores for every request, like this

This way can be used with Nano Stores without any changes. Just move stores from files like ~/stores/*.js and build store’s instances during the app initialization.

@AlexandrHoroshih
Copy link
Author

AlexandrHoroshih commented Oct 25, 2021

I'm not using SSR at all right now, but it is a common problem with Next.js (if it is used as a classic Node.js server or something that is not serverless), so i was interested, how it works with nanostores

This way can be used with Nano Stores without any changes. Just move stores from files like ~/stores/*.js and build store’s instances during the app initialization

But, if i'm not missing something, this way allTasks state still will be shared? 🤔

@ai
Copy link
Member

ai commented Oct 25, 2021

But, if i'm not missing something, this way allTasks state still will be shared? thinking

Got it. Yeap, it will be a problem here. You need to use explicit store loading Promise like await post.loading in this case.

@vovaspace
Copy link

Hi there.

And what about hydration/dehydration/snapshoting to transfer the state from the server to the client? Are there any plans for a full SSR support?

@ai
Copy link
Member

ai commented Oct 28, 2021

And what about hydration/dehydration/snapshoting to transfer the state from the server to the client?

You can do it by

store.set(initialState)

To do it automatically, you need a framework like Next.js or Svelte Kit, some wrap on top of state manager.

Maybe you want to maintain this project? Do you know a good examples around other state managers?

@euaaaio
Copy link
Member

euaaaio commented Oct 28, 2021

Hi there.

And what about hydration/dehydration/snapshoting to transfer the state from the server to the client? Are there any plans for a full SSR support?

Nanostores doesn't have a root state. Just use store.get() for each store you need. So you can hydrate as you want. For example JSON.stringify/JSON.parse.

@vovaspace
Copy link

Thanks for answers.

Do you know a good examples around other state managers?

Unfortunately, I do not know state managers with atomic stores, which can be given as an example.

As for the store.get()/store.set(), it seems to me that there are not enough examples in the documentation with mass getting/setting values from/to several stores. I mean, there can be a huge number of stores in an application, some of them can be created dynamically, for example. Of course, I can save links to each store, take values, pass it to the client, and so on, but there is not enough "true way" how to do it without such a large manual setup.

@euaaaio
Copy link
Member

euaaaio commented Oct 29, 2021

Just an idea.

We can extend Vue integration for SSR with useSSRStore(store) composable, with useStore() under the hood. All the store states that have been used on the current page will be collected in one tree. Then you can use it through hydrate()/dehydrate().

@AlexandrHoroshih
Copy link
Author

AlexandrHoroshih commented Nov 20, 2021

I think, nanostores could utilize similiar stack-based approach like the one in effector 🤔
(+ React.Context and Hooks are also using this approach, but in a bit diffirent situation though)

General idea is something like this
https://share.effector.dev/sr3BtOLj

// internal field
let scope = null

const makeStore = (name, initState) => ({
 // all stores will need an unique name or identifier, which should be the stable between server and client
 name,
 state: initState,
 getState() {
  // if scope is set during operation, use state from scope
  if (scope) {
   return scope[name] ?? initState
  }
  return this.state;
 },
 set(v) {
  if (scope) {
   scope[name] = v;
   return;
  }
  this.state = v;
 }
})

// scope fabric
const getScope = () => ({});

// general pattern for working with scope
// pass target scope explicitly
const runInScope = (s, cb) => {
 // set it to internal field
 scope = s;
 // run logic
 cb()
 // thanks to callstack all store api calls will see
 // which scope is set now
 // but requires additional work to handle async stuff
 
 // clear internal field
 scope = null;
}

// in the app code
const myScope = getScope()

runInScope(myScope, () => {
 store.set("foo")
})
myScope[store.name] // "foo"
// all synchronous reactions will also be "in the scope" automatically
myScope[someDeeplyDerivedStore.name] // "derived from foo"
// but it is still needed to somehow preserve scope for any async operations or effects

This would allow to automatically collect all the states during SSR and then transfer it to the client:

// ssr
const myServerScope = getScope()
// ... run some logic on scope
const stringifiedStateOfTheApp = JSON.stringify(myServerScope)

// client
const myClientScope = JSON.parse(stringifiedStateOfTheApp)

And also separate state of different requests/tests:

const myTestOrSsrRequestHandler = () => {
  // each test/request gets its own isolated box for app state
 const myScope = getScope()
 ...
}

This is very roughly how effector scopes work, but advantages are the same: effector is able to both isolate and automatically collect all atomic stores states without any manual boilerplate

@ai
Copy link
Member

ai commented Nov 22, 2021

Yeap, Effector’s scope isolation is very good for SSR, when you render HTML for multiple users in the same context.

Maybe we can create some sort of the wrap around current minimalistic stores to support scope?

@AlexandrHoroshih
Copy link
Author

AlexandrHoroshih commented Nov 23, 2021

I don't think that wrapper would work - if user missed some store, then it will be missed from serialization and further hydration at the client, which is no good, if it, for example, was changed during SSR (on the other hand, it may make sense to ignore some stores in serialization, for e.g. internal stores of libraries)

Probably it should be a core feature, so all APIs of the library are aware of the scopes and can properly handle it 🤔

I don't think that it is actually matter, if server context is isolated or not - the problem of collecting all parts of decentralized state of the app is still persists, since it is needed to hydrate app state at the client somehow anyway 🤔

@eddort
Copy link
Contributor

eddort commented Nov 23, 2021

I have another solution, but the documentation is very weak.

https://github.com/Eddort/nanostores-ssr

The main idea is to create a handler for each request (SSR function) and create a new context for each new request.

@droganov
Copy link
Contributor

droganov commented Feb 3, 2023

Hm, could we pass getServerSnapshot to the React hook to prevent render time side effects on Server?

@bernatfortet
Copy link

Any updates on using SSR? The documentation is very short and I don't get enough clarity.

My use case is to be able to hydrate the store from a Server Component in React 18 / Next.js. The underlying reason is that by hydrating I hope to avoid an extra re-render.

Thanks!

@ai
Copy link
Member

ai commented May 7, 2024

I don’t use Next.js and we need some advanced user to do the research and optimization

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

No branches or pull requests

7 participants