Transcript for:
Overview of React Version 18

2,161 days. That's how long it took the React teaming to ship version 18. Now, we're long past it now, but in my opinion, React version 18 was both the most underrated and undervalued release React has ever had. So, why did it take so long and why was it so critical to the entire future of React? To better answer these questions one more time, we need to go back. The year was 2016. It was a simpler time. Harambe was alive. The world was united in catching virtual Pokémon. The React team had one goal. Make React and therefore millions of websites more performant. The biggest hindrance to this didn't necessarily have to do with React itself, but with the language that React was built on, JavaScript, and how JavaScript worked in the browser. In a nutshell, the browser handles all of your JavaScript, user events, renders, paints, layouts, reflows, all in a single main thread. Now, this is usually fine, but if you're not careful, it can cause some issues. So today, React is synchronous, which means when you update a component, React is going to synchronously process that update, it's going to do all of the work to finish the update in a single uninterrupted block on the main thread. Right? So the problem of course is that if the user uh is that user events also fire on the main thread. So if you're chugging along rendering an update and in the meantime the user tries to type into an input in synchronous React that input event can't be processed until after the currently executing render has completed. So like all the way past the blue, right? So what was the solution? Well, if blocking the main thread is a problem, can't we just do all of the work inside of another thread using a web worker? Yes, but actually no. React didn't need a way to offload work onto another thread. What React needed was a way to make the current work it already does a little more flexible. Now, let's take a closer look at what we have here. What we have is some IO work followed by some CPU work. And we've seen this pattern before. We should ideally be able to do some of that in parallel because this is not a performance problem. This is fundamentally a scheduling problem. What the React team figured out was if they could somehow enhance React to be able to both differentiate between low priority work like rendering a list and high priority work like user input or animations as well as give React the ability to jump between these tasks. Then theoretically the experience of every React app would improve since React would always prioritize the most important work first. So this is what they did and they named it React Fiber. It seemed to us that scheduling is a core primitive of UI engineering. We just have to deal with it in very ad hoc ways right now. And that's why we investigated and experimented with a lot of different models like based on threads and workers and built-in primitives and everything have trade-offs. And finally, we settle on a model that we think will change the trajectory of how we build the UIs. And that's why we built React Fiber, a complete reimplementation of the core algorithm inside of React with groundup support for built-in scheduling. Sebastian went on to explain that even though it was a fundamental change to React itself, Viber wasn't designed as a new framework and instead would launch as part of the next major version of React version 16 and it would come with the ability to opt into async interruptible rendering. Now, this is when the story gets a little bit hard to follow. We'll try and keep up. Remember, we're still in early 2017 here. At this point, React was objectively smarter with the ability to prioritize work and interrupt rendering. But that was really only half the picture. The other, arguably more difficult half was figuring out a public API that allowed React developers to consume these features in a way that wasn't completely destructive to the entire React ecosystem. A month later, we got another preview of what the React team was now calling async rendering. The the main problem here is not the performance, but scheduling. And the solution has to think about the scheduling. And so we call this uh see the features async rendering in React. And our goals is to let app developers adapt to users constraints such as device and network to make fast interactions feel instant without the junk key uh things popping out of the screen and uh make slower interactions feel responsive and be designed intentionally. The problem, as Dan went on to explain years later, was there was still something missing from React to make using these new scheduling features more consumable. That something was hooks. Hooks were released in React Comp in October of 2018, and they were the biggest change to React since it was released. The initial focus of hooks were that they allowed you to use functions instead of classes for creating React components. However, in reality, they were so much more than that. With Hooks, you got better code reuse, improved composition, and more reasonable defaults. and not often talked about. Hooks were designed in part to help developers naturally write code that was more compatible with async rendering. At the same conference, we also got another update on async rendering, now rebranded as concurrent react. So, we we think we have uh a way to address this type of problem and it's called concurrent react. Now, if you're confused because you've heard of something called async React before, um we had a naming workshop and we decided that the name the term async is a very broad term that describes many things. Uh and while concurrent react does in fact encompass many capabilities, we think the word uh concurrent properly emphasizes the part that makes it special. So, let me explain what I mean when I say concurrent. So concurrent React can work on multiple tasks at a time and switch between them uh using cooperative multitasking according to their priority. Concurrent React can also do something else. It can partially render a tree without committing the result uh to the DOM. Uh so for example, React can start rendering an update and if it hits a component that hasn't finished loading, for instance, React can wait for it to complete before it continues. Uh, and it doesn't immediately have to show a fallback or a spinner or a placeholder or nothing, right? Um, and also concurrent React crucially, it does not block the main thread. This was a nice recap, but once again, there were no actionable updates. Then a year later at React Comp 2019, we finally got something we could download, this time rebranded as concurrent mode. Techniques like selective hydration and the full set of suspense features are made possible by concurrent mode. Concurrent mode enables React apps to be more responsive by giving React the ability to interrupt large blocks of lower priority work in order to focus on something that's higher priority like responding to a user input. Concurrent mode is now available in our experimental channel. It includes all of our concurrent features. Our goal is to enable sharing of knowledge and cross-pollinate ideas before the APIs are finalized. Finally, over 3 years since the start of this journey, users had something that they could experiment with. Was it the final API? No. Was it ready for production? Also, no. But it was something. The idea behind concurrent mode was that you would be able to turn it on for your entire app and therefore see the performance benefits of concurrent rendering just by upgrading. In reality, it didn't really pan out quite like that. The problem with an all or nothing upgrade strategy is that it's just not realistic for large applications. It was clear this strategy needed an incremental story. React 17 was the first step to address this. Released a year later in August of 2020, React 17 allowed you to run multiple versions of React in the same application. The idea was that this would help incremental adoption since you could upgrade parts of your application to React 18 and therefore get the concurrent rendering benefits while parts that weren't able to upgrade could remain on React 17. In short, this didn't really work in part because it also didn't give enough granular control over upgrading. Now, there was another mode called blocking mode, which was sort of a hybrid between the old legacy mode and the new concurrent mode. Let's just say it also didn't work for reasons related to how you probably feel looking at this chart right now. So, one last time, the React team went back to the drawing board with a new focus on improving the incremental adoption story of these concurrent features. Then, a year later, and over 5 years from the very first PR, it was finally here with one final rebrand. After hearing your feedback, we're excited to share that concurrent mode is nowhere to be found in React 18. It's replaced by our gradual adoption strategy where you can adopt concurrent rendering at your own pace. The way this works is that now by using any of the concurrent features, you're essentially telling React that you want to opt into concurrent rendering for this particular part of your application. So finally now with all of the context, what exactly were these concurrent features that React spent over 5 years building? Well, remember if one of the main goals of this entire process was the ability to prioritize work and interrupt rendering, it would make sense that we would get an API to do just that. That's where use deferred value can help us out. You can think of use deferred value as a way to tell React to defer updating a value until all its high priority rendering work has finished. Why is that helpful? because it allows React to keep showing an existing computationally heavy but lower priority portion of the UI while it prioritizes updating a higher priority section of the UI. Take this scenario. Say you had an input field whose value was hooked up to some state which triggered the rerender of an expensive component. Historically, React would treat updating the input field with the exact same priority as updating the expensive component. Often times this meant the value and therefore the text in the input field would lag behind what the user had actually typed since React was too busy rendering the expensive list component to notice. Instead, what we want is for React to always prioritize updating the input field and then only when it's finished rerender the component with the final value. This is exactly how use deferred value works. You pass it the value you want to defer and React will automatically defer updating the value until all its high priority rendering work is finished. If it helps, you can kind of think about use deferred value like a debounce function. However, instead of debouncing at an arbitrary time like when the user stops typing, it will debounce when all of React's high priority rendering work is finished. Now, what if instead of using deferred value to defer updating part of the UI, you wanted to be a little bit more explicit and just tell React which state updates may lead to computationally expensive work. This is exactly what start transition is for. Whenever you wrap a state update inside of React.start start transition that tells React that the update which we'll call a transition may trigger a computationally expensive render and as such to not block any other higher priority events that may occur during that render. The way it works is while React is busy working on the transition, it will continue to show the user what they were already seeing while at the same time checking every 5 milliseconds to see if there are any other higher priority events that it should prioritize. If there are, it will pause the work it's doing and shift its priority. If not, or once all the high priority work is finished, it'll go back to working on the transition, committing the work all at once when it's finished updating the UI. As always, that was a lot of words. Let's see in action. Let's say we had an app with different tabs, some more computationally expensive than others. If you click on a few expensive tabs and then quickly click on a lightweight tab, you'll see the problem. The lightweight tab won't render until React has finished all of the work for the expensive tabs. This is obviously not ideal, especially on low-end devices. As you just saw, by default, when React is busy doing computationally expensive work, it doesn't care about other events, regardless of how important they may or may not be. We can fix that with React.start transition, giving React the ability to prioritize user events, like clicking on a tab. If we wrap our set active chat call inside of start transition, notice now how it behaves. Instead of waiting until all the expensive work has finished, it'll bail and start rendering the lightweight work as soon as it can. In a sense, it's as if we're telling React this state update may lead to some slow work. Because of that, keep showing the user what they were already seeing and make sure you treat it as a non-urgent, interruptible update in case something more important happens in the meantime. Again, is it perfect? No. But it'll go a long way, especially on low-end devices. And if we did want to make it even better, we could add a visual indicator to the UI letting the user know when a transition is in progress. To do that, instead of using React.st start transition, we can use React's use transition hook. When you invoke use transition, you'll get back an array with two values, is pending and start transition. Yes, that start transition is the same as the React.st start transition we used earlier, but now it's coupled to an is pending value, which will be true until the transition is complete. As it relates to our app, let's add a pending class name to the button element when a transition is in progress. To do that, first we'll swap out React.start transition for use transition. And then now you may be tempted to do something like this where you use the is pending value to determine if you should apply the pending class name. That's right, but it's only part of the solution. Notice what happens in this app. Now the pending class name is being applied to every button. We obviously only want to apply it to the button that's being transitioned to. To fix this, you might try doing something like this, where you only apply the pending class name if the transition is pending and the current index of the map matches the active chat index. Good attempt, but that leads to a much more interesting bug. Can you spot it? The button that receives the pending class name and goes red is the currently active button, not the next active button. This gives us an interesting insight into how transitions work. Earlier, we established that when you wrap a state update inside of start transition to keep the UI responsive, React will work on that update in the background and only commit it once it's ready. This is why we're seeing a delay between when we click a new tab and when React updates the active chat. to fix both this problem and the one we saw earlier where all the buttons were turning red. What if we moved the use transition hook down to the button level? If we did that, every button could have its own transition and therefore its own is pending and the current state of active chat would become irrelevant. Now, it's taken a bit of refactoring, but now every button has its own transition and we have more granular control over the rendering of each title. So, now at this point, I know what you're probably thinking. That's it? Well, kind of. React version 18 gave React the ability to interrupt and prioritize rendering via transitions, but for us app developers, it was still mostly unclear whether or not it was actually useful. So, did the React team really spend over 5 years building something that was only useful for a handful of sites when accessed on low-end devices? No. But that only became obvious in December of 2024 with the release of React version 19. We'll see why in the next lesson.