Concurrent Rendering Adventures in React 18
Free Featured Workshop
With the release of React 18 we finally get the long awaited concurrent rendering. But how is that going to affect your application? What are the benefits of concurrent rendering in React? What do you need to do to switch to concurrent rendering when you upgrade to React 18? And what if you don’t want or can’t use concurrent rendering yet?
There are some behavior changes you need to be aware of! In this workshop we will cover all of those subjects and more.
Join me with your laptop in this interactive workshop. You will see how easy it is to switch to concurrent rendering in your React application. You will learn all about concurrent rendering, SuspenseList, the startTransition API and more.
Okay, so it's time to get started. Welcome everyone to this workshop on concurrent rendering adventures in React 18.
Helps if the right window has focus. Who am I? My name is Maurice de Beijer, also known as The Problem Solver. I'm a Microsoft MVP, amongst other things, which doesn't mean I work for Microsoft, but I sort of do their marketing for free occasionally, which I guess gives me the bad end of the deal. But they do give me a bunch of free software and other things.
[00:52] Also, freelance developer/instructor. I kind of believe in combining the two because if I develop, then when I teach, I can tell people what really works and when I teach, I have to keep thinking about new things, other way, better ways of doing things, which keeps me sharp as a developer. So it kind of works well together.
Also on Twitter @mauricedb, if you want to follow me. My website's here or you can scan the QR code, you'll get to my website as well. And my email address.
[01:25] I also publish a weekly newsletter, React newsletter. We're up to issue something like 310 or something. So I've been doing that for a while. You can scan the QR code here and it will take you to a registration form where you can put in your name and email address. You'll get one newsletter a week every Wednesday. So the one for today actually went out about an hour ago, but next week, another one will go out. I'm not going to use your email for anything else, so don't worry about getting spams and things like that or me selling your email address to some other companies. There's a huge list of email addresses leaked by the big company, so my small list isn't going to be very influential there. And if you don't like the newsletter, you can unsubscribe at any point in time. You will get a copy of these slides. So if you didn't get a chance to scan the QR code or that, you'll get it anyway.
[02:32] So what's the workshop goal? We're going to take a look at what's new with concurrent rendering, Suspense in some degree, what's not new. We're going to start off with some stuff in React 17, because what we can do with Suspense now is kind of important. So we're going to take a look at how to look use Suspense, how to parallelize, nest Suspense, how to handle errors that occur in a Suspense boundary.
Then we're going to switch to React 18, which is still in preview at the moment, not released yet. We'll see how we can render existing React application in React 18 using create route. We'll see new capabilities of Suspense with things like Suspense lists and transitions. We'll see what concurrent mode does, how it could potentially influence the performance of your application quite a bit and how you would do that. We'll take a look at some other bits. We're not going to look at everything React 18 has to offer. There is far more. There is a lot of server site rendering stuff there. Like currently with React 17, you can't do server site rendering with Suspense. With React 18, if you use to create route API, you can, but that's not something we're going to cover.
Type it out by hand?
[04:00] A bit of advice. Your memory works better if you do things. If you just watch the presentation and copy and paste all the code from the exercises, that's fine if you want to do that. But you won't remember nearly as much as if you actually try and do it. If you do it, you'll make typos. I make plenty of typos. Those typos will result in errors and you'll recognize those errors when they happen again, because when you really build applications, you can't just copy and paste. Like it would be very nice if you could copy and paste your complete application. But then again, if you can, anyone can and why do it, get paid good money for building applications if all you need to do is be able to do copy and paste, so help your brain cells and type things out.
[04:52] There's also some prerequisites. Now this is not a beginner React course, so I'm going to assume that everyone has Node and NPM set up, but just in case, we're going to check and then we're going to start with the starter GitHub repository, which is going to be the basis of all the stuff we're going to work with, which I've got prepared and I'll show you in a second how to set that up.
But first, prerequisites. If you open up a Terminal window and you do node -- version or node - v, it'll tell you the version of Node and that should be something like Node 14 or later. I'm not 100% sure what the latest version is. In fact, I can check by just clicking on that. And of course that opens on my other window. So the current version is actually 16.13. Don't need to be on the very latest. If you are somewhere on Node 14 or later, that's fine and presumably 12 or later is fine.
[05:56] The same with NPM. If you do NPM -- version or NPM - v, it'll tell you the version of NPM. I'm on 7.24. I think the latest is already at eight something. But anything with version six or later should be fine. So if I open up a Terminal window here and I make this a bit larger, I do node - v and did I mention anything about making typos? But you can see I'm on 14.18 and NPM - V tells me I'm on 7.24, the exact same versions as on the slide.
Then we need the repository, the starter repository. So I've got that on GitHub. I'll copy that link in a moment to the chat window. But that will take you here to GitHub. Close this repository. Copy that. Go to the terminal and do a git clone with the URL. And that should clone the repository. It's not very large, so that shouldn't take long. CD into it. And we can do an NPM install, NPM I or NPM CI, if you want to, whatever you prefer. And it'll start installing all the NPM packages.
[07:30] So let me copy this URL for the GitHub repository into the chat window on Discord and, just in case people didn't get to Discord, in Zoom as well. So that's installed. Let me open up Visual Studio Codes before I actually start the application and show you what it does.
So it's created with great React app. So the usual NPM starts to start it up. Nothing special there. It starts and keeps on opening up on the other window. That's okay. There it is. A simple application. We've got a list of users here. If I click on the user, we see some user details and we see some details about his favorite movie. Same here. Notice when I load the details, two spinners appear and the favorite movie always finishes first. So that results first and the user details always resolve last. That's actually artificially done because I've added a bit of weight there. But that is something which we'll come back to a couple of times during the workshop.
[08:58] There is also a list of prime numbers because every good business applications needs a list of prime numbers. Well, actually maybe not, but it's kind of useful here. Not because they're prime numbers but cause it's expensive to render a list of prime numbers. And expensive slow rendering is something which comes up quite a lot with bigger applications. So this is kind of artificial but it does serve a purpose. Now if I take the slider, you can see I can move it around and it recalculates that list of numbers. Kind of responsive here, kind of works well. But if I go to larger numbers, then all of a sudden it's not that responsive anymore. So I'm dragging my mouse over the slider and you can see that it lags behind. And even if I just click, like I'm clicking now, you can see it takes a bit of time, and if I go to the very large numbers, up to a million, if I click, you can see it's not very responsive at all, which is something we can fix with concurrent rendering.
Back to the slides. So that's the repository clones. That's the NPM install I just did. Now there are lots of interactive bits in this workflow and pretty much every time I'm going to write code, there is going to be or not pretty much every time I'm going to write code, there is going to be a slide like this. And these slides are actually links. If you click on them, you get to the GitHub repository and you get to the actual commit which contained that change. So here on the slide you could see something about Suspense and movie details and stuff like that. What you can see here, with green, I added the Suspense boundary around the existing code. Of course, you can go and copy this. It's code, after all. But like I mentioned before, it's better to use this as an example of what to do than actually copy it. But of course, in the end, that's up to you.
[11:15] Also got a link to the repository here. Also, a link to this slide deck. And let me copy and paste this into the Discord window and in the Zoom chats as well, because that makes it a lot easier to click on those images, like I can scroll down here and where was that first one here? Click on it. And this page is taking too long to load. That's nice. GitHub is playing up. There it is. If I refreshed, it did show up.
Another thing you'll see is Captain Jean-Luc Picard with his famous "Make it so." So that's basically your cue to start doing something. We're going to use the Zoom breakout rooms. I'll open them whenever it's time to do something. Depending on what the actual task is, I'll do it so much slightly longer or shorter. Most tasks are relatively short so I'll open them like five minutes. If you're done with whatever needs to be done, feel free to leave the breakout room, come back to the main room. So if I see that everyone's left breakout room, there's no need to wait. We can just continue with the next step. If not, I'm going to wait for that time out. So let's continue.
[12:46] Like I mentioned in the intro, the first part we're actually going to start with React 17 and how we can use Suspense inside of React 17 and what benefit it brings, too. And then we'll build on top of that when we get to React 18. But this is the basis we need to cover to make sure that we can work with React 18, Suspense, and Suspense List and these kind of things.
[13:17] Now with React 17, we got the Suspense component and the Suspense component will let us suspend some work that's going on.
[14:24] And that something from a server typically is one of two things. It's either you're doing an Ajax request where you're fetching data you want to render. Or the other is you are lazily loading components and you're doing an Ajax request to lazy load some codes so you can renderer that component, but it hasn't been loaded yet so it isn't available yet. And then when that promise is thrown, React basically says, okay, we're going to suspend this component or that component subtree and we're going to wait for that promise to be done.
And the promise can be done in one of two ways. It can either reject, in which case there is an error, or it can resolve, in which case there is success. And if the component subtree is suspended and the promise that suspended it resolves, then React says, okay, we're going to render that subtree again and presumably whatever caused it to suspend is done now. So it will render and produce whatever will mark up DOM elements we want. Potentially it could suspend again to fetch some more data.
[15:42] And in case of an error, React is going to say, okay, in that case there is an error, we're going to call into an error boundary or we're going to search for an error boundary and we're going to let it do its thing. And if there is no error boundary, we're actually going to, well, kill the application.
Now the application I'm using fetches data and if we go to source components, and for instance, in users, there is this account details, and we can see here that it uses the use SWR hook. So that's a data fetching hook from Versel. It's called stale-while-revalidate. So it fetches data and it will re-fetch data, et cetera.
[16:32] And that does an Ajax request. And we've got the traditional code here, like handle errors if there was some error. If we don't have any data yet, we're going to show some kind of loading indicator that we're loading and that's actually what shows that spinner. When I refresh, it already had some data and the spinner that shows up here. That's this loading and if the data is there, then neither of these is going to render and it's actually going to go right here. And of course if there is an error, let me just quickly introduce an error by making that URL inverted. So now if I click on the user, we see the spinner and then we see Not Found because state URL was actually invalid but let's make it vetted again.
Well, turns out SWR data fetching library doesn't work with Suspense out of the box, but it's really easy to make it do so. In the index we'll just... There is this SWR config and it's a React context under the hood and we provide some context on how SWR should work. And it has an option there, Suspense, which defaults to false. But we can set that to true and now it'll start using Suspense automatically unless overridden by some other. We could override this on individual case but we're not doing. So all of a sudden, all our Ajax requests have switched to Suspense. Now this is specific to the way SWR works, but if you're using React query, for instance, it has pretty much the same setup and lots of React libraries for fetching data will do the same. If you're using React lazy to lazy load components, it'll automatically look into Suspense. You can't even turn it off there. It will always do so.
[18:39] However, now my application is broken. If I make sure that my caches are clear by pressing a five, I go to users, we get an error. Oops, something went wrong. User list, that was that list of users which showed up before, is being fetched now, but it started using Suspense and that means that we need to have this Suspense fallback mechanism. So somewhere I need to add the Suspense boundary and I typically add them at multiple places, but I'll start at the very root and we'll add some more later on. And we want some fallback. So I've got a loading component and that doesn't quite resolve yet until I've got the end Suspense and now that should resolve the import.
So I've got a Suspense object and now if I go back to the root and now do the same concurrent rendering adventures, we see that it actually fetches its data and it uses Suspense there. And if I click on the user, it loads the data.
[19:59] Now you might have noticed first a behavior difference, because originally when I clicked on the user, we would see two spinners here, one for the user details and one for the favorite users. Now all of a sudden, the whole UI is replaced by one spinner. So not very nice, I would say. We'll actually fix that in a minute, because we can do quite a lot with Suspense. But if we look at that account details now, it's like, well, are we going to get errors? No, because with Suspense, errors are handled in a different way, so we can get rid of that error.
If we don't have data yet, do we need to show a loading indicator? No, because with Suspense, that whole mechanism of that throwing a promise kicks in. So we should never actually get here without any data. So we should be able to get rid of that.
[20:58] Now I'm using TypeScript and I hope you're all TypeScript fans. I am a TypeScript fan. Any serious React work or frontend work I do with TypeScript or quite a bit of backend work, as well. So it actually complains here, saying, well, account is an account or undefined. That's because the API here under the hood is using Suspense. So we kind of know that this account really is an account or a promise is going to be thrown. It's never going to be undefined. If something went wrong, we won't get here. We'll get to an error boundary, except that the typing can't really know that we're using Suspense. So the typing actually assumes, okay, well account could be undefined.
So little trick with TypeScript is you can't change that in line to be not undefined. Now I've got exactly the same thing, but now I can put an exclamation mark behind this, saying, well, data is maybe an account or undefined, but I know it's never undefined, so it's always an object. So if I check the account type, it's always an account, not undefined, and my compile errors go away and I've deleted quite a bit of code.
[22:27] I can do the same with the movie details. That error and that not loaded part goes away. Don't need that error anymore. And this movie is always an object. The same thing with the exclamation mark. In the user list, we've got similar code. Lots of similar code there. I didn't actually rename the data. I'm just using data here. And now we've got a compile error right here, saying object possibly undefined. So again, an exclamation mark or I could potentially put a question mark. Another way in TypeScript to do it, the null coalescing operator. Either will work. So got rid of a lot of extra stuff there, which we didn't need, and I actually got some inputs left here, which I can get rid of as well, where it was the other one. There, that's the one I was looking for. So make sure everything works. I can load the users. The data shows up. Loading behavior isn't quite perfect, like I mentioned, but we'll fix that later.
So that's the basics of Suspense. Remember that fallback component or fallback prop you have to specify to Suspense. It can be a component, could just be a string. Works really well. There is a slight behavior change with Suspense in React 18 with that fallback prop, but I'll talk more about that when we get to React 18.
[24:23] So SWR, which we're using here to fetch data, pretty nice and convenient utility to use for that. The change I made there, adding Suspenses through to the SWR config and adding the Suspense boundary right at the root of our application. Kind of a last resort to catch all the Suspenses. The update to user lists, account details, and movie details ,where there isn't an arrow, because I all just deleted code, well, pretty much deleted code so there wasn't much to show, which works really well. And of course the results. This is when it's actually loading, which isn't quite as nice but we'll fix that in a minute.
So let's go and do this. I'm going to open up the breakout rooms again. I'm going to do that for slightly longer. Where did my breakout rooms go? The window went away. There it is again. Ravi is asking for the repository. So let me actually copy that into the chat window for him. And then I'm going to open up the breakout rooms for eight minutes, so you can do this step and get everything to work with Suspense. And after that we'll start looking at how we can work with errors, catch errors, and then with different ways of orchestrating different Suspense boundaries together and the results we'll get.
[26:14] Okay, so Suspense. Suspense boundaries. Relatively easy to add. But what happens if there are errors? Well, if there are errors, in a normal React component when rendering, we have an error boundary, and in Suspense, that's really no different.
So I don't have to add any error code to the individual components doing things. I just add error boundaries just like I have Suspense boundaries and actually had one by default. If I go back to my index.ts, right... Oh that's not readable. Right here at the root of our application, I had this error boundary with a fallback component, which actually displays it, and I don't have any code in here to do so. But normally, I would also have codes in here that would make sure that error would be sent to a server collected there and something like Sentry, et cetera, would be used to collect all errors.
[27:17] So I could go and find the bugs and fix them and we can actually see that if I introduce an error, like let me make this URL invalid again. And go back here. Make sure there is no data in the cache. I go to the users. I click on the first user. We see loading. We see this error screen, which is the result of being in the development environment. But at run time, we would see this. Well, something went wrong. The status tax was not found, because it's a 404 not found. Now this is only there because of the error boundary. Let's remove that error boundary for a second and see what happens in a normal application without an error boundary. But I'm sure you've all seen that before.
We'll refresh the application. Click on the user. We'll get that same 404. Because of the development environment, we get this again. But at the run time, the end user would get to see this, a complete blank page. Not very informative, not very useful but because there is no error boundary, React basically unmounts the whole application. If I go to the console in the developer tools, I can actually see that stuff went wrong. And down here, I can see the complete component stack that was unmounted because there was nothing handling this. There was no error boundary handling this, exactly the same as what would happen with other errors.
[28:59] So let me undo this so that error boundary comes back and now it should be able to catch it again. Let's make sure. Yeah. Now still, this isn't very nice, just because one component blew up. It's kind of... It still unmounts the whole application because I've only got one error boundary and that's right at the root of our application. In reality, it's only one component which errored out. Well the nice thing with error boundaries, I can nest them. So I can take this error boundary and go into that user details component, which is responsible for the account details and the favorite movie details. And I could say, well, I want an error boundary in here and I'll do the same around movie details and resolve these imports so it actually compiles again.
And now if I refresh, so I've got my application again, I click on the user, we get the development error page. But now I actually see that, okay, just the user details component error, and the error boundary around that caught it, but all the others still rendered. And I can go to another component or another user and it will still render. Of course, that user details component isn't going to fix itself. The URL is invalid, so it's never going to go there. But potentially, I can close that error boundary and have it retry. It's not going to fix itself, of course, but with some other error it might actually might be because the network was unavailable and now if I close it, it actually comes back and is able to fetch the data.
[30:56] So nesting these error boundaries is really nice and really useful. So that's the error boundary at the root, which is already there. So no need to add that, but introduce that same error I just did by making it URL invalid, so you get a 404 not found when trying to fetch a specific user, and then add that error boundary inside the user details component. And right here, you also see a Suspense component. No need to add that. That's actually a slight error in my screenshots. That's from a later state when there was both a Suspense error and an error boundary. Just add the error boundaries around account details and around the movie details there. So these two.
And then you should be able to get catch the error around just that single component and have all the others render. And I'm actually using the standard React error boundary and PM package here, which is really nice, which actually lets it retry. So if you click on that cross, it will actually retry. I didn't create a custom error boundary or anything. This is a real nice package and highly recommend it to use in your application.
[32:16] So please go and add these. I'm going to open up the breakout rooms for five minutes again. After that, we're going to take a look at nesting Suspense components, just like we can nest error boundaries, because in a lot of respects, they work in very similar ways, but that's the next step. In this step, let's create that nested error boundary and check what happens when errors occur.
[32:48] So everyone back. Everyone's successful with this step, that the error boundary, catch errors? Okay, thumbs up, so looking good. So there are actually two questions. One from Alexei. I thought that Suspense component is catching errors that are thrown by hooks or other child components. And would error boundary component, catch the error if Suspense is its child?
Suspense component don't actually catch errors. They catch promises that are thrown and if a promise is thrown then the Suspense boundary kicks in. If another thing is thrown in error or something else, the string or a number, but you should always throw error objects. Then the Suspense boundary is going to ignore it and it's going to leave it up to an error boundary to handle that.
[33:47] Speaker 1: That makes sense. The only thing if promise is rejected, does it also considered as a error or…
Maurice de Beijer: Yes, if the promise is rejected, that's an error and that will be treated the same way as if an error was thrown in the first place.
Speaker 1: So error boundary will be able to catch this probably.
Maurice de Beijer: Yes. And in that case, react starts at the component which suspends and starts walking up to three to find the first error boundary from there, not from the Suspense component but from the original component which started the Suspense.
[34:27] And there was other question, and I'll just open up the discord here for a moment to show. The difference between the two and the first has the Suspense boundary nested inside of it is the error boundary and inside of that is the component. And with two we've got the error boundary at the outside, Suspense inside of it, and the component inside of that. Is there any difference between the two? Yes, but that said in practical purposes probably not. The only difference is if you look at the error boundary here, it has a fallback property that could potentially suspend. Now it's pretty unlikely that that would happen, but after an error it might lately load different components to show that.
I wouldn't recommend doing that because if you've got an error because of some network connectivity issue, you're going to try to load an error component which also can't load cause of that same network connectivity issue. But in that case it could suspend and it would be caught by this same Suspense components. And here it's the other way around, if the error boundary has some kind of reason to suspend with its error display, there it can't because the suspend is, or the Suspense component I should say is inside of it. But now in case of the fallback from the Suspense, if that's throws an error, well the error boundary can catch that.
[36:08] What I typically do is in the root of my application. I have to error boundary at the very root because I want to be notified of all errors occurring wherever they occur. And you normally shouldn't do any async working there, at least nothing which Suspense. If I send data to an error collection surface, I'm not going to do so immediately there because if there is connectivity issues it might never arrive. So I'm going to put it in some queue and eventually I'll send things and I'll store that queue in local storage or index DB. So even if the user refreshes the browser, those errors don't get lost. They might eventually get lost but not in a normal circumstance. Of course if the user never comes online, I'm not going to get it. But the normal circumstances I will eventually get that error.
So Martinez says, but the first example is actually what we have now in the app because we have a Suspense around the app and the error boundary around error details.
[37:24] Not quite because we also have an error boundary right at the index level. Where is my index TSX there? So here where I render the very first thing is an error boundary, which is kind of the catch-all in case of an error which isn't locally handled will be caught by that. And then inside of that I've got the Suspense boundary, which also is kind of a catch-all. We haven't done nested Suspense yet, but normally I would suspend closure to where I actually want to, and this is kind of a last resort Suspense boundary to make sure that my application doesn't fail. So the error boundary is at the outside Suspense inside and then potentially or most likely I'll have nested versions of those two inside of other components where I can handle things locally.
[38:30] So I hope that clears those two up. Let's go to the next step. Because as I mentioned, we can nest air boundaries but we can also nest Suspense boundaries and that's kind of useful because if I go back to my application for a moment, let's actually start here. Refresh to make sure I've got nothing in the cache. And let's see what happens with a really slow network. So I've set it up to simulate a slow 3G network right now, which is pretty slow, unrealistically slow for most applications. But now with our click on users we see the whole page is pretty much blank. The navigation bar goes away and we only see that spinner. I click on the user and again the whole page is blank and we don't even see the navigation bar. And if we see a list of users, now if I click on the second user, that's goal navigation bar gone.
So that's okay. Suspense boundaries work. They're caught, but it's kind of dramatic and it's kind of removing too much. So what we can do is we can say well let's grab this Suspense boundary and let's take a look at what's inside of this app. Well here we can see the browser router for React router and we can see the NavBar, so the navigation bar at the top and then how the routes are handled. Well in this case they're not done with lazy but that's quite likely to be done. So it's quite likely that dose will suspend.
[40:13] So putting Suspense boundary around this is actually a very good place. So let's resume those imports and let's see what the effect is. So we'll go back here, I'll simulate the 3G again, we'll go to users and now we still see that loading spinner but the navigation bar stays at the top. I click on the user, user list goes away. But the navigation bar stays so slightly better but not quite good because now if I switch between users, I'd like to keep that list of users and just show a spin right here for the user details.
So inside of user details here, I could say well I really want another Suspense component in here and need to resolve these imports as well. Now all of that is inside of a Suspense boundary. So now if I click on a user that actually stays, but it's only... Switch here again. I click on user very briefly. We could see the spin there and now we get spinner here. And we could potentially say, well I want that first header outside of the Suspense, so user details actually appears right away. So now if I click on the user, we see user details and solely the details and the favorite movie part, which appears when the data is loaded.
[41:58] So it's not exactly what it looked like when we started but it's pretty close. And I actually think this is a pretty decent way to nest Suspense boundaries and get a pretty decent UI.
So nesting Suspense boundaries, you can nest them however you want to. And basically anytime a component Suspense React will start walking the component tree. So it will start at the component that suspends, look at its parents, if that'’ Suspense component, it will use that. If not it will go one up, et cetera. So it will locate the closest Suspense component and we'll use that to suspend the application. If there are multiple components suspending, they will each do this little trick by themselves and multiple Suspense components can be active at the same time.
[42:59] Now recent behavior change with React 18, we're still 17 so I can't actually show that yet. But with that fallback UI we specified right here, this one. I've got a component, a loading component which shows that spinner, I could just put some text in here.
Basically anything which would be valids for React to render could go in there and a string is perfectly valid. But React also says no is perfectly valid. I can have a component which in its render returns no. And that means basically no need to render anything here. Well if you do that in React 17, React 17 is going to ignore that no Suspense boundary. It'll barely say treat it as if it doesn't exist and keep walking up the tree, find the next one. With React 18 it'll actually respect that and say okay, it's fine, you don't want to render anything then okay, we're not going to render anything until this results. Pretty unlikely to affect you. I haven't seen any production code where people you no as the fallback for Suspense, but it is possible and it is behavior change.
[44:25] So here was the first dispense boundary. I added the first nested and then we had this result and then I went into the movie details component and added one more Suspense boundary right there. And we got this result when we started click on users, which I'm no UI expert but I kind of like this result and it kind of serves quite well in applications. So even with React 17 nesting these Suspense components, serves us really well.
[45:08] So let's go and do this. It's another five minute task. So I'm going to open up the breakout room in five minutes again, before I do, there were a couple more questions. So from Rafi, so the two one is recommended, I presume you mean from right here in Slack. So that's indeed the one I recommend. And this one I already answered. So let me open up the breakout rooms and I'll see you all back here in five minutes. So that's everyone back again. Everyone's successful with this step at the nest Suspense bondary working,
[46:07] Speaker 1: I was getting an error actually, but I don't know why is that. In a user details, user list component, I'm getting error on the line where we are trying to iterate over data.
Maurice de Beijer: So this line?
Speaker 1: Yes, yes. The question mark fixes this error. But I'm not sure why we are getting undefined here because you're using
Maurice de Beijer: Like this. So you were getting a compiled error, not a runtime error.
Speaker 1: No, a runtime error.
Maurice de Beijer: Runtime?
Speaker 1: Yes.
Maurice de Beijer: Runtime. That shouldn't be the case.
Speaker 1: Yeah, that was the problem.
Maurice de Beijer: Yeah, putting a question mark in here means it will work. But even explanation mark, which is just telling the compiler like, I know data is never undefined, treat it as an object. It's okay, it's really in the right. Trust me. That's not a runtime check.
Speaker 1: I probably just messed up with the Suspense somewhere at the top maybe. Yeah, I just-
Maurice de Beijer: It worked. The thing you might want to check is in here if you've got Suspense there on the SWR config.
Speaker 1: No. Oh, that's the piece that I'm missing actually. Yes. Thank you. Yes. Suspense.
Maurice de Beijer: Okay, solved.
Speaker 1: Thank you.
Maurice de Beijer: You're welcome.
[47:55] So let's do Suspense in Parallel because we did error boundaries in Parallel. And I've mentioned a couple of times that Suspense and error boundaries are really similar to each other and really do the same kind of thing except one does it for a promise being thrown and another does it for an error being thrown. But other than that they're, well not exactly the same obviously, but very close in the way you work with them. And we had... Where was my code and that's user details, We've got error boundaries here in Parallel but we've only got a single Suspense. Well why can't we add that in Parallel? Well, we can. I can just duplicate these.
So I've got a Suspense around account details and I've got a different Suspense around movie details and now if I go back to the application, we get two spinners again if I click on the user. One for the movie, his favorite movie, one for his user details, they each suspend independently of each other and they each resume independently of each other. And just like before, if I do the Suspense inside or outside the air boundary, let me change one of them. It doesn't matter, it's just a matter of okay, if this fallbacks frozen error then this error boundary can catch it and in here if this fallback component suspends, then this Suspense can catch it.
[49:538] But like I mentioned before, that's probably not the wisest of idea. Here I really don't mind what the order is which you nest inside the other. In the root of my application, I think the error boundary should always go outside here it's not that important and they're still going to work exactly the same way. Now this works and it's really nice, but one thing I personally don't like about the UI, but then again I'm not a UI expert as I said before, is the favorite movie results first. So we see two spinners, then we see user detail spin our favorite movie with the actual details and then user details resolve its data. So the favorite movie is actually pushed down.
I don't really like the UI wise. That I can do it, that I've got the capability to organize it that way is really nice. That the UI works that way, nah, I'm kind of not so happy about it. But in this case, I actually did that intentionally. You might have noticed, but if we go into these components on the URL, I've got a service here where I can specify a specific sleep. So every account detail is going to wait one second before it responds. And every movie detail here is going to wait half a second before it responds. So it's always going to respond faster. I did that intentionally to show exactly this behavior of the two users and the data being pushed down. In the real application, you don't know what the order is. And sometimes the first will resolve, sometimes the second. On a development machine, they're typically going to respond pretty fast because you typically run things locally and everything is fast in production. It goes over the internet or maybe some company network but things are probably going to be slower and not quite as well.
[52:00] With React 17, there isn't really much we can do about it. Both these Suspense components are going to be independent of each other. They're going to do their own Suspense, they're going to do their own resumptions and that's it. We don't get more control. The only thing we can do is say well I don't want this behavior then put one Suspense boundary around both components that suspends. When we get to React 18 we'll see the Suspense list components and the Suspense list component will actually coordinate multiple Suspense boundaries like this. And we do get to control how they will react or how they will render together and when they will resolve, which is really nice. With React 17, we don't exactly have that yet.
So the Parallel Suspense. The change I made in this case, I've only got two Suspense but there are boundaries can stay in there. I just add in my components. Basically get them to suspend in Parallel and we have this result which does work pretty nice except in this case, the well the way the UI shifts isn't exactly perfect but we'll fix that when we install React 18.
[53:28] Hello everyone back from the break. Time to continue. Before I continue, Alexey asked an interesting question in the chat window on Zoom. We're using use SWR hook to provide the Suspense compatible fetch API for our components. But what if we already had a network layer in our existing app? How could we make it compatible with Suspense and will we be going through that?
Well I didn't plan to but I can briefly sort of do that. I'm not going to create a complete library.
And the second part, I'm just curious about low level APIs of the Suspense feature. How you can throw a promise in your own code?
[54:13] I can actually show that relatively easy. Where is my application? Right here. So let's actually just see what happens if I throw Suspense. So suppose in the user details here and let's add button to the top. Suspend me or something like that. And we have an onClick. And now we need to do something here. So we'll have an event handler and I'll keep it simple in line. Now this need to render or this need to suspend when we render. So I can't really do it here in this onClick directly because then we're in an event handler and not actually rendering. So I need to introduce a bit of state. So we'll do const suspend Me. Typing is hard. This is new states and we'll start off at false. And then here, we set setSuspendMe to true. Now of course that doesn't actually do anything yet. So if I open up a user, we've got suspend me and I can click on it. Nothing happens. Well, some state changes but we're not using that state. But now I can use this state and in here I could see something like if suspendMe throw a new promise and that will actually make this component suspend and that complains because it needs a call back function. I will leave it empty for now. So now if I open up user and I click Suspend Me, we've actually suspended our component. Now this promise never resolves or rejects so this will stay suspended forever. So let's actually resolve it. So the promise constructor takes a function which gets a resolve function and reject, but I'm just going to resolve it for now. So we'll do a setTimeout and we'll call that resolve after say two seconds. So now if I click on the suspend button and I actually need to set the Suspense flag to false, otherwise it'll rerender and immediately suspend again.
[57:31] So we'll setSuspendMe back to false. And we render. And actually do I want to do that in here? I think they should be fine. Let's check. So we've got usually tills loaded, it's suspended and after two seconds. I was hoping it would resolve, but apparently it doesn't. So why is that? Maybe I need to do this first. Let's check. Shouldn't you invoke to resolve? I am actually doing that. I can do it explicitly, but I should be doing that by passing the function reference to the setTimeout. That should be the same. But let's check. Maybe that is actually the difference.
Speaker 1: Should we set this inside this call back? Set the SuspendMe inside the call back? So after a timeout, like after 2000 millisecond we shoot false. SetSuspendMe to false.
Maurice de Beijer: In here you mean?
Speaker 1: Yeah, yeah, yeah. Inside there
Maurice de Beijer: That sounds reasonable. Let's see if that fixes it. So it Suspense.
Speaker 1: Yeah.
Maurice de Beijer: That was it. Thank you. So now it suspends. It waits two seconds and then it resolves. So the core of Suspense is really very simple, it's just this thing. Throw new promise. And with that, well that's not React specific. You could do this anywhere. You could do this in a hook, you could do this in a library. I can put this in whatever function I want. It's just a matter of it has to be called from this render. If I move all of this codes into that click handler, get rid of this for a second. Now it's not going to do anything. In fact, that doesn't even work. Why doesn't? Got a bracket too many, I think.
Speaker 2: You were having a bracket too few.
[01:00:32] Maurice de Beijer: Oh right. That one. Thank you. So now it's not going to do anything. See no suspension, nothing. Because it's not done in the rendering, it's in an event handler. So if I move this back, let's copy everything and set to suspendMe again, then it should be fine. Suspend for two seconds and it comes back. So with that, it's really not all that hard to build into existing libraries. You just have to make sure you're inside the render cycle and with the hook, that's easy. We'll leave that in. Where are my slides? There.
[01:01:33] So with that out of the way, we're going to switch to React 18 and we're going to take a look at what React 18 brings to us, both with this Suspense boundaries but also with that prime numbers component, which is really slow and sluggish. But of course, the first thing we have to do is we have to install React 18.
And if we look on MPM, this is the React package. But with ReactDOM, I would see exactly the same thing. I can go to versions and here you'll see that there are different versions. Now we've got here a tag latest. That's the normal version you use when you do an MPM install of React. This also this version with the tag next which currently points to 18.0 all file, with same hash. And then there is this date of October 23rd and it's the 27th now. So that's four days old. When it was actually released, it says down here two days ago. So I think, what was that? I think last Friday or so and released on Monday.
[01:02:55] Now at the moment there is also this alpha and experimental version and alpha and next are actually the same thing. Experimental is different. No actually, that's also the same version which you can tell by this hash. It still has the same hash but it doesn't have the 18.0.0 version. So at the moment all of these three work out to be the same thing. The goal here is that next should eventually become the more stable version of React 18 release candidates, et cetera. Alpha is always going to be somewhat less stable and experimental is basically going to be daily builds. So if you're really looking for the next version to be released, the next branch here is the one you're interested in, the release with the tag next.
So we can go and install that. Now if you just do an MPM install of React at next and React-DOM at next, it'll actually fail because of dependencies. I'm not a hundred percent sure which of that is the case, but with a dash dash force it'll actually work. Where's the console? There. So let's clear this one. MPM install. React at next. And React-DOM at next. With dash dash force.
[01:04:33] So that should install pretty quick because those packages are actually pretty small. So those are installed. And for instance, you can see with SWR, it's says it wants React 16.11217. So that's one of the libraries which would actually complain and reject the install without a dash dash force. Another library which could potentially be a problem, if I go to the package of Json, we can see what I've got. Is I've got React router DOM here, a commonly used React router. Now if you used the normal version, version 5, it's not compatible with React 18. So I'm actually using the beta version of React router DOM 6, which is compatible with React 18. And as we already noticed, it's also compatible with React 17. I'm not sure what the earliest version is for React router DOM 6, but it goes to React 16 something.
So relatively recent versions of React will work perfectly fine with it. Now the API has changed. It's not really part of the presentation, but still useful to see. Actually, I wanted to go here. App route. The router is now based on hooks. So there is now a use Route hook passing an array of the different routes and the different options. Like here, I'm saying, "For users, use the element UserList, and for primes, use PrimeNumbers." And for our home, just have some inline markup in here. So it's changed a bit. React router DOM 5 will not work, at least not completely with React 18, so we weren't about that upgrade. But that’s React router... Or sorry, React-DOM installed. So let's start the application again and see if it still runs.
[01:06:52] Loading... And there, we see Parliament building in London again, and we see users, and all of that still works. Suspense boundary works. Does this still work? Yep, that works. Prime Numbers still work, with somewhat larger numbers. It's still slow, so it kind of looks like our application works, but it really hasn't had any meaningful observable effect. That is, until I go to the developer tools. If I open up the console log, and let's set that to no throttling. That's the one I wanted, the console. It comes up with a message here, "Warning, React-DOM.Render is no longer supported in React 18. Use ReactRoute instead. Until you switch to the new API, your app will behave as if it's running in React 17." So even though it's not supported, it does work. But we're not really using any of the React 18 capabilities. It acts as if it's React 17.
Well, that's not what we want, so let's go and fix that. So we'll go back to the index.tsx here. We've got our React-DOM.Render, which we shouldn't use. It says use .CreateRoute. And React-DOM.CreateRoute returns an object. And that still has a render function. So it becomes something like this, except the DOM element we want to render into becomes a parameter for CreateRoute. So it looks like this. The only thing is there is a compile error here. "Type ‘null’ is not assignable to element, or document, or document fragment, or comment." The reason is CreateRoot, if I look at the typing, is typed as being one of those parameters, but not ‘null’ or Undefined, that's not a valid option. And if I look at Get Element by ID, it returns an HTML element or ‘null’. Because if you specify an ID that doesn't exist, Get Element by ID doesn't throw an error, it just returns ‘null’.
[01:09:33] We know that this element exists, so TypeScript fix, the exclamation mark saying, "TypeScript, I know better. I know that getElementById always returns an HTML element, not a ‘null’." And now we're happy again. And now, if I go back to the application and refresh it, no more errors in the console, it renders fine. And we are using React 18 rendering and everything. That said, this is still sluggish. We are using concurrent rendering, but we really can't tell yet. Because we're not taking advantage of it yet, but we are using it.
So here's the updates to the package.json. I've got slightly older versions here. The versions are released quite often. But that shouldn't, at least hopefully. Because I know there is one change to the code, which I'll mention when we get there. But that shouldn't affect us, I hope. So the addition, the change to the index.tsx, the React-DOM.CreateRoot.
[01:11:00] Now, if you're into TypeScript, you might wonder, "How come this actually compiles and works? Because CreateRoot didn't exist in React 17." Well, I have this tsconfig file, which you get by default if you create a CreateReact application using the dash/dash template TypeScript option. But I added one thing to it, and that's this line. The Reacts-DOM and React typings are not included with the original MPM package. They come from Definitely Typed. And by default, it'll look at the standard typings there. But the typings from Definitely Typed contain the next version. So I already told React... oh sorry, TypeScript to look at these next versions of React. So it is already aware that the new API exists. So that's why I didn't get a compile error there, that's previously added. But if you create a new application, or if an existing application, you're going to upgrade to React 18 prior to it being released, then these types will not be updated and you'll need to do the same thing. But right now, that's already done so no need to worry about it. So the change there, and please go and do so.
[01:12:33] So React 18 actually gives us quite a lot of new features. There are new hooks. There is a bunch of server side rendering stuff, which I mentioned, which we're not going to look at. But server side rendering with Suspense now is possible. But the new hooks are kind of interesting. And I'm going to look at a few of them.
There is a new hook called useDeferredValue which can be useful in performance scenarios. Now, we're not going to use that, it doesn't actually help that much with the prime numbers, but that's the kind of case where it could potentially help. In this case, it just is too slow, and using DeferredValue won't fix it. But what DeferredValue basically will let you do is say, "There is some frequently changing value, and I don't particularly care about all the changes, just give me some changes, and give me the final value when it settles down."
[01:13:39] Now, if you look at the documentation for the next version of React, it's still kind of dated, it says there is a parameter there where you can specify how much it can lag behind. That doesn't actually exist in the current code base. So documentation is slightly wrong there and there isn't really much you can configure. You push a value in, which frequently changes, and you get a value out.
There is a useTransition hook, which is very useful for state transitions, and we'll actually use that for a couple of different scenarios, so I'll leave that for later.
[01:14:17] There is a new hook called useMutableSource, which is not really meant for end developers or application developers. It's more intended for library developers like Redux, Mobex, that kind of thing. It's meant to prevent tearing of UI. Now, I'll come back later and explain what the tearing of UI means and how that can happen with React 18, because that's something which previously couldn't happen.
And then there is a new hook, useOpaqueIdentifier, or maybe it's actually called useID, I'm not quite sure yet. Because when I created these slides, it was still called useOpaqueIdentifier, but it also had an unstable prefix. And they announced last week they were renaming it to useID and removing that unstable prefix. So I'm not 100% sure what's in the current version of React we just installed, whether it's already been renamed in there or that it hasn't yet. But that's actually the hook we're going to play around with first. And let me show you where you would want to use that.
[01:15:33] If I go back to the list of users, we've got a bit of an entry form here. Now these fields are disabled, but they can get focused. And you can see that there is a focus bar around it. This is standard bootstrap styling, by the way.
Now, quite often in an HTML form, if you click on a label associated with an input, like I'm clicking on Surname now, then the input associated with that will get focused. But that doesn't happen here. Now I could wire those up manually, but that's kind of tedious, and you'd kind of want to do that automatically. Now that wasn't particularly hard to do with a custom hook in React, as long as everything was client side rendered. But if you wanted to do that server side rendered, and then do exactly the same thing client side again so it was consistent and it wouldn't have to unmount remount components or render them unnecessarily, that turned out to be quite tricky. Well, that's exactly what useOpaqueIdentifier does.
[01:16:43] So if I actually go to the components in question, and let's label inputs, I could say something like... I'll create an ID here. And for now, I'll just create it fixed, as just string ID. And with the label, we do an htmlFor for that ID. And with an equal sign there, that input has an ID. So now they are associated, and we should get that focused behavior. So if I click on the first name, the first name gets focused. Except if I click somewhere else, I click on the email address, it has the same ID so it still sets that first name focus. So instead of this hard coded, that's where we want to use that OpaqueIdentifier. So let's see if that still works as it did last week.
So I import useOpaqueIdentifier with the unstable prefix, and I call it there are no parameters or anything. And then let's make sure this is refreshed. If I click on the first name, the first name gets focused. If I click on surname, then surname gets focused, email, title, et cetera. Except with overview, because that's a text area, but we could do exactly the same in there. So pretty simple, but remember, this will be renamed. And quite possibly, it would've been broken right now. I'm expecting, if I do these same steps again next week, then it's pretty much guaranteed to be broken. And I have to use the useID hook instead of useOpaqueIdentifier. Which, to be honest, is a bit of a mouthful, useOpaqueIdentifier. Wow, why opaque? The reason it's actually called opaque is because you shouldn't put any meaning to that ID. It's actually a string, but the string itself doesn't have a meaning, you shouldn't look into it, it's just an ID. Still, small thing. But useful, especially if you do server side rendering. If you don't do server side rendering, and you have some kind of hook to generate you IDs like this, just stick with that. There is no need to immediately switch. It's not like this is going to be better. But if you're doing server side rendering, then this is definitely the recommended hook to use there.
[01:19:53] So the small change to labeled input, and the two props sets to the input and the label, and the results where it gets focused. If you want to be complete, there is also... Where is it? Text area component, labeled Text Area. You could do exactly the same change in there as well. But of course, that's more of the same, so kind of optional.
[01:20:08] Okay. So like I said, the next step, we're going to take a look at how we can orchestrate different Suspense boundaries using a new component, the SuspenseList component. And SuspenseList is pretty neat. It will let you control a number of different Suspense boundaries, and give you a number of options about how they are displayed, how they resolve, and what fallback component should be rendered or not rendered.
Just like Suspense, you can start nesting SuspenseList components, etcetera. So it's just as flexible. Now, let's actually use it to get rid of this problem, where if I click on the user, we see two spinners. And they kind of always resolve in the order of the bottom first, and then the top one, which means that the UI jumps around a bit. That favorite movie is pushed down when the user details resolve.
[01:21:15] So what we can do is in here we could add a SuspenseList component. And that's at the end SuspenseList. And right now, that's actually not even going to make a difference. I added it, but we see exactly the same behavior as before. If you don't configure it, you don't really get anything else. There are two properties to work with, the most important is to review order. And that can have three different values, backwards, forwards, or together. I'm not sure when I would use backwards, but forwards or together are actually pretty useful. So if I set it to together, we get some what I think is pretty nice behavior. I click on the user, we see two spinners. But now, instead of the favorite movie resolving first and the user details resolving after that, pushing the favorite movie down, they actually resolve at exactly the same time.
What SuspenseList does, if you specify together, it will basically wait until all the Suspense components inside SuspenseList have resolved. And only when they've all resolved will they actually resolve. It's not like one will resolve before the other. So it kind of combines them, which is nice. We get two spinners at the same time. Whenever the last one is done, they both disappear, and the UI renders, which is a lot better than what it was before.
[01:23:08] Another option we can do is forwards. Forwards, with an S. In that case, we can decide what we want to do with the remainder. There is a tail option. And that takes two values, collapsed or hidden. I'll first take hidden, which is the one I'm not a big fan of. But it will certainly have its uses. Now if I click on the user, you can see user details appear. But below that, nothing. Let me do that again. Nothing. But when I click on the second user, it actually behaves slightly different. Now we see loading spinners, and they resolve at the same time. So it's when the component first mounts, it behaves one way, but when it is actually refreshed, and Suspense is well mounted, it behaves in a slightly different way. Not sure if this is a bug or not, but it's not what I would want in the UI. So what I typically do in cases like that, I make sure that it's always mounted. And that's actually pretty simple.
Here I'm in the component which renders that list, and the user detail what's selected. If you use a key there, which is normally done on lists of items, but that will work on any component. If you specify a key and that key changes, it means that the component instance, that user details, will be unmounted. And a new one will be mounted. So now I've got the behavior that we get a new user details component with every user, so we get the same behavior.
[01:24:52] We see the user details appear first, movie always after that, even though favorite movie resolves faster. But no spinners, no fallback UI. The no fallback UI is because that tail is hidden. I can also do collapsed. And this will do almost the same, except it does show the first fallback behavior. So now we see a spinner on user details, and when that resolves, it shows UI, we saw the spinner on favorite movies. So one spinner, another spinner, which is a bit weird if you consider that favorite movie is actually faster. It resolves before user detail. But still, that spinner shows up. But given that it shows up from the top to the bottom, the UI is a lot easier. Things don't get pushed down. It fills from the top down. You could do the same with backwards, but that's not quite as nice. See, now we see the spinner, then we see favorite movies, and then we see it being pushed down, we usually do. I'm sure there are good cases when this would apply, but in most cases, it wouldn't.
I actually think together works best. And as you can see from the compile error, if you use together, the tail option is not supposed to be used. Because there is no tail, they work together. So two spinners, resolving at exactly the same time. And that's really all there is to the SuspenseList. But that's the missing piece with individual Suspense boundaries, where you can't control how they work together. Now with SuspenseList you can, at least to a certain degree. But the normal things I would want to do, I can do, so I'm pretty happy. It's just that with some values, the first time it renders and re renders behave slightly different. But whether that's a bug or not, I'm not 100% sure, but I fixed that by adding a key to that component, so it is remounted every time, and I do get the same behavior every time.
[01:27:31] So here's the change I made. And the nice thing in this case, I added the SuspenseList in the same component as the Suspense, but I could add this somewhere higher up and it would still affect the Suspense. It doesn't have to be an immediate parent, it will just walk up to component three again, and React will find the first SuspenseList, if it's there, and use that setting to work with, so kind of nice. So that addition, and the result. So please go and add the SuspenseList component, play around with the different settings, see how it behaves, see what you like, what you don't like, and what makes sense in the kind of applications.
If nothing happens, it's still the same process. In the end, the DOM is updated and the new UI is visible. But if that click appears, we can now execute it much earlier. We don't have to wait for the complete dom to be updated. Now if that happens, code over here can execute, and it can do whatever it wants to do. And in the case of an event handler or an Ajax request completing for instance, that could potentially change state again, which would cause React to say, "Oh, well, apparently whatever we were rendering has been invalidated by another state change, we're not even going to bother continuing with this because it's already invalidated. We're going to go right back to the start and start rendering everything again."
[01:32:32] Or this click event handler code might not change any state for React. So React says, "Okay. Well, we executed this bit of other code, all very useful, but not interesting for us. And we'll just continue with the current render cycle, and we'll display the results of whatever happened to the user." So React kind of detects, are any of our APIs called use state, set states, any of these things which cause the component three to potentially have to redraw? If not, it will continue. But if they did, it will work.
And that's where tiering of state comes in. Suppose we've got some states. And let me grab my pen. We've got some state object. And there is a value in here. Let's say it has the value one. And this component uses it. Why doesn't it draw? My pen is not helping. So that component draws. And this component uses that same state. Now suppose this goes and updates the state, says, "Well, that isn't one, that's actually a two." Now if React would just continue with its rendering without realizing the status changed, then the component on the left, right here, would render with one, and this would render with two for the same bit of state. And we would get into some kind of invalid UI, where the state was half updated and half not. So that's where React offers new APIs for library developers that do state management libraries, where you can notify React, "Well, we did something which caused state to update, evaluate that, and decide whether we need to re render the complete or partial component three, and not continue or prevent these kind of errors."
[01:34:45] Now as soon as we started doing concurrent rendering, we actually... Or sorry, I should say when we installed React 18 and started using CreateRoute, we actually started using this rendering. But it didn't really help us with the prime numbers list. If I go here, and I go to the 10,000 range, and I drag the slider around, it's still just as sluggish as it was with React 17. The reason is this mechanism allows for things to be injected, and to detect state updates. But by default, applications will behave exactly the same way. They're not going to behave differently. It's just enabling the possibility. And until we actually start using the APIs to use this, we won't see any difference.
And there is one API, or actually two, but are packaged in a different way so it's actually one under the hood, which is startTransition, which would let us start using it. Because basically, what React does, now, is it differentiates between high and low priority work. And by default, everything is a high priority. But with startTransition, we can say, "Well, this bunch of work is actually low priority work. And if there is something more important, you can skip this. Just discard it, don't bother with it." Eventually, there won't be any more important work, and whatever work you're doing in that startTransition will be done. But only the last bits of it, not everything. So lots of intermediate work can be discarded.
[01:36:42] And if I look at this, if I direct the mouse around... Let's actually go to slightly lower numbers. If I direct the mouse around on that input type range, we see these prime numbers being redrawn, and it determines whether numbers are primes or not. Well, that takes time. Here, that's fine. But with the slightly bigger range, it's okay. All that state which determines the value of the slider and the list of prime numbers are tied together, and they're all updated on high priority.
But maybe I can skip updating the list of prime numbers, as long as I'm actually changing this slider. Because as long as I'm changing, the actual list being rendered isn't that important. And with startTransition, I can say, "Well, this part, the prime number part, is actually a lower priority than the value of this range input."
[01:37:47] And it turns out that doing that is pretty simple. Here's the actual components that's responsible for that list. And you can see there is a prime range here, which actually sets the maximum prime number to calculate. And then there is a new pair for each individual number. So that's…where is that component. Let's close all of these. That's in primes, that's in here. And right now, whenever in that prime range, so the top element, that slider is updated, will immediately set this, max primes, which is normal use state, which renders this component, which immediately creates all those prime number check components, and they start doing their thing. Well, let's make that a bit slower, or lower priority, I should... StartTransition takes a call back. So we put that code in there, and that's the whole change. So startTransition was imported from React, part of React 18. Now, we go back here with the low numbers, almost the same behavior. I go to the 10,000 range, which was slow before. I click somewhere, and it responds immediately. I drag the slider response immediately, but the list of prime numbers doesn't update until I stop and it stabilizes. Or if I move them out slowly, it'll occasionally have time to update. There, it actually updates, but not really.
But now even if I go to the very large numbers, up to a million, I can move this slider around and it responds nice and fast, really reactive, but the list of prime numbers isn't actually recomputed until the value stabilizes. So, we're really using concurrent mode now. We've made a difference. The value of the slider is high priority. The list of prime numbers is low priority. And that's the expensive part, so now it becomes much more responsive, much faster. And eventually, everything will be consistent.
[01:40:25] And if I go back to these somewhat slower numbers and I move the most slowly, you will occasionally see it update. The larger the numbers become, the more expensive it becomes, so the less frequent it becomes.
So, Christina asks, “How different is this under the hood from debouncing the input?” And that's a very good question, because with React 17 and before, debounce or throttle would be the go to options to do or to solve a performance issue like this. Well, the big difference between debounce and throttle on the one hand and startTransition on the other hand is that debounce and throttle are time based. I could say, over here, I want to set this value on a debounce after a second delay, which basically means update this max prime value if that function hasn't been called for seconds. Or if I use throttle, it would be a case of, at most, once a second, however often it would be called, which means that if I'm on the low numbers, which are really responsive, even here, if I moved slider around, it would take a second before the actual prime numbers are updated, because that's the throttle. But with the startTransition, it's not a matter of time. It's a matter of available CPU time. And here, there's plenty of time available, so it's really responsive. Here, there is not so much time available because the rendering takes longer, so it just goes as soon as it can. So, that's a difference there. So, pretty simple change, but it has a pretty big impact on the UI and the responsiveness for the application.
[01:42:34] Now, you typically don't want to start introducing start transactions everywhere. It has to make sense. In a case here where I've got the slider which needs to be responsive but this UI can lag behind, that makes sense. In other cases, it might not. It really depends on the circumstances, whether it makes sense.
One drawback of startTransition, which I'm using now, is there is no feedback over here. The list of prime numbers isn't updated, but I can't see that it's currently still... I'm using a still list there. So after this, we'll look at another API, which under the hood also gives a startTransition, but in a slightly different way so we can actually fix that. So, the result, the responsive slider and the, well, list of prime numbers, which lacks behind a bit.
[01:43:44] So, Alexi asks, "Can we wrap our array of prime numbers only? I meant to update the state of the component but render our numbers inside startTransition." Not sure because you don't render inside of a startTransition. We've got all of this code here, which executes with the render. The thing is, over here, these values are filled based on the max prime. So, we're basically making sure that this isn't updated so the list doesn't actually change.
Speaker 1: Yeah, I was just asking, because if you see the actual value while we are dragging this UI element, if you want to see it on every single, every tick, every time you move it even a little bit, but we don't want to query under everything below it.
[01:44:51] Maurice de Beijer: Let me show you how the prime range component works. In here, you can see this is the slider input. There, you see on the change, it actually calls two values on change, which is the external handler passed in, which we just changed over here. And it has its own set max prime range, which is its own internal state here, which is used for its own value. So, that's updated immediately. The other handler is called, and that runs in a lower priority because of the startTransition. And that's why this one is always up to date and the slider was responsive, and now this one can lag behind. And I could have put that transition in here as well, because basically, it appears twice here, so we would've to copy it.
But of course, over here, you can do exactly the same thing, duplicate this state saying, I've got, well, a transition based version of the state value and a direct version of the state value, except in this case, you still want to prevent this from rendering. So, you'd probably have to change this into another pure component and do all the logic in there. By the way, another check or optimization you typically do is CheckNumber would be in a pure component. In here, it's not. It's really doing its rendering every time, and it's really, on every render here, calculating whether the value is prime or not. So, that's definitely not an optimized version of this component. But if I optimize this, it would be actually much harder to show you the startTransition behavior.
[01:46:46] So, startTransition is pretty neat for that, but it turns out there is a second API, useTransition, which we can use, which sort of does the same thing but gives us a bit more. And the thing it does is… StartTransition is just a function we can import, useTransition is a hook. And besides letting us start transitions, it will also give us the current state. Are we in a pending transition or not? And that can be useful if you want to render some specific full back UI, maybe make it clear that we're in some kind of transition hook state.
So, how to do that? Actually really easy. That's a prime numbers. So, instead of startTransition, I'm going import useTransition, and I'm going to call that... Takes no parameters. It returns a tuple of two things, the isPending flag and startTransition flag. Like that. So, this startTransition works exactly the same as the one we used before, so I don't have to go make any changes there whatsoever. This isPending flag is either true or false whether we're currently inside of some transition, so we could use that. Now, it turns out this check number component already has some capabilities there. It already has an optional isPending. And if it does, instead of doing the check mark or cross for whether it's prime or not, it's going to short circuit that and just show us one of those sand glass icons.
[01:48:53] So, all I need to do is pass that into here. And now if I go back to my prime numbers components and I move that around, you see that we get sand glasses until it actually stabilizes. And if I go to the large numbers, still a responsive slider. Well, almost as responsive. Lacks behind a bit more, but the UI actually updates. Could also have done something else, maybe show these grayed out or something like that, maybe not render them, maybe... Whatever is appropriate in your UI. With a pretty trivial change, basically instead of importing startTransition, import useTransition and start using that.
So, pretty simple to do, minimal change, and it gives us some nice benefits. We get to see that we're in a pending state. Now, please make this change. And after that, I'll briefly talk about why we have both these APIs, because the first time I saw this, if startTransition gives us what useTransition already gives us, why do we need both? But it turns out there are some good reasons to have both, but we'll talk about that after we do this. So, I'm going to open up the breakout rooms for five minutes again.
[01:50:46] And I see there is a question. Does it also... A question from Amir, “Is it also compatible with Suspense”? This is really an unrelated API. This has nothing to do with Suspense because there are no promises being thrown here. So in that regard, it's completely compatible because they're not related. You don't need to use Suspense. In fact, there are no Suspense components. Well, actually, there is at the root, but not specifically used for this.
Okay. Everyone's back. Everyone has some nice visual feedback now from the startTransition?
[01:51:20] Speaker 3: Yeah, I have some... This thing…so basically, it looks nicer, I'd say, to have it rendered after you put the dot on some place like left and right. However, I just saw that after a while, when you want to move this dot, it's not... So, you move the dot. Then the table with the numbers is getting rendered or reordered, and you can't move the dot or the dot is moving really very, very slow. It's not very smooth.
Maurice de Beijer: Yeah.
Speaker 3: Yeah. When you have big…yeah.
Maurice de Beijer: Let me refresh, and then go to a hundred thousand. So…
Speaker 3: For example, so at the beginning. Yeah.
Maurice de Beijer: Yeah.
Speaker 3: So, now it's like yes, but when you release the dots now, yeah, and it'll render. And now when you try to move it, yeah, as you see, it's getting stuck at some points.
[01:52:33] Speaker 1: Yeah. It feels like when React gets this opportunity to render, finally render this list, it takes all the resources, and it doesn't release those resources to the browser while it's rendering this huge list.
Maurice de Beijer: Yeah.
Speaker 1: It considers this entire list as a one chunk, one piece of work..
[01:52:57] Maurice de Beijer: Yeah. It stays responsive for me, but it's not quite as responsive as it was before with startTransition by itself. It doesn't get stuck for me. That said, if I drag now, the first re-render actually takes more time.
Speaker 4: Yeah. It seems like because we are defining the hook in the rendering, does it mean that it does the first render and then it triggers the startTransition?
startTransition() vs useTransition()
[01:53:34] Maurice de Beijer: Yes, that's correct. And actually, that's a very good point to bring me to the next slide, because I mentioned I was going to talk about why we have both startTransition and useTransition. Well, as you mentioned, useTransition is a hook, and that means it can only be used from a hook or a functional component, but it has to start with a functional component somewhere. It can't start from some library or something. And startTransition is just a function that could start from anywhere. It could start from a class based component, from a functional component, from some other library which knows about React. So, startTransition is more flexible. The other thing, startTransition starts the transition, and then it's done.
UseTransition, you call the startTransition which is returned from useTransition, it also starts the transition but then it has to trigger one additional render cycle with that isPending flag set to true. So, you get one additional render. And in this case, I've got a pretty large set of prime numbers here. I think I'm rendering 10,000 of them. So, that first time I move it, it first has to render those same 10,000 components again, all with the isPending flag is true, and only then can it stop rendering those and we're in the transition. So, it's the first re-render which actually makes it appear slower, or not just appear slower. It is slower.
[01:55:20] So, the useTransition hook is a bit slower because of the additional render, but it does give us additional feedback, and it needs to be used from a component. StartTransition is faster, but no additional feedback. So, yeah, they both have their advantages and disadvantages, and both are useful.
UI state transitions with useTransition()
[01:55:47] Now, it turns out with transactions, you can do more, specifically UI transactions, because in this case, we actually need the feedback. Because in this user list, if I click on the user, I see loading spinners, but maybe I would actually want to show the fact that we were loading the new user here inside of this list and have some more control over how everything appears. Maybe not show spinners here but just create this out or something like that. Well, it turns out with useTransition, I can do exactly that. So, let's go and do that. So, we'll close this.
I'll go back to my user list. And inside this click, where I set the selected user, I can actually start doing that inside of a transaction. So, we can start import useTransaction, just like before. Actually, that's just... I closed it. Just copy that line. Grab that startTransition here. That should be a fat arrow like this and one more brace. And we've got a startTransition in there, and now I could do something with that isPending flag and maybe pass that into here. So, that could display itself a little differently. And it's not on the interface yet, so let's add it there as a bullion, retrieve it from the props, and then I could maybe make it slightly transparent with a dynamic style.
[01:57:59] So, opacity, if we're pending, we'll set it to 0.2 or something, and otherwise, we'll leave it to the defaults by setting it to undefined. We're basically not setting it. Now in theory, that should work. In practice, it doesn't quite yet.
Let me show you what happens first. Nothing new. Why is that? Well, because the user details in the favorite movies have their own Suspense boundaries. So, the click and the transition here are actually in different Suspense boundaries now, so they don't cooperate. But if I go into user details, and let's get rid of these Suspense boundaries, Suspense boundary there, now they live in the same Suspense boundary and will actually get a bit more.
[01:59:12] So, I click on the user. We see it's being great out. And we don't see any spinners anymore, but we're inside of transition and we control exactly what we want to do. Now, that was already loaded. Let me do this again. The only thing is if I click on the user like I click on spares now, Lupe stays highlighted until that transition is complete. So, we want the active style not after all of this is loaded. We want to update that slightly sooner. So, we actually have to do a bit more work here because that active style here is done over here based on the selected user, which we're setting but in a lower priority. So, this doesn't actually re-render with that selected user until that transition has finished.
So, what we want to do is that, let’s say, well, we need to double that state. We need to select a user or user id. So, let's copy this. We need the selectedUserId and setSelectedUserId. And the UserId is a number, so we can default that to none, not a number. And then in here, we can compare the user.id to the selectedUserId. And then outside of the transaction, we set the ID. And that's the user.id. And that return is not actually needed. It doesn't return anything anyway.
[02:01:19] So now, the user is highlighted as soon as I click on it, and the details are updated inside of the transaction and I get to control exactly how they're updated. But notice how no spinners appear now. The Suspense boundaries with their fallbacks don't actually start any of the fallback rendering. Because I'm using the useTransition hook, they basically leave it up to me to decide how I want to control. So, the Suspense list here doesn't do anything anymore. I can remove that as well. I can leave it in. I can remove it. Doesn't really matter. Doesn't do anything because there are no Suspense boundaries inside of it, and it's basically completely up to me. But that's another nice capability I have, which I didn't have before. So, instead of using a Suspense list to coordinate how different Suspense components work, whether it's one or more, doesn't matter, they all enlist in this transaction because they're all part of the same Suspense boundary. But remember, they have to be part of the same Suspense boundary. If I put one back in, say for the movie, and that's going to have its own behavior.
Now, I was actually expecting to see a spinner here, but for some reason, I'm not seeing a spinner. Interesting. Is that because of the Suspense list? I doubt it, but let's check. No. So, I guess the useTransition overrules the whole Suspense and the Suspense list as long as it's part of it. If I also enable this Suspense, it's not part of it. Now, we do get the spinners. Remove all of those again, which I actually think is pretty neat behavior. Now, you get full control, and you can even do some other rendering. Maybe in here, you want to render the facts that this user is loading by another indicator, something like insert that hourglass there and only do it conditionally. IsPending, then we want hourglass, else, we want nothing. So now, if I click... Oh, I'm rendering them for all. That wasn't exactly my intent, so let's add this check.
[02:04:52] So now, I'm rendering that hourglass only for the user I'm currently loading. Whatever you want. Complete freedom because you get complete control now due to that isPending flag, which I really like. So, that same change which I just made, but remember, you kind of have to duplicate the state, set the state inside of the transaction for the stuff you don't care about immediately inside of the transition, and set the state outside of the transition for the stuff you do care about immediately. And remember, you do get re-render then. So, if things are slow because of render times then you still won't get all the benefits. So, the user details, component without the Suspense, and the Suspense list, and the result.
So, please go and do this. I'll open up the breakout rooms. And after that, we're done. This is the last exercise we're going to do. So, just to wrap up, and we're all done for this workshop. But please go and do this change and I'll see you all back here in five minutes.
[02:06:28] And that's everyone back. So, there was one question I missed before from Alexi. “How does startTransition know if data is ready and promises are ready to be resolved?” I haven't actually looked into the React source code how this exactly works, but my guess is that it looks for the closest Suspense boundary in the parent and it checks its state. Is it suspended? Then it knows that the isPending flag should be true. And when that resolves, it can….it will be... Well, it'll reset that flag and the components are rendered again. And of course, that last part's automatic already because of Suspense. It's just that Suspense knows about startTransition as well, or at least the useTransition hook because it doesn't show its fallback UI. So, I'm not sure how they communicate, but somehow there is communication between the two on that.
Suspense should do a bit less, and startTransition should be a bit more. Well, magic, I'm not sure about magic, but let's put it this way. If you dive into the React source code, it's complex. It's not a trivial bit of code, not by any stretch of the imagination. There are some very clever people working on that.
[02:08:01] Anyway, that's the end of the workshop. So conclusion, Suspense, usable today with React 17. Things work. You can start fetching data, or lately, fetch components using Suspense. That works just fine. Use error boundaries, nest error boundaries, nest or paralyzed Suspense as you need. Having different Suspense boundaries cooperate doesn't work. It doesn't work. You can't do that in React 17 yet. But with React 18, you can use Suspense list for that, use the useTransition hook for that, use the startTransition, either directly or using useTransition hook to defer low priority work and make complex large applications more responsive.
We haven't even talked about server side rendering and all the stuff they added there. That's also a lot more capable. So, looking forward to React 18 shipping. Now, of course, the $10,000 question is, when will React 18 ship? And I'm afraid I can't answer that. I don't know. Maybe the React team knows, but I suspect even they don't know. As usual, they'll probably say, "We'll ship it when it's ready," which is probably the best answer. Not ship it on any fixed schedule, ship it when it's ready. Now, when do I expect it to ship? I'm hoping before the end of the year. But given that we don't have a beta or even a release candidate yet, we're still in what is officially an Alpha. I would expect that it will take at least another month. So, given that it's end of October, I'm not expecting to see React 18 before December. So, I'm hoping sometime December, it ships. But it's quite possible it's going to be January or even February.
[02:10:17] The React team has been known to take its time to make sure things are right before shipping it, which I can't blame them for. Too much software is shipped on a time schedule instead of a quality schedule.
So with that, I'd like to thank you for attending. I hope it was useful. Already seeing some nice feedback in the chat, which I appreciate very much. That's what I do it for. You've got slides, but if not, you can scan this QR code to get to my website in the list of presentations. This one isn't actually on there yet, but I'll add that to my website tomorrow. Actually, I’ll add it to slide share, and then my website picks it up from slide share, but you've got a copy so won't make much of a difference. So, any last questions before we close everything down? No questions. Well, thanks for attending. I hope it's useful. And enjoy React 18 when it ships, or before, playing around with it like this.