Do a one-time server operation upon page load, and then re-use that data
I am looking for a way to do a one-time database query for a very rarely changed dataset -> And then store that data into server memory for subsequent re-use
More details:
1. I have a dynamic route
/map/[country].tsx
2. Upon loading this page the first time, I want to query database for list of country and store that data into server memory. So that subsequent navigation to the map/[country].tsx
can just index the maps
object instead of querying the db again
Currently, I have
7 Replies
Actually I just tried using top-level await with
And it seems to work. Not sure if there are any footgun but subsequent navigation doesn't "GATHERING" anymore
I swear there was a
Error: Cannot call server functions outside of request
error a moment ago, but I couldn't re-produce it anymore, so I guess it works fine ?
Also, I have noticed that, on first load, "GATHERING" is always displayed twice on page load. Not sure why.
Is it because it's supposed to be called twice: once in server to render the page, and once in client to re-validate the data ? Wonder if there's a way to not re-call it client-side"GATHERING" is always displayed twice on page load.Right now the SolidStart server runs two distinct workers: - The server responsible for rendering Page and API responses. - A server for RPC responses for clients
"use server"
sections.
"use server"
sections run in both these workers, in the first for SSR and in the next to satisfy the RPC requests.
Consequently the "GATHERING" module is loaded separately for each worker, each keeping their separate copy of MAP_DATA
in the worker's memory space.
If the server needs anything out of that module, loading of the module will force the setting of MAP_DATA
(i.e. querying the DB).
---
Looking at:
Based on the contained "use server"
section, loadMap
is intended for client side access. loadMap
is defined on the top level of the module, so from the bundler perspective this module needs to be part of the client side bundle.
So again when the client loads this module in the browser MAP_DATA
is assigned client side but this time gatherData
connects to the RPC worker and runs the DB query all over again (as only loadData
uses MAP_DATA
).
So your "server side in memory copy" isn't actually being used whenever a new client spins up.
Then there is the issue whether you should be caching that info anyway because it's your database that should be acting as your data cache (if that's too slow use a caching solution that is collocated with but outside of the SolidStart server).
This still doesn't address that both workers have to have their own map copy (SSR vs RPC) but at least they will reuse the DB results across clients.Thanks a lot @peerreynders for such a detailed response full of insights.
I have some followup questions if you don't mind:
1. You kept mentioning "use server" sections as a module --> Does the bundler split each server function into a separate module/file ?
2. I understood "
loadMap
is intended for client side access", since a server function is intended to be used inside some kind of request/createAsync calls on the client.
However, I don't understand "loadMap
is defined on the top level of the module, so from the bundler perspective this module needs to be part of the client side bundle." --> I have 'use server'
but loadMap
makes it to the client bundle - aren't it supposed to be server-only? I vaguely remember the compilation result being $createServerReferrence(...)
in some cases - is that not the case here ?
3. "When the client loads this module in the browser MAP_DATA
is assigned client side" --> So MAP_DATA
now lives 3 places - in memory of the two workers, and in browser memory ?
4. You separated the gatherData()
function into a separate src/server.ts
file and makes it into a pure server side module WITHOUT 'use server'
--> Is this a special naming convention when the file name contains the word "server", or has the bundler always worked that way - i.e. stuff in routes
folder makes it to client-bundle, and anything outside of routes
folder is server-only ?Does the bundler split each server function into a separate module/file ?No idea. However keep in mind that on the server worker it just runs as plain JS during SSR. Meanwhile in the RPC worker the code is always sandwiched between RPC argument deserialization and result serialization.
I have 'use server' but loadMap makes it to the client bundle - aren't it supposed to be server-only?
loadMap
is converted to a RPC stub that is used client-side. It's only inside that stub that the arguments are serialized and sent to the RPC worker. Then the "use server"
section is executed inside the worker on the server and the result is serialized and sent back to the client.
During SSR the "use server"
has no effect, i.e. loadMap
is just a normal JS function that runs in the server.
So loadMap
will end up in both server and client bundles; in the server bundle as a plain JS function in the client bundle as an RPC stub. But from the client's perspective the module that contains the RPC stubs is just a regular client module.
So MAP_DATA now lives 3 places - in memory of the two workers, and in browser memory ?Correct.
Is this a special naming conventionNo. It's just a way of organizing things that makes it easy on the bundler to know where things belong. A more extreme version: After the RPC compiler replaces with the RPC call, tree shaking can remove this import and the client module no longer references the
src/server.ts
module.Oh very cool. So anything a
use server
function references will be assumed to be inside the server-bundle without needing to explicitly marked "use server", and thus will be tree-shaken away if it lives in a separate file (like src/server.ts
lives in a separate file so when the RPC compiler generate the RPC stub, it knows that src/server.ts
already will have existed on the server, so the client bundle won't need to import this function).
What if the getMapData
function lives in the same file as the route page? Would that mean that the getMapData
function will also be turned into an RPC stub if it has 'use server', or be included as an actual function in the client bundle if it doesn't have 'use server' ?
So MAP_DATA now lives 3 places - in memory of the two workers, and in browser memory ?But doesn't
countryData
gets turned into an RPC stub without the actual function content making it to the client bundle? If it gets turned into an RPC stub, how does MAP_DATA gets created in the browser memory, given that no actual client-side function uses MAP_DATA ?
My apologies if I am being a bit dumb. Thanks a lot for your patienceWould that mean that the getMapData
function will also be turned into an RPC stub if it has 'use server'
Yes.
be included as an actual function in the client bundle if it doesn't have 'use server' ?Also yes, as that is an entirely different case (i.e. the "or" is confusing here).
how does MAP_DATA
gets created in the browser memory,
I was talking about this situation.
As soon as the client loads the module
is run in order to initialize the client side module global (perhaps you'll get lucky and tree shacking will remove it but tree shaking can be tricky) , meanwhile loadMap
will use the MAP_DATA
on the server worker during SSR and MAP_DATA
on the RPC worker during a dynamic route load.Thanks. I understand it now.
gatherData()
is converted into an RPC stub. However, the line const MAP_DATA = await gatherData();
makes it into the client bundle --> const MAP_DATA = await callTheGatherDataRpc();
I think my biggest realization from today is that, in SolidStart, anything inside a route file is automatically included into the client bundle if not explicitly marked as use server
. This is a small surprise to me because, coming from React's Remix.run, I have expected that only the component code is included inside the client bundle (or maybe I understood Remix.run wrong).