Replies: 5 comments
-
|
Pending doesn't and can't really throw, so you can't catch it like you can errored and loading, so the |
Beta Was this translation helpful? Give feedback.
-
That does not really matter. Pending is shown when true and not when it is false. Loading/Pending/Errored will be mutually exclusive on the same data. Edit: This is an outline. I expect some bikesheding before it lands! |
Beta Was this translation helpful? Give feedback.
-
|
I'm going to transfer this to a discussion but I love the time you spent to lay this out because by answering this I think I can shine a light on some of the design decisions here and how they differ from other solutions. We've looked at this API shape before. And don't worry about breaking changes. We are still in beta. Now is exactly the time to talk about this. The first thing to point out is the naming is the last part. The important part is the mechanics. So we should start there. It is possible that the naming is wrong but I think the shape of the mechanics dictate the outcome in a few places. BoundariesLet's start with boundaries. Boundaries exist for one reason to be able to bail out of UI that we can't render. The reason we wouldn't be able to render it is that it is incomplete or inconsistent. The 2 primary causes of that is something is errored, or we don't have have a value yet to display. Those 2 can often be related as they are around say data fetching but also can be completely independent. It is quite possible to have different affordance levels for where you show loading vs where you show errors. You might have individual error boundaries but a single loading or vice versa. In Solid 2.0 they also have different conceptual lifecycles. Loading only happens when we don't have a value to show, ie initially, whereas Errored can happen at any point in the future. Their behavior is asymmetrical so combining them can cause confusion around lifecycle. It also lets us more cleanly add attributes that work for only one mode vs others. Like the new Also since boundaries work by inner most catches it is important to be deliberate where these are. A generic boundary that catches both might no be what you want in all cases. You could take a perspective of if the callback isn't implemented then it doesn't catch and pass through. But that is a looser contract than a component that guarantees it catches. Having separate components lets the code be more streamlined as it doesn't need to conditionally create different internal primitives. Including Errored doesn't mean you are including Loading effectively. That all being said none of this stops someone making a convenience wrapper like you are suggesting for loading/errored on top of the primitives we provide. So that door is open we just keep the core primitives streamlined and effective. Non-Boundary StatesNow the other 2 states Ie. <AsyncSlot
loading={}
errored={}
>
{(loaded) => <Page loaded={loaded()} />}
</AsyncSlot>
So to making pending fit that there was an early design where I was thinking we'd do: <AsyncSlot
loading={}
errored={}
>
{(pending) => <Page loaded={pending()} />}
</AsyncSlot>This actually my starting point when I went to implement 2.0. A shared boundary with pending fed in. I think even Svelte 5 more or less has this shape. But as Svelte also realized that pending was a insufficient for updates (they have The challenge here is that transitions are conceptually a global thing. If you aren't being caught by a boundary then you rise to the top level. If you don't do that then you introduce tearing. It basically defeats the whole point if you want this to align: // top level control
const [userId] = createSignal(1);
// async to be used somewhere lower
const user = createMemo(() => fetchUser(userId())); Like if not being under a boundary is enough to update So the key realization when working on this was that if we made the pending state queryable we could basically collapse the need for transitional/transactional pending state into a single primitive. If you ask Like if you load different data depending on whether you are on the "details" vs the "settings" tab and you are on the details tab the only thing you have in front of you are the fetched details. That is the UI you are showing the user. How do you indicate to them the next page is coming without showing a Loading spinner. Ie if you want to basically paint hold. You Other Non-Boundary StatesI often the biggest mistake I made with Solid 1.0 is having The saving grace of
<Loading fallback="Loading..."><Page /></Loading>
<Show when={!isLoading(user)}><Page /></Show>
The other difference is SSR/Hydration. Streaming needs to know how to flush HTML. Show isn't special. We could make it work but then you'd be streaming data and the client would be rendering it. That's fine in some cases arguably (and how the earliest versions of streaming worked in Solid) but it delays the content swap until hydration. Where as with Loading since we know and can set it to this purpose we can stream in the content before Solid ever loads in the client. Ok let's say you are fine with using Loading for streaming. What about other things with This is tricky to resolve because either you have to make an assumption that might not be right isLoading always true/false during hydration. Or you need to serialize it for each boundary flush as a separate snapshot and update it as you hydrate through so that client matches server. It overcomplicates things for a scenario that is already designed to work with boundaries. Stepping BackI think the current design is consistent, minimal, directed for good behavior patterns, but this suggestion indicates there is a feeling that something is missing. I'd like to understand the shape of those patterns a bit more. Where does this fall down? What are you missing that isn't a convenience wrapper that can be done in user space? I especially want to understand:
As I feel that is what the current proposed APIs do. We don't tie boundaries to components but data so you can put the function User(props) {
// async to be used somewhere lower
const user = createMemo(() => fetchUser(props.userId));
return <Loading fallback={`Loading User ${props.userId}`}>
<SomewhereLower user={user()} />
</Loading>
} |
Beta Was this translation helpful? Give feedback.
-
|
Thanks for such a detailed response and explanation; it is rare to get to see the thought process of a project lead. If you feel that discussion is at any time going out of bounds, please let me know... the very last think I want to do is keep you from working towards a production release of Solid 2.0. Right of the bat, let me agree with you that design you have is consistent and minimal (nothing falls down AFAICT!). This is more of an human interface argument than a functional one. Referring back to the example 10 & 11 on the stream of 6 Feb 2026 and our discussion in the comments thereafter, I would like to see a DX that strongly encourages developers to specify loading and error handling for any component handling async data (rather than making it seem optional). It should really be an opt-out rather than an opt-in (the organ donor argument). If the developer chooses not to specify the boundaries with async data, the framework should really be asking (even if implicitly or as a warning), "Buddy, do you really think this is a good idea to leave that space empty?"
I would have thought the boundaries having mutually exclusive temporal lifecycles exactly the reason to combine them. If I consider the
If we further add in, what you are calling Projections, you have further states:
And if we now throw in writes:
1, (2,3) and 4 are mutually exclusive lifecycle states within the (Also, maybe a difference between our though processes is that you think in terms of boundaries whereas I am thinking in terms of fallback states of UI)
With the exception of the lifecycle argument above and the aside below, I'll grant you that my suggestion might be a preference about the core primitives and userland is always an option. Aside: In the world of systems complexity (which was my academic research area), there are essentially two approaches: How can the system be constructed from primitives or how a generic system can be parameterized to create the specific system. There is no right or wrong approach technically, but for a consumer the two can lead to different measurements or outcomes.
Agreed. While you could have a separate pending component (which shows data and value, so not just loading), the repaint would almost always be wasteful.
Even if you had However the use case I had in mind was more like: where a status bar does not conceptually belong inside a page (See my next comment for the elaborated use-case). Perhaps, you have an alternate way of dealing with this. Do tell! |
Beta Was this translation helpful? Give feedback.
-
|
I am putting a simplified version of use case on StackBlitz. The separate comment is to isolate the use-case from the DX discussion or allow you to move it to a separate thread. https://stackblitz.com/edit/solidjs-templates-phjktrui MotivationI want to build data-first reactive malleable software environment. That is, an environment that can components just by looking at the data type and user intent and load data into that component. And do so recursively. By reclusively, I mean that if that data contains other linked items to be transcluded, and component provides space for such transclusion, the environment can load components and data into those spaces and repeat the process for those child components. The idea here is that once you point the environment to some data source (say, an RDF source), the environment can automatically create the entirety of the UX automatically (by reading from a registry of generic components) just by "following its nose" through the data. I have demonstrated this with an operating environment prototype I had developed called Syntropize (and there are a few other people moving in a similar direction in Malleable Systems and Substrates communities), but did not develop it further because asynchronous reactivity was an unsolved problem at the time and was taking all of my effort fighting Angular 1.x (not that migrating to another framework would have helped all that much). SampleI have create a sample code in Stackblitz with two examples of In the ideal case, there should be two loading boundaries one for the loading of the component, the second for the loading of the data itself. I can swap components as the data in the hole changes. But as you can see, the loading boundaries do not work as expected, and I get a blank area, before the component is first loaded. The second is an optimized case where the data and component loading both race and we have a single loading boundary. Again the boundary does not seem to work. My pain point is that it is not obvious how to do the right thing here. Some of that is lack of using Solid for anything but toy examples. Some is the 2.0 is still in beta. A lot of it is that I am not all that smart. But it still took me so much time, that too with AI helping me out, to end up with a not fully correct example. That's why I think there is still room in Solid to cut through the complexity without sacrificing on the physics! |
Beta Was this translation helpful? Give feedback.
Uh oh!
There was an error while loading. Please reload this page.
Uh oh!
There was an error while loading. Please reload this page.
-
Following @ryansolid streams, I find the work on managing Async Data in Solid 2.0 to be very impressive, and I am still trying to work out the implications of it. The UI (or the store powering it) indeed needs to track a current and future value during the course of an update. However, I feel the proposed UI is minimalist rather than being symmetric, which offends my physics sensibilities.
I'll start by observing that
isPending(fn)is necessitated by the need to show to the consumer what is going on, often overlaid on the data itself. But I would argue that for consistency you should instead also haveisLoading(),isErrored()as well.Consider the UI where you want to show the data and status separately, might be far away from each other, say the status is next to a refresh button or on a status bar. The boundaries are not convenient for building such a UI, whereas the functions allow one to independently calculate and show the status (as well as possibly block a refresh button, if that's what the designer wants).
As far as the component boundaries are concerned (which I see as a common case convenience), I would suggest looking at it as a unified
<AsyncSlot/>which should have an API like so:with the
<Page/>being shown for any undefined attributes.Or even
if you want to be super consistent. But the latter might not be ergonomic.
This will encourage developers to think about providing pending and errored states whenever they write a component with Async Data, rather than thinking of it as outside the component. Think of it as a psychological Nudge.
These changes are backwards compatible. Ideally,
<Loading>and<Errored>should be made redundant, but that is a breaking change.Beta Was this translation helpful? Give feedback.
All reactions