S
SolidJS7mo ago
glassy

createAsync vs createResource and how does Suspense work?

Hello. From doing some searches in here it looks like createAsync (+cache) seems to be the new recommended approach for data loading. I am wondering though, createResource returns a lot of information and options for the request
type ResourceReturn<T> = [
{
(): T | undefined
state: "unresolved" | "pending" | "ready" | "refreshing" | "errored"
loading: boolean
error: any
latest: T | undefined
},
{
mutate: (v: T | undefined) => T | undefined
refetch: (info: unknown) => Promise<T> | T
}
]
type ResourceReturn<T> = [
{
(): T | undefined
state: "unresolved" | "pending" | "ready" | "refreshing" | "errored"
loading: boolean
error: any
latest: T | undefined
},
{
mutate: (v: T | undefined) => T | undefined
refetch: (info: unknown) => Promise<T> | T
}
]
There is the state, the error, and options to mutate and refetch. But createAsync returns just the data. What if I want to also get the state? What if I want to mutate/refetch the data? And how should (or how can) I handle errors when using createAsync? EDIT: (Answering some of my own questions) since createAsync returns the data from createResource then it is possible to use data.loading and data.error If I use createResource instead then I won't be able to use cache. Also, I am wondering how Suspense actually works. How does it detect that some data is still loading. Is it possible to create my own resource manually that Suspense detects without using createResource?
24 Replies
glassy
glassyOP7mo ago
I also read through this: https://discord.com/channels/722131463138705510/1239648801581432903 Still don't get why createAsync doesn't return the resource, or how Error boundary is supposed to work with it. EDIT: I suppose it is because that is just the new api design. Maybe Error boundary still works with it? Haven't tried. Now i just found createAsyncStore exists, but couldn't find any documentation about how it works or how to actually use it. More questions about how cache and createAsync works. The docs just don't explain in detail enough (or I am too dumb). - Can you control how long cache caches the result? I want some keys to be cached forever until manually invalidated. - When the cache is invalidated and/or a new request is made resulting in a new value in the cache, do all users of createAsync for that cache key get the new value reactively automatically? Or is the new value only given to later createAsync calls ? - When the cache has become stale and a new createAsync is called does the cached function and createAsync - A. immediately resolve with the previous data (stale-while-revalidate behavior) and then does the request and reactively update the result - OR - - B. await on the request to get the new data to finish before resolving - EDIT: it looks like the existence of data.latest allows both behaviours? - Is it possible to retrieve the current value for a key in the cache synchronously without triggering a refresh? - Does the revalidate method on the cached function actually execute the request again? If not, then why is it not called invalidate and why is there no invalidate method that just invalidates the cache for the key without re-fetching? * Where can I learn about how this stuff might be changing in Solid 2.0 ( or sooner)? Sorry if these are dumb questions, I am still kind of new to this kind of data fetching paradigm.
intelligent-worker-probe
I think much/some of this is documented in the solid-router main repo page https://github.com/solidjs/solid-router?tab=readme-ov-file#data-apis
GitHub
GitHub - solidjs/solid-router: A universal router for Solid inspire...
A universal router for Solid inspired by Ember and React Router - solidjs/solid-router
intelligent-worker-probe
Design, usage and goals are discussed and presented in the RFC itself from 2023 https://github.com/solidjs/solid-router/discussions/306
GitHub
RFC: Cache/Preload Architecture · solidjs solid-router · Discussion...
Summary In hopes of aligning with a future where cache centric approach is to routing this RFC outlines a change in Router architecture to enable optimal data fetching patterns for CSR & SSR SP...
peerreynders
peerreynders7mo ago
Disclaimer: These are just my own conclusions that I have arrived at from the various hints that Ryan has dropped over the past months. So there is plenty of room for error/change.
since createAsync returns the data from createResource then it is possible to use data.loading and data.error
Don't stake too much on the fact that createAsync is currently based on createResource. That is a temporary measure. Fundamentally createResource isn't primitive enough and createAsync is to new primitive for async state to enter the reactive world. refetch has been replaced by redirect, reload and mutates role has now been supplanted by actions and submissions. While many find createResource more convenient as a mechanism (locality of behaviour and all that) that rise of the new cache, createAsync, and action primitives happens to coincide with the realization within the React ecosystem that components shouldn't be responsible for fetching their data. Furthermore cache, createAsync, action enable the implementation of Flux across the client and server boundary.
Real World React
YouTube
When To Fetch: Remixing React Router - Ryan Florence
Recorded live at Reactathon 2022. Learn more at https://reactathon.com When To Fetch: Remixing React Router We've learned that fetching in components is the quickest way to the worst UX. But it's not just the UX that suffers, the developer experience of fetching in components creates a lot of incidental complexity too: data fetching, data muta...
Meta Developers
YouTube
Hacker Way: Rethinking Web App Development at Facebook
Delivering reliable, high-performance web experiences at Facebook's scale has required us to challenge some long-held assumptions about software development. Join us to learn how we abandoned the traditional MVC paradigm in favor of a more functional application architecture.
glassy
glassyOP7mo ago
Thank you to both of you! Looks like I need to research more.
peerreynders
peerreynders7mo ago
Also, I am wondering how Suspense actually works.
The concept of Suspense hasn't been affected with the arrival of createAsync However it may be easier to understand if you know about the concept of a non-local return. Stated simply rather than returning a result all the way up the call stack (which would mean the callers would have to be aware of that type of result and what to do with it in the first place; think of it as a variation on the issue of prop-drilling) a specialized result type is thrown with the full knowledge that there is somebody way up the callstack ready to catch it. Yes, we are just talking about throw and try…catch here. Now most people associate that with just error handling and in lots of languages “abusing” throw…catch for “flow control” is heavily discouraged, primarily because it makes their runtime work extra hard (i.e. heavy performance penalty) and also because it can make code hard to understand (there's LOB again) especially if it is overused. That didn't stop React from starting to throw any unsettled promise it encountered during the rendering process to be caught by the Suspense boundary. So when a Suspense catches an unsettled promise it simply renders the fallback. But once the promise resolves it tries the render again, at which point it either succeeds or another unsettled promise is thrown (… waterfall incoming); if it rejects you throw the Error up to the next ErrorBoundary. It's stuff like this why I believe that … devs shouldn’t need to know how X works under the hood is ultimately to the detriment of those devs.
MDN Web Docs
throw - JavaScript | MDN
The throw statement throws a user-defined exception. Execution of the current function will stop (the statements after throw won't be executed), and control will be passed to the first catch block in the call stack. If no catch block exists among caller functions, the program will terminate.
MDN Web Docs
try...catch - JavaScript | MDN
The try...catch statement is comprised of a try block and either a catch block, a finally block, or both. The code in the try block is executed first, and if it throws an exception, the code in the catch block will be executed. The code in the finally block will always be executed before control flow exits the entire construct.
Ricky (@rickhanlonii) on X
@acemarke @ryanflorence @RyanCarniato There’s not and there are no plans to add it. As a principle for how we approach docs and APIs, React devs shouldn’t need to know how React works under the hood.
Twitter
glassy
glassyOP7mo ago
Oh wow! So Solid also works by actually throwing too? Actually, my question was more about how would I implement my own 'resource primitive' that Solid's suspense can work with. How does Solid know that this 'data' signal I am accessing in the JSX is a 'resource'. Or can I just use any signal? (Probably not) Or does it need to have a loading and/or error property? Does the signal have to call out to the suspense context somehow? Or is it just not possible and Suspense only works with 'resources' from createResource
peerreynders
peerreynders7mo ago
You are still thinking in terms of the createResource API as it is exposed by its type. But TypeScript doesn't support typed exceptions; so by the same reasoning it doesn't communicate that the resource is going to throw any unsettled promise that it got from the fetcher for the nearest Suspense boundary to catch.
glassy
glassyOP7mo ago
Oh, so if anything throws then the suspense boundary catches it? I probably need to try that. Or does it depend on what I throw?
peerreynders
peerreynders7mo ago
I'm sure there is some kind of protocol involved because the intention is for the framework to throw and catch it. For all I know throwing promises yourself may lead to undefined behaviour. Also the Suspense boundary catches thrown Promises, the ErrorBoundary catches thrown Errors (though it may actually catch more than that, I'm not sure).
peerreynders
peerreynders7mo ago
What if I want to also get the state?
I perceive Solid's design philosophy to focus on performant implementation of what you will need while not getting in the way of you implementing what you may need. If that means that devX has to play second fiddle then so be it. Fundamentally cache exists as a mounting point for zero to many createAsync and createAsyncStore to attach to, to get the latest async state. Whenever a cache updates because of a reload or redirect all attached createAsync will be updated when the updated async state finally resolves. Cache invalidation is one of the two hard things and cache decides not to deal with it. - for preloads the cache is considered stale after 10 seconds largely to de-duplicate fetches that otherwise multiple consumers may cause. - for back/forward cache (mdn) cache values are considered "fresh enough" for up to 5 minutes. It makes sense not to rerun the async ops given that for all intents the bfcache view is stale anyway. Knowing this it should be clear that ideally in 99% of the cases that cache represents the last known valid async state from the server and some pains should be taken to ensure that holds (which is why using the undocumented cache.set for a mutate replacement is probably a bad idea in most cases; instead actions/submissions should be used to realize the UX benefit of optimistic UI). But the value that comes out of cache is completely under your control based on what you return from the async op you pass to it. So if you desperately need that state return it together with the latest known value!
martinfowler.com
bliki: Two Hard Things
There are only two hard things in Computer Science: cache invalidation and naming things -- Phil Karlton (bonus variations on the page)
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.
peerreynders
peerreynders7mo ago
Here is a very simple example for demonstration. Lets say the cache value announces loading simply by going to undefined first:
// src: src/api.ts
import { isServer } from 'solid-js/web';
import { cache, revalidate } from '@solidjs/router';
import { echo } from './server/api';

// FOR DEMONSTRATION ONLY
let result_ = true;
const syncSetResult = (r: boolean) => (result_ = r);

const LOAD_NAME = 'load';
// This needs to be in an isolated app-store if used during SSR
const load = (() => {
let pending = false;
const mTask = () => revalidate(LOAD_NAME, true);
const scheduleResult = () => queueMicrotask(mTask);

const load = cache((): Promise<boolean | undefined> => {
if (isServer || pending) {
pending = false;
console.log('starting');
return echo(result_);
}

// communicate unresolved state to the outside
pending = true;
const result = Promise.resolve(undefined);

// then schedule the real async op
result.then(scheduleResult);
console.log('pending');
return result;
}, LOAD_NAME);

return load;
})();

export { LOAD_NAME, load, syncSetResult };
// src: src/api.ts
import { isServer } from 'solid-js/web';
import { cache, revalidate } from '@solidjs/router';
import { echo } from './server/api';

// FOR DEMONSTRATION ONLY
let result_ = true;
const syncSetResult = (r: boolean) => (result_ = r);

const LOAD_NAME = 'load';
// This needs to be in an isolated app-store if used during SSR
const load = (() => {
let pending = false;
const mTask = () => revalidate(LOAD_NAME, true);
const scheduleResult = () => queueMicrotask(mTask);

const load = cache((): Promise<boolean | undefined> => {
if (isServer || pending) {
pending = false;
console.log('starting');
return echo(result_);
}

// communicate unresolved state to the outside
pending = true;
const result = Promise.resolve(undefined);

// then schedule the real async op
result.then(scheduleResult);
console.log('pending');
return result;
}, LOAD_NAME);

return load;
})();

export { LOAD_NAME, load, syncSetResult };
The async op passed to cache goes through two phases (unless SSR). - It returns a promise that resolves to undefined. But it also schedules a revalidate of the cache point which is going to run this function again. - On the second pass (recognized by pending === true) the "real work" is launched. Once that finishes the "real" result appears on the cache.
And how should (or how can) I handle errors when using createAsync?
The same way as with a resource-with an <ErrorBoundary>. If you throw an error inside the async op and don't catch it then cache will hold a rejected promise. That rejected promise is passed on to the createAsync consumers, which go “What the hell?” and throw the contained error to their nearest ErrorBoundary. Again in the majority case using ErrorBoundarys is the practice of choice while the use of resource's error property was just a nice-to-have in a few edge cases. If necessary you can always have the cache hold a
type ResultType<T,E extends Error> = { result: T } | { error: E };
type ResultType<T,E extends Error> = { result: T } | { error: E };
peerreynders
peerreynders7mo ago
found createAsyncStore exists but couldn't find any documentation about how it works
It's just the stores version; i.e. you need to understand stores first and I would add reconcile as important as well. createResource's approach to stores via the storage option should give you an indication that working with stores can be a bit more involved. I've used createAsyncStore here to handle the messages being shown on the page.
GitHub
solid-start-sse-chat/src/routes/index.tsx at d2b9070f956947c940dc20...
Basic Chat demonstration with server-sent events (SSE) - peerreynders/solid-start-sse-chat
peerreynders
peerreynders7mo ago
I want some keys to be cached forever until manually invalidated.
As already mentioned that's not how it works. It's an extremely short term "cache" so it will run again when a new consumer shows up after the expiry. The benefit is that everybody will get the refreshed value after that happens. And manual invalidation is performed via reload and redirect and again all consumers benefit.
peerreynders
peerreynders7mo ago
Does the revalidate method on the cached function actually execute the request again?
Note that revalidate has a force option which defaults to true. So the intent that I perceive is that you can specify false if you want to ease up within the expiry limit.
GitHub
solid-router/src/data/cache.ts at f43d85c5d698594170a9c3f87bbb1d4e7...
A universal router for Solid inspired by Ember and React Router - solidjs/solid-router
peerreynders
peerreynders7mo ago
There is also this from last November: https://hackmd.io/@0u1u3zEAQAO0iYWVAStEvw/rkRVTcTWp This reminds me of a quote from 2009:
… engineering in 1980 was not what it was in the mid-90s or in 2000. In 1980, good programmers spent a lot of time thinking, and then produced spare code that they thought should work. Code ran close to the metal, … -- it was understandable all the way down. … Nowadays you muck around with incomprehensible or nonexistent man pages for software you don't know who wrote. You have to do basic science on your libraries to see how they work, trying out different inputs and seeing how the code reacts.
… it's a consequence of how quickly things are moving (which still isn't fast enough for the "are we there yet" crowd).
HackMD
Router Data APIs - HackMD
Potential Router APIs for data
peerreynders
peerreynders7mo ago
So you want to know how something works? - Download a fresh template - Devise and implement the most minimal experiment to either reinforce or refute your current mental model - execute, observe, adjust Rinse and Repeat. OODA loop Observe, Orient, Decide, Act. Deming/Shewhart Cycle Plan, Do, Check, Act
OODA loop
The OODA loop (observe, orient, decide, act) is a decision-making model developed by military strategist and United States Air Force Colonel John Boyd. He applied the concept to the combat operations process, often at the operational level during military campaigns. It is often applied to understand commercial operations and learning processes. ...
Build Stuff
YouTube
Programming with GUTs by Kevlin Henney
Kevlin Henney is an independent consultant and trainer based in the UK. His development interests are in patterns, programming, practice and process. He has been a columnist for various magazines and web sites, including Better Software, The Register, Java Report and the C/C++ Users Journal. Kevlin is co-author of A Pattern Language for Distribu...
peerreynders
peerreynders7mo ago
Nyi Nyi Lwin
StackBlitz
solid js - useSubmission - StackBlitz
Next generation frontend tooling. It's fast!
peerreynders
StackBlitz
solid.js action cache interplay - StackBlitz
A Solid TypeScript project based on @solidjs/router, solid-js, typescript, vite and vite-plugin-solid
glassy
glassyOP7mo ago
Amazing! Thanks so much for all the info and answers! It is a big help in giving me a better idea of how it works and why it is designed this way. I wish the docs themselves explain the how to use, why it is designed this way, and how it works in more detail. Right now to understand it all you need to piece it together from various sources (actually, from exactly those links above you shared thank you again!) Like we are given these tools and we are told 'this is the way'. But without the why, it feels like just ...WHY?
sabercoy
sabercoy7mo ago
I agree the docs are lacking. Many primitives are skimmed over with one very basic example and do not elaborate on why it behaves the way it does or what the implications of its behavior are. createResource seems nice off the rip because it gives you so many controls to tap into that makes it seem so versatile. But one issue is waterfalls. If you watch Ryans videos he spent/spends much time thinking about how to eliminate them. If you hover over a link that goes to a page with many components that wrap each other and each have createResource to fetch some data, the data does not even start fetching until the components within are rendered (note: this could be on the server during SSR). That means the parent starts fetching, then the child, then grand child, etc. Inefficient! Another issue is what if you want to share this resource data in multiple places in your app and fetch it only once? Get prepared to use context or prop drill. cache and createAsync solves these. If you hover over a link for a page with those nested components mentioned, and that page supplies the route.load with the cache function, then the data will start to fetch before the component loads, renders, anything. This often results in the data arriving before the link is even clicked, making the page seem to load instantly. This is possible because the functions to fetch the data are "hoisted": detached from the components and not relying on them rendering to begin fetching. cache and createAsync solve the "pass my data through the app" issue too. I think of a cached value as a sort of global "store" you can tap into with createAsync A caveat to the cache/action paradigm is the inability to manually set the cache (there is a cache.set method, but I have never gotten it to work, even if it did, it is against the paradigm). The cache value always gets its new data from the same defined function it started with (which is probably reading from a database or something, and not manually provided by the user in the UI revalidating in an action is your way of telling the cache to "get some new data in the way you already know how" you do not have the convenience of having the action say "hello cache, I am the action, and now I want you to have this as your new data"
peerreynders
peerreynders7mo ago
There is one question you haven't gotten around to asking yet:
Now that I have all this working why did <Suspense> break?
Suspense doesn't break; revalidate automatically triggers a transition. Transitions implement paint holding for the content under the boundary; i.e. when revalidate (the transition) starts the stale view is locked in while the fresh view is rendered in the background and only swapped in once it is complete. Consequently during a CSR render a Suspense boundary will show it's fallback only once, the very first time the content under the boundary is rendered. Past that revalidate will always trigger a transition. During an SSR render where the promise is consumed through a createAsync with { deferStream: true } you will not see the fallback at all as the response won't start streaming until that promise settles, allowing the content under the boundary to render before it leaves the server. Now before you mount the barricade in protest of being deprived of choice, research shows that once you manage to load in under 1 second (something you should be aiming for anyway) local fallbacks diminish UX. So recognize that you are being guided towards implementing one, innocuous global loading indicator that's easily ignored when interactions are fast while hinting at progress whenever there is a spike in latency. This is as good a time as any to spam Ryan's favorite slide.
Chrome for Developers
Paint Holding - reducing the flash of white on same-origin navigati...
A quick overview of paint holding. A Chrome feature for reducing the flash of white on same-origin navigations
O'Reilly
YouTube
Velocity 09: Eric Schurman and Jake Brutlag, "Performance R
Eric Schurman (Microsoft) and Jake Brutlag (Google), "Performance Related Changes and their User Impact"
peerreynders
StackBlitz
Using a global loader for transitions - StackBlitz
solid-router's implicit transitions discourage page partial loading fallbacks in favour of paint-holding the stale page while showing one central loading indicator.
Nielsen Norman Group
Skeleton Screens 101
A skeleton screen is used as a placeholder while users wait for a page to load. This progress indicator is used for full page loads and reduces the perception of a long loading time by providing clues for how the page will ultimately look.
peerreynders
peerreynders7mo ago
I think it has to be acknowledged that documentation tends to focus on documenting features where the use cases and benefits are assumed to be self evident so the WHY is rarely expounded upon. As far as features go primitives don't "feature" a lot. The power of primitives comes from the way they compose, giving rise to emergent properties. But enumerating the ways primitives compose isn't really something traditional documentation covers and it also gets in the way of the need for concision. People claim to want documentation but groan in exasperation when facing a wall of text. They want to be pointed to the exact paragraph that pertains to them, right now in this moment. That is hard to do with a primitives based approach where the power only becomes evident once you step back and start viewing them in relation to one another which is really what composition is all about.
glassy
glassyOP7mo ago
I really do hope the documentation improves, for the sake of adoption of Solid. A lot of people don't know the latest best practices in web development, unless they are coming straight from the other latest frameworks. It doesn't have to be a wall of text. It can be spread out in multiple pages one per topic. (ie. a link to "read to learn more", or "read this page to learn why it is designed this way to make your life easier") Even just including the links you shared above at the relavant places in the documentation would be a big help. I agree with this post a lot: https://discord.com/channels/722131463138705510/1234366566947356773/1234366566947356773 Sometimes I feel even the latest best practices for Solid itself isn't clearly stated in the docs. But of course things are changing fast. For example, the whole topic of my post the document page that is the guide for 'fetching data' in Solid JS https://docs.solidjs.com/guides/fetching-data createAsync isn't even mentioned
peerreynders
peerreynders7mo ago
Likely because that is because under "Core" and because the equivalent section under "Router" doesn't exist yet, so that it could link to it before it eventually gets moved to "Core" come SolidJS 2.0 anyway. It's an unfortunate fact that the best way to maximize the impact of the documentation teams efforts is to delay documentation until there is a certain degree of stability to the subject matter. In my judgement open source has really changed the meaning of v1.0. It's a declaration of stability for production use for those who are in the know. It doesn't declare that the documentation is in a state where onboarding newcomers is relatively painless. A certain amount of threading through documentation, README's and even (boundary) source code (in addition to a fair bit of experimentation) is still necessary to get the full picture. I'm pretty sure that Vercel has paid staff maintaining their docs. Paid writers tend to be in a precarious position. As far as I'm aware Solid's docs team is entirely volunteer so their efforts have to be directed judiciously.
Want results from more Discord servers?
Add your server