├── .gitignore ├── Readme.md ├── demo ├── app.js ├── index.html ├── package.json └── webpack.config.js └── dilithium ├── LICENSE ├── Readme.md ├── dilithium.js ├── package.json ├── src ├── ChildReconciler.js ├── Component.js ├── DOM.js ├── DOMComponentWrapper.js ├── Element.js ├── HostComponent.js ├── Mount.js ├── MultiChild.js ├── Reconciler.js ├── UpdateQueue.js ├── assert.js ├── instantiateComponent.js ├── shouldUpdateComponent.js └── traverseAllChildren.js └── webpack.config.js /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | dilithium/build 3 | demo/bundle.js 4 | -------------------------------------------------------------------------------- /Readme.md: -------------------------------------------------------------------------------- 1 | # Building React From Scratch 2 | 3 | This is a talk I gave at React Rally 2016 where I walked through a simplified implementation of React in order to explain how it worked. This simplified implementation is called Dilithium. 4 | 5 | 6 | ## Watch the talk: 7 | 8 | 9 | 10 | Be sure to check out the other talks too - all of the speakers did an amazing job. 11 | 12 | ## Check out the code: 13 | 14 | I've included the annotated code that I used in my talk, in the `dilithium` directory. Keep in mind it probably differs slightly from what was shown in the slides (I took a few liberties to make it fit). 15 | -------------------------------------------------------------------------------- /demo/app.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const Dilithium = require('../dilithium'); 4 | 5 | class CounterButton extends Dilithium.Component { 6 | constructor(props) { 7 | super(props); 8 | this.state = {count: 0}; 9 | setInterval(() => { 10 | this.setState({count: this.state.count + 1}); 11 | }); 12 | } 13 | 14 | render() { 15 | return ( 16 |
17 |

{this.props.title}

18 | 19 |
Count: {this.state.count}
20 |
21 | ); 22 | } 23 | } 24 | 25 | class ColorSwatch extends Dilithium.Component { 26 | render() { 27 | const red = this.props.number % 256; 28 | return ( 29 |
36 | ); 37 | } 38 | } 39 | 40 | window.addEventListener('click', () => { 41 | Dilithium.render( 42 | , 43 | document.getElementById('container'), 44 | ); 45 | }); 46 | -------------------------------------------------------------------------------- /demo/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | Dilithium Test 7 | 14 | 15 | 16 |
17 | Click anywhere for first render. 18 |
19 | 20 | 21 | 22 | 23 | -------------------------------------------------------------------------------- /demo/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "scripts": { 3 | "build": "webpack --progress --colors", 4 | "watch": "webpack --progress --colors --watch" 5 | }, 6 | "dependencies": { 7 | "babel-core": "^6.13.2", 8 | "babel-loader": "^6.2.5", 9 | "babel-plugin-transform-class-properties": "^6.11.5", 10 | "babel-plugin-transform-react-jsx": "^6.8.0", 11 | "webpack": "^1.13.2" 12 | }, 13 | "babel": { 14 | "plugins": [ 15 | [ 16 | "transform-react-jsx", 17 | { 18 | "pragma": "Dilithium.createElement" 19 | } 20 | ], 21 | "transform-class-properties" 22 | ] 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /demo/webpack.config.js: -------------------------------------------------------------------------------- 1 | let webpack = require('webpack'); 2 | 3 | module.exports = { 4 | entry: './app.js', 5 | output: { 6 | path: __dirname, 7 | filename: 'bundle.js', 8 | }, 9 | module: { 10 | loaders: [ 11 | { 12 | test: /\.js$/, 13 | loader: 'babel', 14 | query: { 15 | plugins: [ 16 | ['transform-react-jsx', {pragma: 'Dilithium.createElement'}], 17 | 'transform-class-properties' 18 | ] 19 | } 20 | } 21 | ] 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /dilithium/LICENSE: -------------------------------------------------------------------------------- 1 | BSD License 2 | 3 | For React software 4 | 5 | Copyright (c) 2013-present, Facebook, Inc. 6 | All rights reserved. 7 | 8 | Redistribution and use in source and binary forms, with or without modification, 9 | are permitted provided that the following conditions are met: 10 | 11 | * Redistributions of source code must retain the above copyright notice, this 12 | list of conditions and the following disclaimer. 13 | 14 | * Redistributions in binary form must reproduce the above copyright notice, 15 | this list of conditions and the following disclaimer in the documentation 16 | and/or other materials provided with the distribution. 17 | 18 | * Neither the name Facebook nor the names of its contributors may be used to 19 | endorse or promote products derived from this software without specific 20 | prior written permission. 21 | 22 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND 23 | ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED 24 | WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE 25 | DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR 26 | ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES 27 | (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; 28 | LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON 29 | ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT 30 | (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS 31 | SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 32 | -------------------------------------------------------------------------------- /dilithium/Readme.md: -------------------------------------------------------------------------------- 1 | # Dilithium 2 | *A basic re-implementation of the React Stack Reconciler with zero dependencies.* 3 | 4 | * * * 5 | 6 | The code here was written with the purpose of breaking down the implementation details of React. It was written as part of my "Building React From Scratch" talk at React Rally 2016. 7 | 8 | I attempted to comment the code throughout so that it could be used for others to learn how React works. 9 | 10 | Obviously this is not meant to be a replacement for React and doesn't cover all of the things that React does. In fact, it's missing some major pieces, like the event system. 11 | 12 | ## Building 13 | 14 | ```sh 15 | npm install 16 | npm run build 17 | ``` 18 | 19 | This builds a standalone browser version. It wasn't really tested that way and you might have some issues. Testing was done running the demo, which just bundles this with the app. 20 | 21 | ## License 22 | 23 | The code here is based on React and thus retains React's BSD license. 24 | -------------------------------------------------------------------------------- /dilithium/dilithium.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | var Component = require('./src/Component'); 4 | var Element = require('./src/Element'); 5 | var Mount = require('./src/Mount'); 6 | 7 | // Do dependency injection to work around circular dependencies 8 | var DOMComponentWrapper = require('./src/DOMComponentWrapper'); 9 | var HostComponent = require('./src/HostComponent'); 10 | HostComponent.inject(DOMComponentWrapper); 11 | 12 | module.exports = { 13 | Component: Component, 14 | createElement: Element.createElement, 15 | 16 | render: Mount.render, 17 | unmountComponentAtNode: Mount.unmountComponentAtNode, 18 | }; 19 | -------------------------------------------------------------------------------- /dilithium/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "main": "dilithium", 3 | "scripts": { 4 | "build": "webpack --progress --colors", 5 | "watch": "webpack --progress --colors --watch" 6 | }, 7 | "dependencies": { 8 | "babel-core": "^6.13.2", 9 | "babel-loader": "^6.2.5", 10 | "babel-plugin-transform-class-properties": "^6.11.5", 11 | "circular-dependency-plugin": "^1.1.0", 12 | "webpack": "^1.13.2" 13 | }, 14 | "babel": { 15 | "plugins": [ 16 | "transform-class-properties" 17 | ] 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /dilithium/src/ChildReconciler.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const instantiateComponent = require('./instantiateComponent'); 4 | const traverseAllChildren = require('./traverseAllChildren'); 5 | const shouldUpdateComponent = require('./shouldUpdateComponent'); 6 | 7 | const Reconciler = require('./Reconciler'); 8 | 9 | // This *right here* is why keys are critical to preventing reordering issues. 10 | // React will reuse an existing instance if there is one in this subtree. 11 | // The instance identity here is determined by the generated key based on 12 | // depth in the tree, parent, and (in React) the key={} prop. 13 | function instantiateChild(childInstances, child, name) { 14 | let isUnique = childInstances[name] === undefined; 15 | 16 | if (isUnique) { 17 | childInstances[name] = instantiateComponent(child); 18 | } 19 | } 20 | 21 | function instantiateChildren(children) { 22 | // We store the child instances here, which are in turn used passed to 23 | // instantiateChild. We'll store this object for reuse when doing updates. 24 | let childInstances = {}; 25 | 26 | traverseAllChildren(children, instantiateChild, childInstances); 27 | 28 | return childInstances; 29 | } 30 | 31 | function updateChildren( 32 | prevChildren, // Instances, as created above 33 | nextChildren, // Actually elements 34 | mountImages, 35 | removedChildren, 36 | ) { 37 | // Just make our code a little bit cleaner so we don't have to do null checks. 38 | // React skips this to avoid extraneous objects. 39 | prevChildren = prevChildren || {}; 40 | 41 | // Loop over our new children and determine what is being updated, removed, 42 | // and created. 43 | Object.keys(nextChildren).forEach(childKey => { 44 | let prevChild = prevChildren[childKey]; 45 | let prevElement = prevChild && prevChild._currentElement; 46 | let nextElement = nextChildren[childKey]; 47 | 48 | // Update 49 | if (prevChild && shouldUpdateComponent(prevElement, nextElement)) { 50 | // Update the existing child with the reconciler. This will recurse 51 | // through that component's subtree. 52 | Reconciler.receiveComponent(prevChild, nextElement); 53 | 54 | // We no longer need the new instance, so replace it with the old one. 55 | nextChildren[childKey] = prevChild; 56 | } else { 57 | // Otherwise 58 | // Remove the old child. We're replacing. 59 | if (prevChild) { 60 | // TODO: make this work for composites 61 | removedChildren[childKey] = prevChild._domNode; 62 | Reconciler.unmountComponent(prevChild); 63 | } 64 | 65 | // Instantiate the new child. 66 | let nextChild = instantiateComponent(nextElement); 67 | nextChildren[childKey] = nextChild; 68 | 69 | // React does this here so that refs resolve in the correct order. 70 | mountImages.push(Reconciler.mountComponent(nextChild)); 71 | } 72 | }); 73 | 74 | // Last but not least, remove the old children which no longer have any presense. 75 | Object.keys(prevChildren).forEach(childKey => { 76 | // debugger; 77 | if (!nextChildren.hasOwnProperty(childKey)) { 78 | prevChild = prevChildren[childKey]; 79 | removedChildren[childKey] = prevChild._domNode; 80 | Reconciler.unmountComponent(prevChild); 81 | } 82 | }); 83 | } 84 | 85 | function unmountChildren(renderedChildren) { 86 | if (!renderedChildren) { 87 | return; 88 | } 89 | Object.keys(renderedChildren).forEach(childKey => { 90 | Reconciler.unmountComponent(renderedChildren[childKey]); 91 | }); 92 | } 93 | 94 | module.exports = { 95 | instantiateChildren, 96 | updateChildren, 97 | unmountChildren, 98 | }; 99 | -------------------------------------------------------------------------------- /dilithium/src/Component.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const Reconciler = require('./Reconciler'); 4 | const UpdateQueue = require('./UpdateQueue'); 5 | const assert = require('./assert'); 6 | const instantiateComponent = require('./instantiateComponent'); 7 | const DOM = require('./DOM'); 8 | const shouldUpdateComponent = require('./shouldUpdateComponent'); 9 | 10 | class Component { 11 | constructor(props) { 12 | this.props = props; 13 | this._currentElement = null; 14 | this._pendingState = null; 15 | this._renderedComponent = null; 16 | this._renderedNode = null; 17 | 18 | assert(typeof this.render === 'function'); 19 | } 20 | 21 | setState(partialState) { 22 | // React uses a queue here to allow batching. 23 | this._pendingState = partialState; 24 | UpdateQueue.enqueueSetState(this, partialState); 25 | } 26 | 27 | // We have a helper method here to avoid having a wrapper instance. 28 | // React does that - it's a smarter implementation and hides required helpers, internal data. 29 | // That also allows renderers to have their own implementation specific wrappers. 30 | // This ensures that React.Component is available across platforms. 31 | _construct(element) { 32 | this._currentElement = element; 33 | } 34 | 35 | mountComponent() { 36 | // This is where the magic starts to happen. We call the render method to 37 | // get our actual rendered element. Note: since we (and React) don't support 38 | // Arrays or other types, we can safely assume we have an element. 39 | let renderedElement = this.render(); 40 | 41 | // TODO: lifecycle methods: compnentWillMount 42 | 43 | // Actually instantiate the rendered element. 44 | let renderedComponent = instantiateComponent(renderedElement); 45 | 46 | this._renderedComponent = renderedComponent; 47 | 48 | // Generate markup for the child & effectively recurse! 49 | // Since CompositeComponents instances don't have a DOM representation of 50 | // their own, this markup will actually be the DOM nodes (or Native Views) 51 | let markup = Reconciler.mountComponent(renderedComponent); 52 | 53 | // React doesn't store this reference, instead working through a shared 54 | // interface for storing host nodes, allowing this to work across platforms. 55 | // We'll take a shortcut. 56 | // this._renderedNode = markup; 57 | 58 | return markup; 59 | } 60 | 61 | receiveComponent(nextElement) { 62 | this.updateComponent(this._currentElement, nextElement); 63 | } 64 | 65 | updateComponent(prevElement, nextElement) { 66 | // This is a props updates due to a re-render from the parent. 67 | if (prevElement !== nextElement) { 68 | // React would call componentWillReceiveProps here 69 | } 70 | 71 | // React would call shouldComponentUpdate here and short circuit. 72 | // let shouldUpdate = this.shouldComponentUpdate(nextElement.props, this._pendingState) 73 | 74 | // React would call componentWillUpdate here 75 | 76 | // Update instance data 77 | this._currentElement = nextElement; 78 | this.props = nextElement.props; 79 | this.state = this._pendingState; 80 | this._pendingState = null; 81 | 82 | // React has a wrapper instance, which complicates the logic. We'll do 83 | // something simplified here. 84 | let prevRenderedElement = this._renderedComponent._currentElement; 85 | let nextRenderedElement = this.render(); 86 | 87 | // We check if we're going to update the existing rendered element or if 88 | // we need to blow away the child tree and start over. 89 | if (shouldUpdateComponent(prevRenderedElement, nextRenderedElement)) { 90 | Reconciler.receiveComponent(this._renderedComponent, nextRenderedElement); 91 | } else { 92 | // Blow away and start over - it's similar to mounting. 93 | // We don't actually need this logic for our example but we'll write it. 94 | Reconciler.unmountComponent(this._renderedComponent); 95 | let nextRenderedComponent = instantiateComponent(nextRenderedElement); 96 | let nextMarkup = Reconciler.mountComponent(nextRenderedComponent); 97 | // React defers to the host environment to keep this implementation agnostic. 98 | // We'll just call directly. 99 | DOM.replaceNode(this._renderedComponent._domNode, nextMarkup); 100 | this._renderedComponent = nextRenderedComponent; 101 | } 102 | } 103 | 104 | performUpdateIfNecessary() { 105 | // React handles batching so could potentially have to handle a case of a 106 | // state update or a new element being rendered. We just need to handle 107 | // state updates. 108 | this.updateComponent(this._currentElement, this._currentElement); 109 | } 110 | 111 | unmountComponent() { 112 | if (!this._renderedComponent) { 113 | return; 114 | } 115 | 116 | // TODO: call componentWillUnmount 117 | 118 | Reconciler.unmountComponent(this._renderedComponent); 119 | 120 | // TODO: reset fields so everything can get GCed appropriately 121 | } 122 | } 123 | 124 | // Mark this class so we can easily differentiate from classes that don't extend 125 | // this base class. 126 | Component.isDilithiumClass = true; 127 | 128 | module.exports = Component; 129 | -------------------------------------------------------------------------------- /dilithium/src/DOM.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | // Remove all children from this node. 4 | function empty(node) { 5 | [].slice.call(node.childNodes).forEach(node.removeChild, node); 6 | } 7 | 8 | // Very naive version of React's DOM property setting algorithm. Many 9 | // properties need to be updated differently. 10 | function setProperty(node, attr, value) { 11 | // The DOM Component layer in this implementation isn't filtering so manually 12 | // skip children here. 13 | if (attr === 'children') { 14 | return; 15 | } 16 | 17 | node.setAttribute(attr, value); 18 | } 19 | 20 | // Remove the property from the node. 21 | function removeProperty(node, attr) { 22 | node.removeAttribute(attr); 23 | } 24 | 25 | function updateStyles(node, styles) { 26 | Object.keys(styles).forEach(style => { 27 | // TODO: Warn about improperly formatted styles (eg, contains hyphen) 28 | // TODO: Warn about bad vendor prefixed styles 29 | // TODO: Warn for invalid values (eg, contains semicolon) 30 | // TODO: Handle shorthand property expansions (eg 'background') 31 | // TODO: Auto-suffix some values with 'px' 32 | node.style[style] = styles[style]; 33 | }); 34 | } 35 | 36 | function appendChild(node, child) { 37 | node.appendChild(child); 38 | } 39 | 40 | function appendChildren(node, children) { 41 | if (Array.isArray(children)) { 42 | children.forEach(child => appendChild(node, child)); 43 | } else { 44 | appendChild(node, children); 45 | } 46 | } 47 | 48 | function insertChildAfter(node, child, afterChild) { 49 | node.insertBefore( 50 | child, 51 | afterChild ? afterChild.nextSibling : node.firstChild, 52 | ); 53 | } 54 | 55 | function removeChild(node, child) { 56 | node.removeChild(child); 57 | } 58 | 59 | module.exports = { 60 | setProperty, 61 | removeProperty, 62 | updateStyles, 63 | empty, 64 | appendChild, 65 | appendChildren, 66 | insertChildAfter, 67 | removeChild, 68 | }; 69 | -------------------------------------------------------------------------------- /dilithium/src/DOMComponentWrapper.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const MultiChild = require('./MultiChild'); 4 | const DOM = require('./DOM'); 5 | const assert = require('./assert'); 6 | 7 | class DOMComponentWrapper extends MultiChild { 8 | constructor(element) { 9 | super(); 10 | this._currentElement = element; 11 | this._domNode = null; 12 | } 13 | 14 | mountComponent() { 15 | // TODO: special handling for various element types 16 | // TODO: determine namespace DOM element should be created in (eg svg) 17 | // TODO: validate DOM nesting for helpful warnings 18 | // TODO: even more specifically handle