1. Introduction to Suspense and Apollo Client
Today we'll be talking about how to use suspense and GraphQL with Apollo Client to build great user experiences. We'll start with an overview of React Suspense concepts and then explore a real client rendered application using Apollo Client's new suspense hooks. We won't cover React Server Components and Streaming SSR, but you can check out our experimental package for more information. Andrew Clarke's tweet about iOS transitions sets the stage for the relevance of suspense. Suspense helps coordinate transitions in our app for a smoother user experience. We'll also discuss the use query hook in Apollo Client.
Hi, everyone. So today we'll be talking about how to use suspense and GraphQL with Apollo Client to build great user experiences. But first, let's quickly introduce ourselves. So I'm Gerald Miller. I'm a principal software engineer at Apollo GraphQL, working as a maintainer on Apollo Client. And you can find me online at Gerald Miller. And my name is Alessia Bellacerio. I'm a staff software engineer also working on Apollo Client, and you can reach me at AlessBell.
So first we'll be starting with what this talk is about. And we'll begin with a brief overview of React Suspense concepts. Then we'll look at a real client rendered application that's using Apollo Client's new suspense hooks, officially released in August of this year, to see what can be gained from pairing these technologies. And also importantly, let's talk about what we're not going to talk about today, which is React Server Components and Streaming SSR. While super cool, and a lot of great things happening there, just not something we have time to touch on. If you are interested in those, we highly encourage you to check out our experimental package at Apollo Experimental of XJS App Support. Shout out to our co-maintainer, Lenz, who has done a lot of the legwork in this. Also, we're not going to be talking about implementing suspense support in a data fetching library, because trust us, it's actually a little more complicated than meet-CI.
Okay, so let's go back to January 2018 when Andrew Clarke tweeted this out. Andrew Clarke is a member of the core React team, who's currently working at Percel. And this tweet was sent out into the universe January 2018, as I said, more than a month before Dan Abramoff would deliver his JSConf Iceland talk titled, Beyond React 16, with some of the first public demos of suspense. And I think this is a good place to start, because what Andrew is talking about here is very relevant to suspense and what we'll be showing you. Andrew said, one reason iOS feels so much nicer than the web, fewer unnecessary loading states. Look what happens when you tap an option in the settings app in iOS. Instead of transitioning the view immediately and showing a fallback spinner for a split second, it pauses for a moment, prepares the view. When the view is ready, it slides in. And it just feels so much smoother than if you would see a flash of a spinner. So, this is one of the things that Suspense helps us do, coordinate these transitions in our app for a really, really nice end user experience. First, let's talk a little bit about the status quo today. So, for those of you that have used Apollo Client for any amount of time, you should be familiar with the use query hook. So, we're going to start taking a look at just an example that uses use query, that should look familiar to all of us here. So, with a hook like use query, you're fetching a list of albums, and the important thing to note here is that the use query hook is going to give you this loading boolean.
2. Understanding the Power of Suspense
Our albums component renders the loading fallback until data is returned from the network request. Suspense is more than just showing a different fallback UI. It provides powerful tools for managing transitions and creating a smooth user experience.
And so, our albums component that is going to be rendered when we click this button here is going to be responsible for rendering the loading fallback until the data is returned from our network request, and we see that dreaded flash of a spinner here.
One thing before we look at some practical examples, dive into a demo, that we wanted to note here is that we're going to be talking a lot about loading states in this talk. But to reduce suspense to just, you know, this notion that suspense is just about showing and a different way of showing your fallback UI in your app, would miss the core value prop of suspense entirely.
And so that's another takeaway that we hope you have today. That suspense gives us these really powerful tools for managing and coordinating transitions in our app, beyond just the initial loading experience or the UX on navigation, et cetera, but it's really just these transitions that are so fundamental to how the user is experiencing your app and making those as pleasant, as smooth as possible.
3. Introducing the Used Suspense Query
Today we're introducing the Used Suspense Query, a new hook in Apollo Client 3.8 that adds suspense support to your application. It fetches data integrated with React 18 Suspense and supports React 18 transitions. We'll explore this API through a Spotify clone, where different areas of the UI load data. Currently, the UI has a popcorn effect, with components popping in as they load data. We'll update the application from use query to use suspense query, starting with one component. Let's dive in and write some code together.
So today, we're happy to announce... Used Suspense Query. So this is our new hook released in Apollo Client 3.8 that is primarily responsible for adding suspense support in your application. So what is Used Suspense Query? It is essentially a suspenseful use query that fetches some data that's integrated with React 18 Suspense, which includes support for React 18 transitions.
So for the remainder of this talk, we're going to explore this API, what Apollo gives you, through the lens of this Spotify clone that we've built. This is fully integrated with Apollo's suspense features, along with React, to give you an idea of how working together with this technology works. I want to point out a couple key things that we're going to be looking at here, which is these different sections. So each of these colored areas represent a different area of our UI that loads some data. So you see we have the sidebar over there. We've got our main route area, which is that playlist. The top right corner, we've got that green user menu. Down at the bottom, we've got the play bar there. So again, each of these areas represent an area of our app that loads some data.
So let's actually take a look and see what this looks like through the lens of use query as it is today, if I were to load this page fresh. Yes, and here we can see the starting point of our UI. So with the highlighted areas around each of the four boundaries that we see in our app that are each responsible for fetching their own data, we can see this popcorn effect as they all load in and stop rendering their fallback UI and return UI populated with data. So we see the sidebar comes in first, then the play bar at the bottom. The user menu at some point is also able to begin rendering with data, and then finally our route component. And so this is a pretty jarring effect with each component kind of popping in, you know, whenever it's able to begin showing the user some data. So what we're gonna do is we are going to update this application from use query to use suspense query. So let's dive in and write some code together. So again, here's my application. I'm just going to refresh to show you that we're kind of in that same state that we just looked at with that popcorn effect. This is rendered using use query as of this moment. But we're going to start by actually converting one of our components to use suspense query. So just to give you a little bit of an idea of what we're looking at here. Here's our main layout. You can see those areas. Again, each of those were colored areas. Look at the sidebar, the user menu or route component and our play bar there.
4. Using Suspense with User Menu and Other Components
We're going to dive into the user menu and replace the import from Apollo client with use suspense query. We'll add a suspense boundary around the user menu to show the fallback UI. Then we'll add suspense boundaries to our other three components: sidebar, play bar, and route.
But we're going to we're going to dive into user menu first here. So so this is a fairly typical usage of use query. As you can see, we're calling use query hook down in our user menu component with that loading Boolean and to convert. I'm going to start up here by first just replacing my import from Apollo client to for use query to use suspense query. I'm going to go down to my use query usage. Let's make sure that gets called to use suspense query. We're going to see something immediately, which is that this loading Boolean is not actually something that's exported from this hook when you convert over to this. And again, this is because in the suspense world, we're not managing that loading state directly in this component. So I'm going to do is actually just delete that loading Boolean. I'm going to remove that loading state altogether from this component. And with that, let's go to our application and take a look and see what that change did here.
Okay, so we're seeing a black screen at first, instead of the fallback UI for the user menu in the top right hand component in the top right hand corner there. And that's because we haven't added a suspense boundary yet. So that react knows what fallback UI to show for that component. And so let's add a suspense boundary around the user menu to render that that fallback UI. Awesome. So to do so I'm going to come into my layout here. And to start I'm gonna import suspense from react. And with that suspense gives us a component that we can wrap around a bit of our UI. And this takes a single prop called fallback, which is the UI I want to show while while the component is loading. So here I'm going to use the user menu dot loading state components as my my fallback UI as this this component is suspending. So with that in place, let's go and see what that did to our application here. OK, and we saw the green highlighted box and no more blank screen on initial load. So we're now using suspense to render the fallback only for the user menu there. But aside from that, we haven't changed our user experience yet. So we see that, you know, suspense is not a magic wand that we can wave at our applications. But before we can look at the composability of our suspense boundaries and the actual power that we get from using suspense, let's add suspense boundaries around our other three components so that we can explore some some of these concepts a bit further. Awesome. So, since we're already in the layout here, I'm just going to start here. We're gonna we're gonna start by wrapping each of these areas that load some data with our suspense components, we're going to do our sidebar, our play bar, and then let's also make sure we do the route.
5. Updating Components with Suspense
Let's update the components to be integrated with suspense. Replace the import to use query with use suspense query, remove the loading Boolean and loading state. The use suspense query is designed to feel familiar to those who have used use queries. Now let's go back to the application and see the changes.
And for each of these, let's make sure we're using the right loading state here, and we use sidebar loading state for here, the route loading state for the route and the play bar loading state for the play bar. And let's go to each of these, these components as well. We want to update these to be integrated with suspense. So I'm going to come over here to my import, I'm going to replace the import to use query with use suspense query, go down to the usage here, make sure I'm using use suspense query. Again, we don't need that loading Boolean anymore. So I can delete that. And then my loading state, I can get rid of. Do the same thing for play bar, update use query to use suspense query, call it, get rid of the loading Boolean, get rid of the loading state. Hopefully you're starting to see a pattern here, and this is something that I want to point out too that's important when we are designing use suspense query, we wanted it to feel familiar to those that have used use queries so that hopefully in the vast majority of use cases, should be as easy as updating this import and doing this. Of course, obviously, you'll have to have a loading state to fall back to, but there we go. So with that update, let's go ahead and go back to our application and see what these changes did.
6. Using Suspense Boundaries and Tradeoffs
Now that all of our components are relying on suspense to show the loading fallback, we can use the React-y composable API that suspense gives us to group these updates. By replacing all the suspense boundaries with a single one around our entire application, we can achieve a single update to the screen. However, there is a tradeoff with the granularity of suspense boundaries, causing React to blow away all UI in every component in the React tree, suspending the whole app on any variety of interactions. We want to keep the shell of our app on the screen, including the sidebar, play bar, and user menu, as the user navigates around.
Nice. Thanks, Gerald. So again, we still haven't fixed the UX problem yet that we had with use query. And I promise, we're much closer to actually being able to improve this loading experience. But suspense is giving us a superpower now. Now that all of our components are relying on suspense to show that loading fallback, we can use this API, this very React-y composable API that suspense gives us to group these updates. Let's replace all of these suspense boundaries with a single one around our entire application to see what happens.
Awesome. So this is where we start to see what this API gives us. So what I'm going to do is start out by wrapping my layout container with that suspense boundary. Let's make sure I'm actually am writing valid React code. Let's start with that. I'm going to delete these other suspense boundaries, suspense, suspense. Let's make sure I'm using the right loading state here. So of course I shouldn't also remove my sidebar, put that back in place. But with that update, let's go and take a look and see what we've got.
Ta-da. So now we have a single update to the screen, and suspense makes this trivial. We were able to accomplish this just by moving around some suspense boundaries in our layout. So let's take a look now at a different playlist and navigate around our app a bit more. Awesome. So my daughter would be psyched if we all listened to her playlist together, so I'm going to go and navigate to hers. We'll go over to Ivy Dance here. And well, I wouldn't say this is a great experience. By navigating over, we're seeing that I'm getting my loading fallbacks again. So what might be happening here?
Yeah, so here we're seeing our first tradeoff with the granularity of suspense boundaries that we're using. So as Gerald said, by moving the suspense boundary to the edge of our app, all requests to fetch more data that are causing any component in our React tree to suspend are all bubbling up to this outer suspense boundary, causing React to blow away all the UI in every single component in the React tree, suspending the whole app on any variety of interactions in our app. And we know that's not a great UX. We know we want to keep the shell of our app on the screen for an app like our Spotify showcase here. We definitely want to have our sidebar, our play bar at the bottom, and our user menu persisting on the screen as the user navigates around.
7. Introducing Another Suspense Boundary
Let's introduce another suspense boundary to fix the problem. By wrapping a suspense boundary around the route component, we can keep the existing UI on the screen as we navigate. This adds another update to the initial load but improves the user experience overall. Suspense requires us to consider tradeoffs and be intentional about boundary placement.
So let's go ahead and introduce another suspense boundary to fix this problem. Awesome. So I'm going to go back in here, and because we know the route component is what suspends as I navigate around, I'm going to wrap a suspense boundary back around that route component. Let's make sure we're using the right loading state here. And so now we've got this nested suspense boundary here. But let's go ahead and see what that change did to our application. So I'm going to refresh. We're going to start back at our, in this case, Ivy Dance playlist. And let's just go back to our original React Summit US playlist here. Nice. So we saw that on the initial load, we had two updates to the screen instead of one. And so that was another impact to the end user experience that adding another granular or more granular suspense boundary had on our UX. But now when we navigate around the app, we're able to keep all of that existing UI on the screen, that persistent UI in the user menu, et cetera. So, again, suspense is going to really force us to think about these tradeoffs in our app as we're designing and really being intentional about the placement of these boundaries.
8. Optimizing Playlist Fetching with GraphQL
But what if the playlist request is faster than other fetches? Let's remove synthetic timeouts for faster execution. One update on initial load, worst case gives two updates. Remove all synthetic timeouts for better performance. Explore GraphQL concepts to improve user experience. Integrate a new directive to load fields later when ready. Add additional loading UI for playlist details. Two loading states in the best case.
But I have one question, Gerald, because we saw that the, in this case, the playlist query is a little bit slower and so we have two updates on the initial load, but what if the playlist, the request to fetch the data for our playlist route is faster than the other fetches that we're doing to populate the data in our UI?
That's a great question. So to answer that, we're going to go back to this, our playlist route here. And for those that, with a keen eye, you've probably noticed these add synthetics around it. These directives just help us artificially slow down parts of our query so that we can demonstrate some of these things. So in this case here, let's, let's remove this synthetic timeout for the playlist because we want this to execute a little bit faster. So let's go back here and see what kind of user experience we get with this change.
Okay, and now we're back to one update on initial load. If we click to another playlist, let's, let's confirm that we still have the expected behavior of our inner row components still suspending and being encapsulated by that inner suspense boundary. So it's really nice that now if our row component, our playlist data fetching is fast in the best case scenario, we're going to have one update to the screen on initial load. But if it's a little slower and React essentially can't wait for it, then the worst case scenario is going to give us two updates to the screen. And for our app here, that's definitely the right trade off in terms of the number of suspense boundaries and their placement. But let's remove all the other synthetic timeouts in our app here to get it feeling a little bit faster. Awesome. Yeah, because obviously, it'd be terrible user experience if we ship this to our customers. Great for a talk, bad for an actual application. So we're going to go back in here to each one of these and we're going to just delete each of those synthetics directives. And here we're going to see, as we reload this, probably something a little more like we expect in terms of load performance here. But as I load this, if you see that playlist loading in, we can see it's still taking a little bit too long for my liking here. And that's because I've taken some time to kind of figure out, like, there's a slow field in here somewhere that's just causing my query to take a long time to finish here. So this wouldn't be a GraphQL talk if we explored some GraphQL concepts to figure out how we can help ourselves here to update this experience. So we've got a new directive that is almost ready in the spec, the GraphQL spec, that we can integrate with that allows us to mark part of our query as some fields that can be loaded in later when they are ready. So we're going to go back to the playlist route here. And no surprise to the keen eye, we've got our tracks field here that has a synthetics timeout as well. But if this were a production application, we would assume that you've done some sleuthing to determine that, hey, it's these tracks that are really taking a long time. So what I'd really like in my user experience is to be able to show some of those playlist details first, and then as the tracks are ready and load in, be able to show those when they are ready. So again, we've got a new directive that we can use that works on fragments, I'm going to include that in here by adding an inline fragment with defer, I'm just going to wrap my tracks there with it. And let's go and see what that does with my application here. Okay, so we've added one additional layer of loading UI here. So in the best case, we're back to two loading states. But to illustrate the worst case, let's see what happens if the playlist details takes a little longer.
9. Loading More Tracks and Using Transitions
In the worst case scenario, we'll have three updates if the playlist details take longer to load. Loading more tracks can result in a not-so-great user experience. Apollo Client's fetch more API resuspends the component while loading, but we can use transitions in React to keep the existing UI on screen. By wrapping the fetch more call in a transition, we achieve a better user experience.
Awesome. So I'm just going to add our trusty synthetics directive back here. I'll just add a second here, just to get an idea. And let's go and refresh so we can see it fresh. And now we see in the worst case scenario, we'll have three updates. If the playlist details takes longer than the rest to load, and then we're deferring a subset of the fields on our playlist query there.
Finally, let's talk about one more aspect of our app, which is loading more tracks. Awesome, so I'm going to scroll down here. Here, we'll start that again so we don't have any overlap. I'm sorry. All right, so I'm going to scroll down here, because this wouldn't be a playlist unless I could listen to something towards the end as well. And let's take a look and see. And now, again, we've got a not so great user experience.
So for those of you that have worked with Apollo Client for any amount of time, you know that anything to do with pagination typically use that fetch more API that we export from our hooks to be able to load the next page of data. So no surprise here, we've got a fetch more call as well. But in the world of suspense, fetch more is going to resuspend our component as it's loading. So what can we do to help mitigate this? Because obviously this is not a great user experience. As I load down, seeing my loading fall back for the entire route. Not super great. So let's take a look at one more aspect that React gives us to be able to handle this. So as we mentioned in the beginning, React has this idea of transitions that you can use. So Apollo Client has integrated with those, and we're going to use a transition here, which is going to tell React to mark this as a essentially low priority update because we want to keep our existing UI on screen while that component is loading in the background. So to do that, I'm going to import Start Transition from React. Just to keep this a little bit snappier, I'm going to delete our synthetics here as well. We're going to go down to our Fetch More and I'm going to wrap Fetch More in a transition. So to do so, we're going to do Start Transition here, and this just takes a callback that I can mark an update in here as a transition, in this case, the Fetch More. So let's go back here, I'm going to refresh and let's take a look and see what happens as I scroll down now. Much better. That's exactly what we would expect from our infinite scroll paginated query here. So let's recap.
10. Concluding Remarks and Future Developments
We started with Use Query and Use Suspense Query to manage loading states. We explored suspense boundaries and the defer directive in GraphQL. We also discussed the upcoming features in Apollo Client 3.9, including a suspenseful version of the use fragment hook and a lazy version of UseBackgroundQuery. Check out our documentation for more details. Take a look at our Spotify Showcase repo for a hands-on experience. Thank you!
We started with Use Query and having components manage their own loading states in our Spotify showcase here, and then we updated each of them to use Use Suspense Query and explored how moving around the suspense boundaries would impact the UX in our app.
We then showed how we can defer parts of our UI with the defer directive in GraphQL, and finally how to use Start Transition to mark our Fetch More pagination request as a transition to tell React to keep our existing UI on the screen while that network request completes in the background.
And that's our demo. So, unfortunately we didn't have all the time in the world to go over all the parts of the suspense story that we released in 3.8, so I highly encourage you to go to our documentation to take a look at UseBackgroundQuery and UseQuery, which give you the tools to help avoid request waterfalls but also utilize suspense.
And looking to the future, we aren't done iterating on our suspense story in Apollo Client. The next minor version, 3.9, is currently in development, where we'll be working on a suspenseful version of our use fragment hook, which we've gotten a lot of great feedback about, and also a lazy version of UseBackgroundQuery, another one of our suspense-compatible hooks for preloading, prefetching data on some user interaction, which we're really excited about.
If you liked what you saw today and want to learn a little bit more about how this demo was put together, we've got a repo that you can go take a look at, clone, and run yourself at our ApolloGraphQL.org, the Spotify Showcase repo. So I highly encourage you to go take a look. We want to use this as a teaching tool, and we as maintainers are going to use this as a means to stress-test new APIs and whatnot. So again, highly encourage you to go take a look at that.
With that, thanks so much.