-
Notifications
You must be signed in to change notification settings - Fork 50.5k
[New Docs] Reconciliation and Web Components docs #7962
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from 5 commits
9865828
2821ec7
79f20c8
5a4653d
a011f17
9313429
34c91d1
e8ef077
a00514d
c73c5bd
1c31c02
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
This file was deleted.
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -1,49 +1,44 @@ | ||
| --- | ||
| id: reconciliation | ||
| title: Reconciliation | ||
| permalink: docs-old/reconciliation.html | ||
| prev: special-non-dom-attributes.html | ||
| next: webcomponents.html | ||
| permalink: docs/reconciliation.html | ||
| --- | ||
|
|
||
| React's key design decision is to make the API seem like it re-renders the whole app on every update. This makes writing applications a lot easier but is also an incredible challenge to make it tractable. This article explains how with powerful heuristics we managed to turn a O(n<sup>3</sup>) problem into a O(n) one. | ||
|
|
||
| React provides a declarative API so that you don't have to worry about exactly what changes on every update. This makes writing applications a lot easier but it might not be obvious how this is implemented within React. This article explains the choices we made in React's "diffing" algorithm so that component updates are predictable while being fast enough for high-performance apps. | ||
|
|
||
| ## Motivation | ||
|
|
||
| Generating the minimum number of operations to transform one tree into another is a complex and well-studied problem. The [state of the art algorithms](http://grfia.dlsi.ua.es/ml/algorithms/references/editsurvey_bille.pdf) have a complexity in the order of O(n<sup>3</sup>) where n is the number of nodes in the tree. | ||
| Generating the minimum number of operations to transform one tree into another is a complex and well-studied problem. The [state of the art algorithms](http://grfia.dlsi.ua.es/ml/algorithms/references/editsurvey_bille.pdf) have a complexity in the order of O(n<sup>3</sup>) where n is the number of elements in the tree. | ||
|
|
||
| This means that displaying 1000 nodes would require in the order of one billion comparisons. This is far too expensive for our use case. To put this number in perspective, CPUs nowadays execute roughly 3 billion instructions per second. So even with the most performant implementation, we wouldn't be able to compute that diff in less than a second. | ||
| This means that displaying 1000 elements would require in the order of one billion comparisons. This is far too expensive for our use case. To put this number in perspective, CPUs nowadays execute roughly 3 billion instructions per second. So even with the most performant implementation, we wouldn't be able to compute that diff in less than a second. | ||
|
|
||
| Since an optimal algorithm is not tractable, we implement a non-optimal O(n) algorithm using heuristics based on two assumptions: | ||
|
|
||
| 1. Two components of the same class will generate similar trees and two components of different classes will generate different trees. | ||
| 1. Two components of the same type will generate similar trees and two components of different types will generate different trees. | ||
| 2. It is possible to provide a unique key for elements that is stable across different renders. | ||
|
|
||
| In practice, these assumptions are ridiculously fast for almost all practical use cases. | ||
|
|
||
| In practice, these assumptions are valid for almost all practical use cases. | ||
|
|
||
| ## Pair-wise diff | ||
|
|
||
| In order to do a tree diff, we first need to be able to diff two nodes. There are three different cases being handled. | ||
|
|
||
| In order to do a tree diff, we first need to be able to diff two elements. There are three different cases being handled. | ||
|
|
||
| ### Different Node Types | ||
| ### Different Element Types | ||
|
|
||
|
Collaborator
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Can we use "element" instead of "node" here and below? While technically reconciliation also affects "nodes" (booleans and text), this article talks specifically about how we treat elements. It was written before modern terminology was introduced.
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. ok |
||
| If the node type is different, React is going to treat them as two different sub-trees, throw away the first one and build/insert the second one. | ||
| If the element type is different, React is going to treat them as two different sub-trees, throw away the first one and build/insert the second one. | ||
|
|
||
| ```xml | ||
| renderA: <div /> | ||
| renderB: <span /> | ||
| => [removeNode <div />], [insertNode <span />] | ||
| => [removeElement <div />], [insertElement <span />] | ||
|
||
| ``` | ||
|
|
||
| The same logic is used for custom components. If they are not of the same type, React is not going to even try at matching what they render. It is just going to remove the first one from the DOM and insert the second one. | ||
|
|
||
| ```xml | ||
| renderA: <Header /> | ||
| renderB: <Content /> | ||
| => [removeNode <Header />], [insertNode <Content />] | ||
| => [removeElement <Header />], [insertElement <Content />] | ||
| ``` | ||
|
|
||
| Having this high level knowledge is a very important aspect of why React's diff algorithm is both fast and precise. It provides a good heuristic to quickly prune big parts of the tree and focus on parts likely to be similar. | ||
|
|
@@ -52,15 +47,14 @@ It is very unlikely that a `<Header>` element is going to generate a DOM that is | |
|
|
||
| As a corollary, if there is a `<Header>` element at the same position in two consecutive renders, you would expect to see a very similar structure and it is worth exploring it. | ||
|
|
||
| ### DOM Elements | ||
|
|
||
| ### DOM Nodes | ||
|
|
||
| When comparing two DOM nodes, we look at the attributes of both and can decide which of them changed in linear time. | ||
| When comparing two DOM elements, we look at the attributes of both and can decide which of them changed in linear time. | ||
|
|
||
| ```xml | ||
| renderA: <div id="before" /> | ||
| renderB: <div id="after" /> | ||
| => [replaceAttribute id "after"] | ||
| renderA: <div className="before" /> | ||
| renderB: <div className="after" /> | ||
| => [replaceAttribute className "after"] | ||
| ``` | ||
|
|
||
| Instead of treating style as an opaque string, a key-value object is used instead. This lets us update only the properties that changed. | ||
|
|
@@ -73,61 +67,56 @@ renderB: <div style={{'{{'}}fontWeight: 'bold'}} /> | |
|
|
||
| After the attributes have been updated, we recurse on all the children. | ||
|
|
||
|
|
||
| ### Custom Components | ||
|
|
||
| We decided that the two custom components are the same. Since components are stateful, we cannot just use the new component and call it a day. React takes all the attributes from the new component and calls `componentWillReceiveProps()` and `componentWillUpdate()` on the previous one. | ||
| The last case is comparing two custom components of the same type. Since components are stateful, we must keep the old instance around. React takes all the attributes from the new component and calls `componentWillReceiveProps()` and `componentWillUpdate()` on the previous one. | ||
|
||
|
|
||
| The previous component is now operational. Its `render()` method is called and the diff algorithm restarts with the new result and the previous result. | ||
|
|
||
|
|
||
| ## List-wise diff | ||
|
|
||
| ### Problematic Case | ||
|
|
||
| In order to do children reconciliation, React adopts a very naive approach. It goes over both lists of children at the same time and generates a mutation whenever there's a difference. | ||
| In order to do children reconciliation, React adopts a naive approach. It goes over both lists of children at the same time and generates a mutation whenever there's a difference. | ||
|
|
||
| For example if you add an element at the end: | ||
| For example, if you add an element at the end: | ||
|
|
||
| ```xml | ||
| renderA: <div><span>first</span></div> | ||
| renderB: <div><span>first</span><span>second</span></div> | ||
| => [insertNode <span>second</span>] | ||
| => [insertElement <span>second</span>] | ||
| ``` | ||
|
|
||
| Inserting an element at the beginning is problematic. React is going to see that both nodes are spans and therefore run into a mutation mode. | ||
| Inserting an element at the beginning is problematic. React is going to see that both elements are spans and therefore run into a mutation mode. | ||
|
|
||
| ```xml | ||
| renderA: <div><span>first</span></div> | ||
| renderB: <div><span>second</span><span>first</span></div> | ||
| => [replaceAttribute textContent 'second'], [insertNode <span>first</span>] | ||
| => [replaceAttribute textContent 'second'], [insertElement <span>first</span>] | ||
| ``` | ||
|
|
||
| There are many algorithms that attempt to find the minimum sets of operations to transform a list of elements. [Levenshtein distance](https://en.wikipedia.org/wiki/Levenshtein_distance) can find the minimum using single element insertion, deletion and substitution in O(n<sup>2</sup>). Even if we were to use Levenshtein, this doesn't find when a node has moved into another position and algorithms to do that have much worse complexity. | ||
| There are many algorithms that attempt to find the minimum sets of operations to transform a list of elements. [Levenshtein distance](https://en.wikipedia.org/wiki/Levenshtein_distance) can find the minimum using single element insertion, deletion and substitution in O(n<sup>2</sup>). Even if we were to use Levenshtein, this doesn't find when an element has moved into another position and algorithms to do that have much worse complexity. | ||
|
|
||
| ### Keys | ||
|
|
||
| In order to solve this seemingly intractable issue, an optional attribute has been introduced. You can provide for each child a key that is going to be used to do the matching. If you specify a key, React is now able to find insertion, deletion, substitution and moves in O(n) using a hash table. | ||
|
|
||
| In order to solve this issue, React supports an optional `key` attribute. You can provide for each child a key that is going to be used to do the matching. If you specify a key, React is now able to find insertion, deletion, substitution and moves in O(n) using a hash table. | ||
|
|
||
| ```xml | ||
| renderA: <div><span key="first">first</span></div> | ||
| renderB: <div><span key="second">second</span><span key="first">first</span></div> | ||
| => [insertNode <span>second</span>] | ||
| => [insertElement <span>second</span>] | ||
| ``` | ||
|
Collaborator
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Maybe add something like "Now React knows that an element with
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. ok |
||
|
|
||
| In practice, finding a key is not really hard. Most of the time, the element you are going to display already has a unique id. When that's not the case, you can add a new ID property to your model or hash some parts of the content to generate a key. Remember that the key only has to be unique among its siblings, not globally unique. | ||
|
|
||
|
|
||
| ## Trade-offs | ||
|
|
||
| It is important to remember that the reconciliation algorithm is an implementation detail. React could re-render the whole app on every action; the end result would be the same. We are regularly refining the heuristics in order to make common use cases faster. | ||
|
|
||
| In the current implementation, you can express the fact that a sub-tree has been moved amongst its siblings, but you cannot tell that it has moved somewhere else. The algorithm will re-render that full sub-tree. | ||
|
|
||
| Because we rely on two heuristics, if the assumptions behind them are not met, performance will suffer. | ||
| Because React relies on heuristics, if the assumptions behind them are not met, performance will suffer. | ||
|
|
||
| 1. The algorithm will not try to match sub-trees of different components classes. If you see yourself alternating between two components classes with very similar output, you may want to make it the same class. In practice, we haven't found this to be an issue. | ||
|
|
||
| 2. Keys should be stable, predictable, and unique. Unstable keys (like those produced by Math.random()) will cause many nodes to be unnecessarily re-created, which can cause performance degradation and lost state in child components. | ||
|
|
||
| 2. Keys should be stable, predictable, and unique. Unstable keys (like those produced by Math.random()) will cause many elements to be unnecessarily re-created, which can cause performance degradation and lost state in child components. | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,47 @@ | ||
| --- | ||
| id: web-components | ||
| title: Web Components | ||
| permalink: docs/web-components.html | ||
| --- | ||
|
|
||
| React and [Web Components](https://developer.mozilla.org/en-US/docs/Web/Web_Components) are built to solve different problems. Web Components provide strong encapsulation for reusable components, while React provides a declarative library that keeps the DOM in sync with your data. The two goals are complementary. As a developer, you are free to use React in your Web Components, or to use WebComponents in React, or both. | ||
|
|
||
| Most people who use React don't use Web Components at all. Unless you are using or creating third-party UI components, Web Components are likely not useful for building an app. | ||
|
||
|
|
||
| ## Using Web Components in React | ||
|
|
||
| ```javascript | ||
| class HelloMessage extends React.Component { | ||
| render() { | ||
| return <div>Hello <x-search>{this.props.name}</x-search>!</div>; | ||
| } | ||
| } | ||
| ``` | ||
|
|
||
| > Note: | ||
| > | ||
| > Web Components often expose an imperative API. For instance, a `video` Web Component might expose `play()` and `pause()` functions). To access the imperative APIs of a Web Component, you will need to use a ref to interact with the DOM node directly. If you are using third-party Web Components, the best solution is to write a React component that behaves as a wrapper for your Web Component. | ||
| > | ||
| > Events emitted by a Web Component may not properly propagate through a React render tree. | ||
| > You will need to manually attach event handlers to handle these events within your React components. | ||
|
|
||
|
|
||
| ## Using React in your Web Components | ||
|
|
||
| ```javascript | ||
| var proto = Object.create(HTMLElement.prototype, { | ||
| attachedCallback: { | ||
| value: function() { | ||
| var mountPoint = document.createElement('span'); | ||
| this.createShadowRoot().appendChild(mountPoint); | ||
|
|
||
| var name = this.getAttribute('name'); | ||
| var url = 'https://www.google.com/search?q=' + encodeURIComponent(name); | ||
| ReactDOM.render(<a href={url}>{name}</a>, mountPoint); | ||
| } | ||
| } | ||
| }); | ||
| document.registerElement('x-search', {prototype: proto}); | ||
| ``` | ||
|
|
||
| You can also check out this [complete Web Components example on GitHub](https://github.com/facebook/react/tree/master/examples/webcomponents). | ||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
"to efficiently convert the first tree into the second tree" makes it sound like React mutates the tree you provide to it. Maybe "how to efficiently update the UI to match the most recent tree".
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
OK. I took these words. Yeah I found this confusing when I first read it - to me the way that reconciliation makes the most sense is to think of there as being three trees. There is the "element tree" which the developer is creating when they return JSX, there is the DOM which the browser knows about, and there is this "secret internal tree" which only React uses, React creates it when you first do a ReactDOM.render, and the secret internal tree has nodes where each node corresponds to some element and might correspond to something in the DOM. And reconciliation is basically a recursive algorithm on the internal tree. But this is so not the terminology that we are publicly using, I don't really want to go attempt to rewrite everything to refer to three trees right now. That's how I'd explain reconcilation on a whiteboard though. Anyway I just updated this sentence to not imply React is mutating the tree returned by render().