Video Summary and Transcription
Suspense is a mechanism for orchestrating asynchronous state changes in JavaScript frameworks. It ensures async consistency in UIs and helps avoid trust erosion and inconsistencies. Suspense boundaries are used to hoist data fetching and create consistency zones based on the user interface. They can handle loading states of multiple resources and control state loading in applications. Suspense can be used for transitions, providing a smoother user experience and allowing prioritization of important content.
1. Introduction to Suspense in JavaScript Frameworks
Suspense is a mechanism for orchestrating asynchronous state changes in JavaScript frameworks. It's a complicated area that has taken years of research to develop the best patterns for. JavaScript frameworks rely on synchronous execution guarantees to keep UIs in sync, but async introduces challenges. Reactivity is how frameworks keep the view in sync with data, aiming to avoid trust erosion and inconsistencies. An example from an introductory article explains the importance of avoiding manual subscriptions that can lead to out-of-sync UIs. The story of Facebook Messenger's bug highlights the annoyance and impact of inconsistent UI, overshadowing the launch of the app.
Actually, an intro to Solid Talk. It's talking about suspense. Suspense is a mechanism for orchestrating asynchronous state changes in JavaScript frameworks. You find it in React, Vue, and Solid, as I mentioned. And this is a complicated area. One that's taken years of research to develop the best patterns for. The reason being, JavaScript frameworks are built to keep UIs in sync. And they rely on guarantees that are based on synchronous execution. Async throws a wrench in it.
How do we ensure that UI is consistent in these conditions? Well, first, you have to understand what I mean by async. Sorry, consistency. This example is pulled from a introductory article explained the inner workings of his React library. Reactivity is basically how frameworks keep the view in sync with your data. Any system based on manual subscriptions was doomed to eventually get out of sync like what's displayed here. This is what all frameworks want to avoid.
Think of what happens when you see this. It erodes trust. You might not believe what you're seeing and be forced to reload the page. And it can persist in even more annoying ways than something that can be solved with a reload. In an early React talk, I remember rethinking web app development at Facebook. Jing Chen recounted the story of how Facebook Messenger had this bug that kept coming back. They fixed it. Sometimes painstakingly so because of how complicated the synchronization logic was. And at that time, Messenger was still part of the app. The bug was people would get these phantom notifications showing they had a message. And they had already seen or cleared all their messages. Seemingly simple annoyance. But as a user, you would think you would have a new message and you didn't. Having this happen multiple times a day, suddenly you would either just start ignoring it and you would miss actual messages or some of the more obsessive users would be full of perhaps disappointment and anxiety constantly clicking and seeing nothing. This seemingly harmless bug was so annoying for users, that whenever Facebook launched it was overshadowed by the demand for that.
2. Consistency Guarantees in Reactive Models
Frameworks have consistency guarantees in reactive models, allowing us to trust in those guarantees. However, not all frameworks are the same, and each has a different idea of consistency. The answer to which model is more correct is not simple.
They would fix this bug. Not being able to depend on the software that you use can be crippling. Luckily, the concerns around this sort of thing, we have consistency guarantees in reactive models you find in UI frameworks. This allows us not to focus on those details and just trust in those guarantees.
However, not all frameworks are the same. But if you understand the rules, you can depend on them. This example was really fun for me. Basically, I took some state, I took some derived state, like count and double count, and I put that state in the DOM and I took a ref to that state in the DOM. And after I was in a click handler, I clicked it and I was like, console log all three values. As it turns out, every framework had a different idea of what that looked like. And honestly, a good argument could be made for this consistency model for each of them. So, you could debate among yourselves which one is more correct. The answer is not that simple.
3. Handling Async Inconsistency with Suspense
Tearing is a common example of async inconsistency. It occurs when you switch tabs and the UI doesn't update immediately. In React, this can happen when fetching data based on the current tab state. To avoid this, you can use placeholders, stay in the past, or show the feature with optimistic updates. These problems are solved using suspense in modern frameworks.
But what does this look like when we talk about inconsistency? The most common example I can think of is something called tearing. I've ever been to a movie's listing site, where you have new releases and then you maybe have your favorites on two different tabs. And you have all the poster views. What ends up happening when you have tearing might be, you're on the new releases, you can see the new release tab highlighted, and you can see those posters, and then you click on your favorites tab, and for a moment, now the title says favorites, the tab says favorites, but you're still looking at the new releases. This is an example of async inconsistency. And honestly, it's pretty easy to happen. I think we've all written code kind of like this. This is some React code, just for example, but just you have like, say, some state, like a category, and then you fetch the movies based on that state. It's a bit of a chicken and egg problem, though. Because you need to change the state to the new tab so that the application knows to fetch the new list of movies, but you don't have the data yet to display it. If you build your app in a simple way, you only have a single state variable to indicate which tab you're on. And it's used both to trigger the data fetching and to update the UI. So this naturally causes inconsistent state. So how can we avoid this? Well, we got three options generally. We can show a placeholder so we just don't show it. We can stay in the past. But this is a little bit tricky because, as you can imagine, you still need to update that tab. You need to get the data fetching kicked off. How do you do that while still showing the old state? And the other option is we can show the feature, but showing the feature is also kind of limited if you don't have the data yet. In some cases like mutations, you can use what you send to the server to kind of inform this. And we call that optimistic updates. Interestingly enough, all of these problems use suspense to solve them in some modern frameworks. Today I'm only going to focus on the first two for the sake of time, but trust me, there's still a lot here to get through.
4. Understanding the Role of Suspense
Suspense is a mechanism for creating boundaries in your view representation to ensure async consistency for users of your application. It's not a magic solution for fetching waterfalls, but rather a way to hoist data fetching and read it where it's used. This approach allows for setting consistency zones based on the user interface and makes the system resilient to change. Signals, along with promises and suspense boundaries, play a key role in this concept.
So all of this to outline, so we can even talk about this, when you were first introduced to the topic around suspense, someone probably told you it was a way to show loading spinners. And they aren't wrong, but this sort of undersells the importance of what's going on here.
Suspense is a mechanism for creating boundaries in your view representation to ensure async consistency for users of your application. That's the definition I'm going with. This manifests itself in many ways like those loading spinners, but that's only one way we can leverage it.
But before we go further, there's a lot of things suspense is not. It is not some magic to cure data in code fetching waterfalls. Things built around suspense can be informed about async nature of dependencies in your application, but it can't make the impossible possible. The only way to prevent waterfalls is to fetch sooner higher up in the tree. Whether that's using a compiler like Relay or explicit loader patterns, like you find a RemixSvelte kit, this is a must. In some cases you can de-dupe downstream requests by using some kind of key to save you from around, but fetching still needs to happen above in a non-blocking manner, and truthfully, sometimes these waterfalls are not avoidable anyways.
The common ground is in all suspense implementations is the mechanism is actually based on reading future async values. It's not on the fetching. This is a super important distinction because it enables us both to hoist the data fetching above and then read it where we use it. This lets us set consistency zones that are based on your user interface. It makes system resilient to change as you build your layouts, and as more async functionality comes in, you have the same loading experience. You can approach your suspense boundaries as more of a designer than a developer.
It's important to understand how the read is important. You need a mechanism that even maybe a data primitive to register these reads. We can all use promises, but promises execute once. You need more of a promise factory, something that would generate multiple promises, and they need to be cached. In solid, luckily, we already have something that works like this. As you can guess, it's my answer to pretty much every question. Signals. Signals are already multivalued and they intercept reads. In our case, we have a special async signal, which I'm showing on the screen. Promises along with suspense boundaries are the underlying piece behind everything I will show today. Let's do a little live coding to show what I'm talking about. That's a lot of heavy theory. Let's move into this.
5. Loading Spinners and Suspense Boundary
This part introduces the concept of loading spinners and a basic app with tabs. The app uses a mini router to display the selected tab. The speaker explains how they upgraded the app to use a resource API and fetch user data. They demonstrate the use of signals and the loading of John Lennon data. To handle async data loading, they wrap the router in a suspense boundary with a fallback. The result is a catch-all mechanism that displays loading when refreshing or navigating between tabs.
Where this starts, yes, it starts with loading spinners. So I'm going to get in here one sec. There we go. I built this really basic app. It's got some tabs that go back and forth. Nothing too special here.
We have an unordered list, which shows which tab is selected, and then solid has these control flows. This is like a mini router, where we show a tab based on what is currently the selected tab is, which is just a signal with details. This is very like our movie category example I had a moment ago.
What I'm going to do here though, is we're going to upgrade this hello world to actually using our resource API, which is going to use our user ID as an input to fetch user, which is some fake API that I stole off a React demo that they did about four years ago, where they have some data about the beetles. So yeah, thank you React team for making great demo examples that I can steal. Anyway, let's get back in there.
Instead of hello world, all we're going to do is signals are a function, so we're just going to call this as a function and actually do this, .name, and suddenly now, we are loading our John Lennon data into it. And if you notice when I go back and forth, it's blank for a second. It's not great, but at least we're loading some async data. So what do we do here? Well let's wrap our router in a suspense boundary. All right. Suspense. And what we get to do here is we get to set a fallback. And our fallback, I'm just going to make a div with class equals loader. That's right, class. Not class. Name. And we'll just put something like loading in here. Okay. Now, when I refresh the page, oh, I got a typo don't I? Where's my typo? Close the suspense. Thank you. All right, when I refresh the page, we see loading for a moment. And when we go back and forth, we see loading again. So very simply, we have this kind of catch-all.
6. Handling Multiple Resources with Suspense
By using another suspense boundary, we can handle the loading states of multiple resources in parallel. This ensures a consistent user experience as we navigate back and forth. The initial load is delayed for a specific time to simulate different loading durations. The solution to this is to use another suspense boundary.
And what I mean by catch-all is if I go back to this details page and go, okay, let's add another resource. As it turns out, thank you to the fake React API. I also have a list of posts. So I can just add another resource. And since they both depend on user ID, we can, in parallel, fetch these. Fetch posts. And let's just do a little bit more. Let's add a fragment that I closed. Thankfully. And then we rely on Prettier to fix this for me in a second. But we're gonna make an unordered list again. And we're gonna do... What is it? Solids. For component. Which I need to import. And... Each posts... Let's go post... Is... Yeah, sorry for all the typing. Post dot text? Yeah. Okay. Now, if this all works, now we have some details. What you've noticed here is we have that same experience where that same placeholder handles both loading states. And as we go back and forth, we see it again. The only thing... This is taking a little bit longer, because I delayed the initial load 600 milliseconds and I delayed the other one a second and a half. So what we can do here is, well, we can just use another suspense boundary. We can go... Okay.
7. Using Suspense Boundaries to Control State Loading
Let's wrap this in a suspense boundary to control how the state loads in your application. Suspense works off any promise and can resolve anything. It can also be used outside the browser to determine when the server is done rendering and which parts of the page are ready. The concept of suspense was introduced in Marco Framework and is now used in Solid.
Well, let's wrap this also in a suspense boundary. And the way suspense works is it looks for the nearest suspense boundary to where the read happens, which this read happens actually right here when we're reading the post. The other read happens when we're reading the user. So we can just nest our suspense boundary and make another fallback here. And again... What is it? Class equals loader. And let's say loading posts this time. And if I did that right, let's format our document.
I have to import suspense. You now see that we've broken it up. And it shows each piece as we go. Similarly, we can get this kind of waterfall or cascade of loading states. But the cool thing about this is, if this, say, loads faster, like 200 milliseconds, it's just going to skip the second loading state because it doesn't have to wait any longer. So this is a really powerful tool here to be able to kind of control in a generic way based on the layout how the state loads in your application.
It's easy to show this to data fetching but it really works off any promise. Technically it doesn't need to be a promise, like in theory, but promises have some nice properties. First, they're designed to resolve or reject. You can assume the API is built with them and expect resolution. Secondly, they only complete once. This might be less convenient in a world of async data flows, but for our purposes we have a contract. We have this kind of guarantee. You can use this to resolve anything. Async devices, you just feed them in and then the suspense boundaries handle it for you. But there's also no rule that suspense only needs to run in the browser. This awareness of async can not only tell us when we're done server ending, but what parts of the page are ready as they're ready.
While suspense can be credited to React, a very similar concept was introduced in Marco Framework back in 2013, and that's what powers eBay.com. I know a little bit about this because I joined the Marco team a few years ago before I was working at Netlify and I was blown away and I was like, okay, I've got to add this to Solid. So I completely stole this from Marco. But basically, in the same way we load data on the client, we can send parts of the page from the server. As it turns out, you can leave the response open when serving a web page and just keep appending HTML to the bottom.
8. HTML, Script Tags, and Streaming
And that HTML can contain script tags, move the HTML into position, serialize data, and trigger hydration. The demo works similarly to Solid. The data fetching happens on the server, but it looks similar to what we did before. We can control when we start streaming by using defer stream true. Loading placeholders can be jarring. Keeping things in the past is trickier. Navigating to the next page in a carousel can lead to inconsistent states.
And that HTML, well, it can contain script tags. And those script tags can actually move the HTML into position and serialize any data and trigger hydration. So all this work can happen independent of the framework actually even being loaded on the page.
I took this demo off Marco's landing page, but it works very similar to how we do this in Solid. And to do that, I have another code example. But not really another. What I did actually was I'm using Solid's metaframework Solid Start, but essentially now we have HTML in the head and a few other things, but what you're going to see is almost the same demo again where I have a suspense boundary with loading, wrapping our router, and then I have a header and a footer component to actually prove that I'm not making this up. And if I go over to our routes and get our index route, what you're going to see is the component looks very similar to the one we just authored where we have our two resources fetching the stuff, and a nested suspense boundary to show the exact same stuff. And the reason I want to show this is when you reload the page, it looks actually really similar to what we just did. The only difference here is now the data fetching is happening on the server. It's exactly the same code, but it works straight ahead in SSR. And the kind of thing that's cool here, and I'm going to show this really quick, hopefully, is we get our HTML back, you know, we got our head and some CSS getting loaded here, but what's really interesting here is the main body has the header, and has the footer, like I showed, and then you actually see the loading in here, and some placeholder comments and stuff, some scripts to get started, but then we close the body and the HTML, so the document's done, but then what's this at the end? A template where we have John Lennon, and loading posts, so we have the next UI state, the data we need to hydrate it, which is just this John Lennon, and this actually is the code here, that just moves the stuff from the bottom of the document and inserts it, and then underneath that, well, as it completes, we get another template, and this has our listing of posts, and again, the data we need to hydrate it, so it just incrementally comes in on the page. Now this is obviously pretty cool, but maybe you don't want to show the page until the title only shows up, and there's an easy enough way to do that. We can just, to our resource, go defer stream true, and now, when I reload the page, you can see it actually waits until John Lennon is there before, so we get complete control over when we start streaming. We can, you know, do our auth first, make sure the person's all good, and then stream in some of the content that takes slower to load perhaps, but if you notice again, it's all the same code that we have been using in the other example, whether you're using SSR, you don't need server components or any kind of fancy stuff. This is just suspense plus resources.
Loading placeholders are helpful, but they can sometimes be jarring, pulling the user out of the experience. When the page initially loads, we have nothing to show, so, showing a loading placeholder is, you know, a great affordance, but on subsequent navigation, we already have content to show, the current page. So, I already mentioned that keeping things in the past is a little bit trickier to do. We can't just block an async, we can't just make an async component and expect it to work. That's because some unrelated state change or end-user action might interact with the page, and it might see that inconsistent state when it is reading it. And, you won't get your expected outcome. Perfect example is, pretend you navigate to the next. You have a carousel and you navigate to the next page.
Two minutes? Really? Oh. All right, sweet. So, there's like an eight-minute countdown over here. Okay. Well, I want to at least show a transition off here.
9. Showing Transition with Use Transition
I removed the nested suspense example and added a use transition to wrap the set tab. This ensures that every async thing that comes from this is in its own transaction. When we flip to the other page, it doesn't show the loading state until all the data is loaded. It holds the whole thing.
Well, I want to at least show a transition off here. Basically, I removed the nested suspense example, so you can see you have this fallback. And what we can do here is, we can add a use transition. And const start, or sorry, other way around. Is pending start equals use transition. And what this does is basically we're going to take the start transition and we're going to wrap our set tab with it. And what this does is kind of goes, hey, this, and every async thing that comes from this, should be in its own kind of transaction. And just simply doing that alone will cause when we flip the other page, it's not very interactive, but you see that it didn't actually show the loading state. It actually held. If you actually look at the tab it does not switch the tab until all the data is loading. It actually holds the whole thing.
10. Using Suspense for Transitions
And of course that's a little bit jarring. So what we can actually do is we can use that is pending here. I'm just going to use solid's class list finding here and then go pending, class is pending. And if I do that, see initial load has loading. But then it grays it out, gives you kind of an indicator. In many cases this kind of interaction is a little bit more gentle on the user than just swapping across. The last thing I want to show is transitions are kind of global. They have to freeze everything. They freeze that tab even though the tab isn't under the suspense boundary. Sometimes you want to opt in and opt out of it. The way we can do that is by essentially bringing back that nested suspense boundary I had a moment ago. The reason is any existing suspense boundary will hold in the transition, but any new suspense boundary will go to the fallback. For this reason, we can then go div class equals loader, and we'll just put loading posts. What you're going to see now is initial loading has a cascade, but when we go back to the initial page, it's going to wait for the John and then only do the delay then. You can prioritise important content that you want to hold the page to, and load the slower content or less important content later. So this is a very quick whirlwind tour through Suspense. I would have told you it works a bit like a Git merge and a rebase. That's the mechanics of it, but it's not that important. Ultimately, Suspensive Transitions provide useful tools for making our user interfaces consistent, and you can use the same API to solve a whole bunch of problems. That's a big benefit both to the end-users and the developer. It isn't just about performance, it isn't just about data fetching, it's about making easier to create UIs that users can trust and that behave in expected ways, and offer smooth experience no matter how they navigate your web application.
And of course that's a little bit jarring. So what we can actually do is we can use that is pending here. I'm just going to use solid's class list finding here and then go pending, class is pending. And if I do that, see initial load has loading. But then it grays it out, gives you kind of an indicator. In many cases this kind of interaction is a little bit more gentle on the user than just swapping across.
The last thing I want to show is transitions are kind of global. They have to freeze everything. They freeze that tab even though the tab isn't under the suspense boundary. Sometimes you want to opt in and opt out of it. The way we can do that is by essentially bringing back that nested suspense boundary I had a moment ago. The reason is any existing suspense boundary will hold in the transition, but any new suspense boundary will go to the fallback. For this reason, we can then go div class equals loader, and we'll just put loading posts. What you're going to see now is initial loading has a cascade, but when we go back to the initial page, it's going to wait for the John and then only do the delay then. You can prioritise important content that you want to hold the page to, and load the slower content or less important content later.
So this is a very quick whirlwind tour through Suspense. I would have told you it works a bit like a Git merge and a rebase. That's the mechanics of it, but it's not that important. Ultimately, Suspensive Transitions provide useful tools for making our user interfaces consistent, and you can use the same API to solve a whole bunch of problems. That's a big benefit both to the end-users and the developer. It isn't just about performance, it isn't just about data fetching, it's about making easier to create UIs that users can trust and that behave in expected ways, and offer smooth experience no matter how they navigate your web application. Anyway, like what you heard today, find more information on SolidJS.com, follow me on Twitter, or if you like nerding out on JS Framework stuff, I stream every Friday for five hours on Twitch and YouTube when I'm not going to these conferences.
Signals and React
Signals are popular in JavaScript frameworks, except React. The move towards solving problems beyond component boundaries is driving the need for state management that can handle global and local state. This enables new possibilities like partial hydration and resumability. While React focuses on the component model, signals provide a way to go beyond components and define boundaries based on signals themselves.
So thank you very much. Ryan, please come over to the wall of interrogation, stand there so the snipers can take good aim. Lots of questions, only four minutes and 49 seconds. So the highest rated question. I've noticed that signals are in most JavaScript frameworks except React. Why are they so popular and widespread and why does React hate them? Yeah, that's a great question. Honestly, I think we're getting to a point where the problems we're trying to solve move beyond component boundaries. I think that having, we've always needed state management and being able to actually have the same state management for global state, for local state and kind of get away from the component model is going to enable a lot of the future of the web. Partial hydration, resumability, a lot of the new kind of buzzwords you're hearing about. It's not only great for performance but it actually opens up whole new categories of how we can do more with less JavaScript. Why React is not so interested? It's almost like on the fundamental opposite side of the scale. They said that they might use signals underneath the hood but they're very much on their component model. They view their whole components as being reactive which is a cool idea but as I said I'm very interested in what goes beyond the components and the only way to do that is have the boundaries be the signals themselves. Thanks man.
Using Cached Data and Suspense in Solid JS
Yeah, caching is definitely doable with the on hydrate hook and wrappers like React Query. The suspense fallback in Solid triggers a remount only if there's a change in the reactive logic. UseTransition is implemented in Solid and React, but not in Vue. The data HK attributes in Solid help ensure the correct order of DOM elements during hydration. Including reactive primitives in the JavaScript spec would be a monumental undertaking, given the lack of agreement among frameworks.
Is there a way to use cached data and if so how do you trigger a refetch? Yeah, our resources actually have an on hydrate hook which you can use to precede your cache when the page first loads but from there inside that data fetcher you can read synchronously from a cache or go to async so a lot of wrappers, you might have heard of React Query, we have query it just wraps the resource and implements a cache on top and from there you get access to all the automatic serialization streaming and all that just with the third-party libraries so yeah caching definitely doable.
Gotcha. Does the suspense fallback trigger a remount of the child components or are their states saved? It does. Yeah it depends. It doesn't necessarily trigger a remount because in solid we just kind of we lift it out. If there's a conditional logic underneath, like a show statement, yes it will remount because you will have changed the thing but if you're just pulling it out of the view and putting it back in again it's not considered like nothing if nothing reactive has changed then nothing reactive will run. Our mounting is just an effect essentially, it's just based on the signal change, if that makes sense.
Is useTransition solid JS specific and is there an alternative or alternatives in other frameworks? UseTransition is actually a React API that I copied but I think it's only solid and React that have actually implemented this. I think there's a few smaller frameworks but of the known frameworks I don't think Vue does this. It's very powerful but you need concurrent rendering, you need that way of splitting the stuff. In React's case they just render a different VDom and merge the diff. In solid's case it's like a bunch of getNodes that we rebase. So, I think only solid and React.
This is one that I wondered as well. What were those weird interesting data HK attributes? Yeah, you'll see this in any SSR app in solid. When we do partial hydrations like server components there's less of them. But generally JSX executes out of order. You can literally create it anywhere. And for that reason I can't trust the order of the dom. So the sequence of which stuff is created gets encoded back into the dom so that when we hydrate we can actually pull out the sections of the view codes, so to speak. In our case these were all separate elements just the way it was coded. But if you have large chunks of static elements you're only going to have one date HK attribute per template part because we clone large template sections that are continuous.
Brilliant. And do you think that reactive primitives like signals and refs should one day be actually included in the JavaScript Ecumascript spec itself? Maybe. I mean that would enable a lot of stuff but right now we can't... You saw that slide where I showed four frameworks with four different results. We haven't all even agreed on how that works. Getting into the spec would be a monumental undertaking, I think.
Brilliant. Thank you. Hans Romans and countrymen, big it up for Ryan Cardiano.
Comments