How does `query` cache actually work?

Caches are tricky and the docs do not explain the usage details, here are some questions: - When is the cached value invalidated? - Why do I want to use this cache? - When could I run into issues with stale data by using this cache? I read through the docs already: - https://docs.solidjs.com/solid-router/reference/data-apis/query
11 Replies
REEEEE
REEEEE3d ago
query is meant to deduplicate requests in a short time frame (I think it's 5 or 10 seconds) and not meant as a traditional cache. This confusion is part of why it was renamed from cache to query
Josesito
JosesitoOP3d ago
Oh yeah, it would be great to add this information to the documentation, and it should be as accurate as it can be, for me there is a difference between "1 second" and "10 seconds", and I would like to know such details for sure. Another piece of valuable information that is missing from the docs is this: - https://discord.com/channels/722131463138705510/1312420332333961216 According to these users, the cache does not persist across requests. This, for instance, has very different implications from a "cache that invalidates after 10 seconds" Assuming that those comments are accurate, ofwhich I am not in a good position to verify
peerreynders
peerreynders3d ago
It's a mistake to view query in isolation. - To some degree the aim is for the route to be able to load all the data that may be needed by every component that is on it's page. - From that perspective query helps to decouple the aquisition of data from the component consuming it. The component then can simply access the data it needs via createAsync. - So more than one component on the page can consume that data without multiple fetches. This is the "request deduping". More fundamentally this "1 to N fan-out", i.e. 1 set of data driving N components has some benefit later with actions. The data can be shared among components without going through context. - At least with Solid 1.x the query / createAsync seam establishes the "asynchronous value" to "synchronous reactivity" boundary into Solid's reactive graph where you compose reactive primitives. query deals with the "value asynchronously" while createAsync makes that value accessible via a reactive accessor. - Ideally you'll want to "warm up" all the querys on the route in the route's preload. As soon as the cursor hovers over a link to the route the preload querys will start their loading process in an attempt to shorten the loading time when the route's link is clicked.
peerreynders
peerreynders3d ago
- actions tend to mutate server state that will require some querys on the route to rerun. At the end of an action revalidate is used to refresh the relevant querys. - When a query is refreshed all the components on the route depending on it will receive the up-to-date data. - These actions can send the fresh data for the affected querys in their response body; this is referred to as singleflight mutations.
GitHub
solid-start/packages/start/CHANGELOG.md at 5812ac69632d0d3fdd7861ab...
SolidStart, the Solid app framework. Contribute to solidjs/solid-start development by creating an account on GitHub.
peerreynders
peerreynders3d ago
That's not entirely accurate. The intention is that the query value is "global" for the duration of the "route request"; i.e. until the route data settles after navigation to that route. After that actions can respond with data for multiple querys in a single request (aka single flight mutation).
peerreynders
peerreynders3d ago
A query value is considered "fresh" for 5 seconds. All components drawing their data from that query within 5 seconds will be served the same data from the initial request. Should any component connect after 5 seconds, an entirely new request is run. https://github.com/solidjs/solid-router/blob/50c5d7bdef6acc5910c6eb35ba6a24b15aae3ef6/src/data/query.ts#L15-L16 For use in the context of bfcache every 5 minutes query values older than 3 minutes are completely removed.
web.dev
Back/forward cache  |  Articles  |  web.dev
Learn to optimize your pages for instant loads when using the browser's back and forward buttons.
Josesito
JosesitoOP2d ago
This is a wonderful explanation @peerreynders! Thank you so much. It would be great to add all of your thoughts into the documentation for future travelers. As far as my curiosity is concerned, satisfaction has been surpassed. Even your explanation of "preload" in this context is much better than trying to understand each part in isolation as you say, I had not understood this interaction yet until you connected the dots for me. Likewise with "action". Kudos to the API design too, this is all radically elegant. is the query value scoped to the route of a given request? For example... - Given this query
export const getProduct = query(async (slug: string) => {
"use server";
try {
return await directus.request(readItem("product", slug));
} catch (e) {
return null;
}
}, "productById")
export const getProduct = query(async (slug: string) => {
"use server";
try {
return await directus.request(readItem("product", slug));
} catch (e) {
return null;
}
}, "productById")
- Given this route: solid/src/routes/product/[id].tsx - Given this preload:
export const route = {
preload: (({ location, params }) => {
if (location.pathname) {
return getProduct(params.id)
}
})
} satisfies RouteProps<string>
export const route = {
preload: (({ location, params }) => {
if (location.pathname) {
return getProduct(params.id)
}
})
} satisfies RouteProps<string>
Here I am using the slug [id] to determine the data that ought to be loaded. However, the cache key for query is a common key that does not differ between two different ids. I.E. - http://localhost:3000/product/thang - http://localhost:3000/product/foo Each of these routes should load very different data, but both use the same string key for their query cache: "productById". If both URLs are loaded almost at the same time, will the second route use the cached value from the first route? Now, people have been using the word "global" to describe the cache, but that is not a term that I understand as I do not know the implications. The most "global" definition would indicate that the first value to get cached will determine the value of the second request.
snnsnn
snnsnn2d ago
In Solid, because of the reactivity, changing route unloads the current component and loads the next one. This means whatever you did in the current route will be discarded. In other words, if you fetch the data for the next route, it will be discarded as well. The query function is a solution this problem. It store the data so that it won't be fetched again when the current route changes. The preload function will be associated with the corresponding route, and it will be called by the router component to fetch the data for that route. So, no need to check for location.pathname as it is designed to run when the path matches.
Preload function will be triggered when the user hovers over the link pointing to that route, assuming Router's preload prop is set to true. You can manually call the preload function, it will work the same. Sole purpose of the preload function is to fetch the data for the route. So, you should wrap the server function in a query and call it, otherwise the data you fetch will be discarded and fetched again when the route's component loads. That is why the value is cached for a short period of time. On the server side, query function store the queried value for the duration of a request. That way, if a query calls another query, the value will be fetched once. The data is stored on the rotuer. The request path is nothing to do with caching. Hope this clarifies your confusion.
peerreynders
peerreynders2d ago
Just to add to what has already been stated. These are two separate requests:
http://localhost:3000/product/thang
http://localhost:3000/product/foo
http://localhost:3000/product/thang
http://localhost:3000/product/foo
but more importantly
export const getProduct = query(async (slug: string) => {
"use server";
try {
return await directus.request(readItem("product", slug));
} catch (e) {
return null;
}
}, "productById")
export const getProduct = query(async (slug: string) => {
"use server";
try {
return await directus.request(readItem("product", slug));
} catch (e) {
return null;
}
}, "productById")
initiates it's own RPC request based on the route parameter. You can can still use createAsync(() => getProduct('foo')) and createAsync(() => getProduct('thang')) on the same route/page, each issuing their own, independent request. But getProduct takes an argument. query values are partitioned by a hash key based on the arguments that are used to acquire them. That's why it is important to use the query's keyFor to target the correct partitioned value that is stored under the query. At the end of an action
return revalidate(getProduct.keyFor('foo'), true);
return revalidate(getProduct.keyFor('foo'), true);
would only rerun the query function for 'foo' but not for 'thang' (driving components connected to the 'foo' value but not components connected to the 'thang' value).
Josesito
JosesitoOP2d ago
Oh! That's awesome that it uses the arguments to key the hash, this makes it make sense, and this also makes me understand the keyFor method. I think this is the most that the docs mention about the arguments of the function that gets passed into the first param of cache():
When this newly created function is called for the first time with a specific set of arguments
Time permitting, assuming no one else gets to it before myself, I may try to contribute to the docs if someone else helps me read-proof and re-phrase when I make a PR. --- So if two separate routes use the same query with the same key and arguments, and both of them get loaded within less than 5 seconds, the second route will use the cached value, correct? Even if the routes get loaded from different browsers.
peerreynders
peerreynders2d ago
Even if the routes get loaded from different browsers.
The query values exist within the client side JS scope of the browser tab; so each browser, even browser tab, will issue a separate request creating their own isolated query values. Now if you navigate inside the same browser tab within the 5 secs window to the second route, I assume that the query request won't be repeated. The query values live here
GitHub
solid-router/src/data/query.ts at 50c5d7bdef6acc5910c6eb35ba6a24b15...
A universal router for Solid inspired by Ember and React Router - solidjs/solid-router

Did you find this page helpful?