You do not need a deep understanding of how the reconciliation internals of React work. This would defeat the purpose of React and how it encapsulates all of this work for us. However, understanding the motivation for the major internal changes that have happened in React 16 and how they work at a higher level will help you think about how to best design your components today and for the future React applications.
React has established itself as one of the standards when it comes to choosing a library to help build user interfaces. The two key factors for this are its simplicity and its performance. React is simple because it has a small API surface that's easy to pick up and experiment with. React is performant because it minimizes the number of DOM operations it has to invoke by reconciling changes in a render tree.
There's an interplay between these two factors that has contributed to React's skyrocketing popularity. The good performance provided by React wouldn't be valuable if the API were difficult to use. The overarching value of React is that it's simple to use and performs well out of the box.
With the widespread adoption of React came the realization that its internal reconciliation mechanics could be improved. For example, some React applications update the component state faster than rendering can complete. Consider another example: changes to part of the render tree that aren't visible on the screen should have a lower priority than elements that the user can see. Issues like these are enough to degrade the user experience so that it doesn't feel as fluid as it could be.
How do you address these issues without disrupting the API and render tree reconciliation that work so well?
JavaScript is single-threaded and run-to-completion. This means that by default, any JavaScript code that you run will block any other browser tasks from running, such as painting the screen. This is why it's especially important that JavaScript code be fast. However, in some cases, even the performance of the React reconciliation code isn't enough to mask bottlenecks from the user. When presented with a new tree, React has no choice but to block the DOM updates and event listeners while it computes the new render tree.
One possible solution is to break the reconciliation work into smaller chunks, and arrange them in such a way that prevents the JavaScript run-to-completion thread from blocking important DOM updates. This would mean that the reconciler wouldn't have to render a complete tree, and then have to do it all over again because an event took place while the first render was taking place.
Let's look at a visual example of this problem:
This figure demonstrates that any time state changes in a React component, nothing else can happen until rendering has completed. As you can see, reconciling entire trees can get expensive as the state changes pile up, and, all the while, the DOM is blocked from doing anything.
Reconciling the render tree is in lock-step with the run-to-completion semantics of JavaScript. In other words, React cannot pause what it's doing to let the DOM update. Let's now look at how React 16 is trying to change the preceding figure:
This version of the React render/reconciliation process looks similar to the previous version. In fact, nothing about the component on the left has changed—this is reflective of the unchanging API in React 16. There are some subtle but important differences though.
Let's start by looking at the reconciler. Instead of building a new render tree every time the component changes state, it renders a partial tree. Putting it another way,
it performs a chunk of work that results in the creation of part of a render tree. The reason it doesn't complete the entire tree is so that the reconciliation process can pause and allow any DOM updates to run—you can see the difference in the DOM on the right-hand side of the image.
When the reconciler resumes building the render tree, it first checks to see if new state changes have taken place since it paused. If so, it takes the partially completed render tree and reuses what it can, based on the new state changes. Then, it keeps going until the next pause. Eventually, reconciliation completes. During reconciliation, the DOM has been given a chance to respond to events and to render any outstanding changes. Prior to React 16, this wasn't possible—you would have to wait until the entire tree was rendered before anything in the DOM could happen.
In order to separate the job of rendering components into smaller units of work, React has created an abstraction called a fiber. A fiber represents a unit of rendering work that can be paused and resumed. It has other low-level properties such as priority and where the output of the fiber should be returned to when completed.
The code name of React 16 during development was React Fiber, because of this fundamental abstraction that enables scheduling pieces of the overall rendering work to provide a better user experience. React 16 marks the initial release of this new reconciliation architecture, but it's not done yet. For example, everything is still synchronous.
React 16 lays the groundwork for the ultimate goal of asynchronous rendering in the next major release. The main reason that this functionality isn't included in React 16 is because the team wanted to get the fundamental reconciliation changes out into the wild. There are a few other new features that needed to be released too, which we'll go over in the following sections.
Once asynchronous rendering capabilities are introduced into React, you shouldn't have to modify any code. Instead, you might notice improved performance in certain areas of your application that would benefit from prioritized and scheduled rendering.
Better component error handling
React 16 introduces better error-handling capabilities for components. The concept is called an error boundary, and it's implemented as a lifecycle method that is called when any child components throw an exception. The parent class that implements componentDidCatch()
is the error boundary. You could have different boundaries throughout your application, depending on how your features are organized.
The motivation for this functionality is to give the application an opportunity to recover from certain errors. Prior to React 16, if a component threw an error, the entire app would stop. This might not be ideal, especially if an issue with a minor component stops critical components from working.
Let's create an App
component with an error boundary:
The App
component does nothing but render MyError
—a component that intentionally throws an error. When this happens, the componentDidCatch()
method is called with the error as an argument. You can then use this value to change the state of the component. In this example, it sets the error message in the err
state. Then, App
will attempt to re-render.
As you can see, this.state.err
is passed to MyError
as a property. During the first render, this value is undefined. When App
catches the error thrown by MyError
, the error is passed back to the component. Let's look at MyError
now:
This component throws an error with the message 'epic fail'
. When App
catches this error, it renders MyError
with an err
prop. When this happens, it simply renders the error string in red. This just happens to be the strategy I've chosen for this app; always check for an error state before invoking the errant behavior again. In MyError
, the application as a whole is recovered by not executing throw new Error('epic fail')
for a second time.
With componentDidCatch()
, you're free; set any strategy you like for error recovery. Usually, you can't recover a specific component that fails.
Rendering multiple elements and strings
Since React was first released, the rule was that components could only render one element. This has changed in two important ways in React 16. First, you can now return a collection of elements from your component. This simplifies cases where rendering sibling elements would drastically simplify things. Second, you can now render plain text content.
Both of these changes result in fewer elements on the page. By allowing sibling elements to be rendered by components, you don't have to wrap them with an element for the sake of returning a single element. By rendering strings, you can render test content as the child or another component, without having to wrap it in an element.
Here's what rendering multiple elements looks like:
Note that you have to provide a key
property for elements in a collection. Now let's add an element that returns a string value:
The Label
component simply returns a string as its rendered content. The p
element renders Label
as a child, adjacent to the {v}
value. When components can return strings, you have more options for composing the elements that make up your UI.
The final new feature of React 16 that I want to introduce is the notion of portals. Normally, the rendered output of a component is placed where the JSX element is located within the tree. However, there are times when we have greater control over where the rendered output of our components ends up. For example, what if you wanted to render a component outside of the root React element?
Portals allow components to specify their container element at render time. Imagine that you want to display notifications in your application. Several components at different locations on the screen need the ability to render notifications at one specific spot on the screen. Let's take a look at how you can target elements using portals:
In the constructor of this component, the target element is created and stored in the el
property. Then, in componentWillMount()
, the element is appended to the document body. You don't actually need to create the target element in your component—you can use an existing element instead. The componentWillUnmount()
method removes this element.
In the render()
method, the createPortal()
function is used to create the portal. It takes two arguments—the content to render and the target DOM element. In this case, it's passing its child properties. Let's take a look at how MyPortal
is used:
The end result is that the text that's passed to MyPortal
is rendered as a strong element outside of the root React element. Before portals, you would have to resort to some kind of imperative workaround in order for something like this to work. Now, we can just render the notification in the same context that it's needed in—it just happens to be inserted somewhere else in the DOM in order to display correctly.