In this five part series, we will "recreate" React from the ground up, learning how it works along the way. Once we have finished, you should have a good grasp of how React works, and when and why it calls the various lifecycle methods of a component.
The series
- part one: basic rendering <- you are here
- part two: componentWillMount and componentDidMount
- part three: basic updating
- part four: setState
- part five: transactions
disclaimer
Some Background: Elements and Components
At the heart of React are three different types of entities: native DOM elements, virtual elements and components.
native DOM elements
These are exactly what they sound like, the actual DOM elements that the browser uses as the building blocks of a webpage. At some point, React will call document.createElement()
to get one, and use the browser's DOM api to update them such as element.insertBefore()
, element.nodeValue
, etc.
virtual React elements
A virtual React element (just called an "element" in the source code), is an in memory representation of what you'd like a given DOM element (or entire tree of elements) to be for a particular render. An element can either directly represent a DOM element such as h1
, div
, etc. Or it can represent a user defined composite component, which is explained below.
Components
"Component" is a pretty generic term in React. They are entities within React that do various types of work. Different types of components do different things. For example, ReactDOMComponent
from ReactDOM is responsible for bridging between React elements and their corresponding native DOM elements.
User Defined Composite Components
You are already familiar with one type of component: the composite component. Whenever you call React.createClass()
, or have an es6 class extend React.Component
, you are creating a Composite Component class. It turns out our view of the component lifecycle with methods like componentWillMount
, shouldComponentUpdate
is just one piece of the puzzle. These are the lifecycle methods that we hook into because they benefit us. But React components have other lifecycle methods such as mountComponent
and receiveComponent
. We never implement, call, or even know these other lifecycle methods exist. They are only used internally by React.
React is declarative
When it comes to components, our job is to define component classes. But we never instantiate them. Instead React will instantiate an instance of our classes when it needs to.
We also don't consciously instantiate elements. But we do implicitly when we write JSX, such as:
class MyComponent extends React.Component { render() { return <div>hello</div>; }}
That bit of JSX gets translated into this by the compiler:
class MyComponent extends React.Component { render() { return React.createElement('div', null, 'hello'); }}
so in a sense, we are causing an element to be created because our code will call React.createElement()
. But in another sense we aren't, because it's up to React to instantiate our component and then call render()
for us. It's simplest to consider React declarive. We describe what we want, and React figures out how to make it happen.
A tiny, fake React called Feact
Now with a little bit of background under our belt, let's get started building our React clone. Since this clone is tiny and fake, we'll give it the imaginative name "Feact".
Let's pretend we want to create this tiny Feact app:
Feact.render(<h1>hello world </h1>, document.getElementById('root'));
For starters, let's ditch the JSX. Assuming Feact was fully implemented, after running the JSX through the compiler we'd end up with
Feact.render( Feact.createElement('h1', null, 'hello world' ), document.getElementById('root'));
JSX is a large topic on its own and a bit of a distraction. So from here on out, we will use Feact.createElement
instead of JSX, so let's go ahead and implement it
const Feact = { createElement(type, props , children ) { const element = { type , props: props || {}, };
if (children) { element .props.children = children ; }
return element ; },};
Elements are just simple objects representing something we want rendered.
What should Feact.render() do?
Our call to Feact.render()
passes in what we want rendered and where it should go. This is the starting point of any Feact app. For our first attempt, let's define render()
to look something like this
const Feact = { createElement() { /* as before */ },
render(element, container ) { const componentInstance = new FeactDOMComponent(element); return componentInstance .mountComponent(container); },};
When render()
finishes, we have a finished webpage. So based on that, we know FeactDOMComponent is truly digging in and creating DOM for us. Let's go ahead and take a stab at implementing it:
class FeactDOMComponent { constructor(element) { this._currentElement = element ; }
mountComponent(container) { const domElement = document.createElement(this._currentElement.type); const text = this._currentElement.props.children; const textNode = document.createTextNode(text); domElement .appendChild(textNode);
container .appendChild(domElement);
this._hostNode = domElement ; return domElement ; }}
In about 40 lines of pretty crappy code we've got an incredibly limited and pathetic little "React clone"! Feact isn't going to take over the world, but it's serving as a nice learning sandbox.
Adding user defined components
We want to be able to render more than just a single, hardcoded, DOM element. So let's add support for defining component classes:
const Feact = { createClass(spec) { function Constructor(props) { this.props = props ; }
Constructor.prototype.render = spec .render;
return Constructor; },
render(element, container ) { // our previous implementation can't // handle user defined components, // so we need to rethink this method }};
const MyTitle = Feact.createClass({ render() { return Feact.createElement('h1', null, this.props.message); }};
Feact.render({ Feact.createElement(MyTitle, { message: 'hey there Feact' }), document.getElementById('root'));
Remember, we're not dealing with JSX for this blog post series, because we've got plenty to deal with already. If we had JSX available, the above would look like
Feact.render( <MyTitle message ="hey there Feact" />, document.getElementById('root'));
We passed the component class into createElement
. An element can either represent a primitive DOM element, or it can represent a composite component. The distinction is easy, if type
is a string, the element is a native primitive. If it is a function, the element represents a composite component.
Improving Feact.render()
If you trace back through the code so far, you will see that Feact.render()
as it stands now can't handle composite components, so let's fix that:
Feact = { render(element, container ) { const componentInstance = new FeactCompositeComponentWrapper(element);
return componentInstance .mountComponent(container); },};
class FeactCompositeComponentWrapper { constructor(element) { this._currentElement = element ; }
mountComponent(container) { const Component = this._currentElement.type; const componentInstance = new Component(this._currentElement.props); const element = componentInstance .render();
const domComponentInstance = new FeactDOMComponent(element); return domComponentInstance .mountComponent(container); }}
By giving users the ability to define their own components, Feact can now create dynamic DOM nodes that can change depending on the value of the props. There's a lot going on in this upgrade to Feact, but if you trace through it, it's not too bad. You can see where we call componentInstance.render()
, to get our hands on an element that we can then pass into FeactDOMComponent.
An improvement for composite components
Currently our composite components must return elements that represent primitive DOM nodes, we can't return other composite component elements. Let's fix that. We want to be able to do this
const MyMessage = Feact.createClass({ render() { if (this.props.asTitle) { return Feact.createElement(MyTitle, { message: this.props.message }); } else { return Feact.createElement('p', null, this.props.message); } }}
This composite component's render() is either going to return a primitive element or a composite component element. Currently Feact can't handle this, if asTitle
was true, FeactCompositeComponentWrapper
would give FeactDOMComponent
a non-native element, and FeactDOMComponent
would blow up. Let's fix FeactCompositeComponentWrapper
class FeactCompositeComponentWrapper { constructor(element) { this._currentElement = element ; }
mountComponent(container) { const Component = this._currentElement.type; const componentInstance = new Component(this._currentElement.props); let element = componentInstance .render();
while (typeof element .type === 'function') { element = new element.type(element.props).render(); }
const domComponentInstance = new FeactDOMComponent(element); domComponentInstance .mountComponent(container); }}
Fixing Feact.render() again
The first version of Feact.render()
could only handle primitive elements. Now it can only handle composite elements. It needs to be able to handle both. We could write a "factory" function that will create a component for us based on the element's type, but there's another approach that React took. Since FeactCompositeComponentWrapper
components ultimately result in a FeactDOMComponent
, let's just take whatever element we were given and wrap it in such a way that we can just use a FeactCompositeComponentWrapper
const TopLevelWrapper = function (props) { this.props = props ;};
TopLevelWrapper.prototype.render = function () { return this.props;};
const Feact = { render(element, container ) { const wrapperElement = this.createElement(TopLevelWrapper, element );
const componentInstance = new FeactCompositeComponentWrapper( wrapperElement );
// as before },};
ToplevelWrapper
is basically a simple composite component. It could have been defined by calling Feact.createClass()
. Its render
method just returns the user provided element. Since TopLevelWrapper will get wrapped in a FeactCompositeComponentWrapper
, we don't care what type the user provided element was, FeactCompositeComponentWrapper
will do the right thing regardless.
Conclusion to part one
With that, Feact can render simple components. As far as basic rendering is concerned, we've hit most of the major considerations. In real React, rendering is much more complicated as there are many other things to consider such as events, focus, scroll position of the window, performance, and much more.
Here's a final fiddle that wraps up all we've built so far:
JS Fiddle