I work with React every single day. I try my best to follow various guides, discussions , videos, blog posts, personal learnings, etc in order to write good and maintainable code, yet I've rarely had to dig into the framework like I've had to for other types of frameworks and libraries in order to achieve the same goal. I really appreciate this about React. After just learning the foundations, like props and state, classes vs stateless components, and a little about lifecycle methods you are already on your way to writing a decent React app.
But I couldn't help but look at a typical component and wonder what the hell was going on. I mean, it all pretty much looks like vanilla JavaScript. Well, apart from JSX, nothing looks super out of the ordinary... right? So where is all this React magic coming from? How do these lifecycle methods get called? Where is setState defined?
I recently rebuilt my own Redux/React-Redux and I use some of the learnings in both code and new patterns from that on a daily basis. This made me realize that I needed to do the same thing for React.
In the spirit of that, together we will be building a mini React clone. It is hopelessly naive, but it follows for the most part the same design and naming conventions of React.
You can find the completed code here
It might be advantageous to realize that React is a very helpful library for building out dynamic trees. For web apps we are using it to build a DOM tree or for React Native we are using the same React engine to build out a native app hierarchy of ui controllers.
(Web app vs native app using React)
In order to give the most value from this I'm going to rely on Pareto's Principle, or the 80-20 rule, or in any situation, 20 percent of the inputs or activities are responsible for 80 percent of the outcomes or results. I'm going to show you from a high level how the React trees are formed, respond to some lifecycle methods, and to some degree how they get updated through setState.
In the code snippet below, there's a JavaScript class called App with a single function called render. React and ReactDOM and their APIs aren't built yet, but we expect it to return a div with a message in it. The createElement function will return us an object that will represent a dom element. The ReactDOM.render function will set the base node and then recursively make its way down the tree.
class App {
render() {
return React.createElement('div', null, this.props.message)
}
}
ReactDOM.render(React.createElement(App,
{message: "this is the result of calling this.props.message"}),
document.getElementById('root'))
Hmm, there are already some things that don't exactly look like the React apps you've seen before. You might be wondering why our class isn't extending React.Component, or why we are not using JSX in this example. Simply put, this is what comes out on the other side after babel transpiles JSX. I also want to show everything in vanilla JS as a way of showing how arguments are passed around.
But for a little familiarity
<div> {this.props.message} </div>
would be equivalent to React.createElement('div', null, this.props.message )
. This is the function decleration, createElement(type, props, children)
.
<App message={"this is the result of calling this.props.message"}/>
would be equivalent to React.createElement(App, {message: "this is the result of calling this.props.message})
. This is the function decleration, createElement(type, props, children)
.
The function createElement takes 3 arguments:
type - Denotes a primitive type like a 'div', 'p', 'h1', etc. or a reference to a user defined class.
props - Properties like what color to render or event listeners.
children - Can be text, a primitive type, or a user defined object. Think of children as the text in <div> children </div>
it then will return an object({}) known as an element. Elements are a data representation of a DOM element.
React = {
createElement(type, props, children) {
const element = {
type,
props: props || {}
};
if (children) {
element.props.children = children;
}
return element;
}
}
The ReactDOM object provides a DOM-specific method that can be used at the top level of your app. Think of the ReactDOM as the glue between your React app and the DOM. Typically you would only ever call ReactDOM.render()
but it does have the ability to find specific DOM elements with ReactDOM.findNode()
.
When calling render we need to first wrap the element in a private React class. This enables React to have control over this passed in object. React relies upon this to build out the DOM tree.
ReactDOM = {
render(element, container) {
const componentWrapperInstance = new ComponentWrapper(element);
componentWrapperInstance.mountComponent(container);
}
}
Here is our componentWrapper
. The main concept is that every user defined component is incomplete. Wrapping each component gives React the ability to activate lifecycle hooks and let each component participate in React.
In this we will instantiate our element, call various lifecycle and component methods, and eventually update (but that will come later).
class ComponentWrapper {
constructor(element) {
this._currentElement = element
}
mountComponent(container) {
const ComponentType = this._currentElement.type
const componentTypeInstance = new ComponentType(this._currentElement.props) // see extends Component below
if (componentTypeInstance.componentWillMount) {
componentTypeInstance.componentWillMount()
}
this._performInitialMount(componentTypeInstance, container)
if (componentTypeInstance.componentDidMount) {
componentTypeInstance.componentDidMount()
}
}
_performInitialMount(componentTypeInstance, container) {
// element => {type: 'div', props: {children: "this is the result of..."}}
const element = componentTypeInstance.render();
const child = instantiateReactComponent(element);
child.mountComponent(container);
}
}
Calling mountComponent instantiates an element, checks for eventual lifecycle methods defined on the user provided class, and calls to perform the initial mount. In _performInitialMount
we are essentially calling render on the componentTypeInstance
and then wrapping the child to recursively build out the tree.
Just like we did in ReactDOM.render we need to wrap each and every public component in order to call upon them from the private wrapper. When instantiateReactComponent
is called from _performInitialMount
we will check the element type and then wrap it accordingly so that when mountComponent
is called on the return object from instantiateReactComponent
, the right implementation of mountComponent whether it is the one defined in ComponentWrapper or ReactDOMComponent will be called. We will define the ReactDOMComponent and its own mountComponent function shortly.
function instantiateReactComponent(element) {
if (typeof element.type === 'string') {
return new ReactDOMComponent(element);
} else if (typeof element.type === 'function') {
return new ComponentWrapper(element);
}
}
I promised we would come back to extending a Component and now is the time. Extending a Component allows us to pass props to a user defined class when a constructor is not present like it is in the App class. We can also define functions like setState in Component which will then be inherited into any class that extends the Component.
class Component {
constructor(props) {
this.props = props
}
setState(partialState) {
//more on this later
}
}
class App extends Component {
render() {
return React.createElement('div', null, this.props.message )
}
}
const appInstance = new App({ message: "hey I am accessible" })
console.log(appInstance.props.message) // => "hey I am accessible"
console.log(appInstance.setState) // => [Function: setState]
The ReactDOMComponent is where we will finally mount a primitive element to the DOM. Instantiating this and calling mountComponent
will create a real DOM element and append it to your document.
As a reminder, if the element type that was passed in to instantiateReactComponent is a string, this private class will wrap the element instead of the ComponentWrapper. This gives us an API that directly interacts with the document.
class ReactDOMComponent {
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);
}
}
There you go! Your tree is rendered. You should now be able to see "this is the result of calling this.props.message"! Here is a working codepen
React has the concept of wrapper components: private components that react uses behind the scenes to handle rendering and updating. This is in addition to the public components that are user provided like App
. These private wrappers control the user defined components like a puppet master, skillfully rendering or updating any public component that needs it, changing attributes like state and props. However, there is one problem. While it is easy for the private wrappers to have access to the public components, the reverse is not as true. In order for us to access the wrapper component from the public domain we rely upon the functionality of a ReactInstanceMap
. The ReactInstanceMap has two functions:
set: We will pass in two arguments key and value. Key being the public facing class and value being that components private wrapper.
get: This will get called inside of setState which is part of the public facing class. Calling ReactInstanceMap.get(this)
will return the private wrapper of this public class.
const ReactInstanceMap = {
set(key, value) {
key.__reactWrapperInstance = value;
},
get(key) {
return key.__reactWrapperInstance;
}
}
Let's first show how to set the component wrapper instance so it can be accessed in setState.
class ComponentWrapper {
...
mountComponent(container) {
const ComponentType = this._currentElement.type
const componentTypeInstance = new ComponentType(this._currentElement.props)
this._instance = componentTypeInstance;
ReactInstanceMap.set(componentTypeInstance, this)
...
}
performUpdate(){
// todo
}
}
Here we are, revisiting mountComponent. After instantiating the ComponentType we will first set that instance onto the ComponentWrapper instance itself so it can be accessible in performUpdate. We then call upon the react instance map to set this ComponentWrapper instance onto the ComponentType. This allows us to get back to the ComponentWrapper instance when we are in setState.
Now let's head back and define setState in the Component class.
class Component {
...
setState(partialState) {
const componentWrapperInstance = ReactInstanceMap.get(this);
componentWrapperInstance._pendingPartialState = partialState;
componentWrapperInstance.performUpdate();
}
...
}
setState, which is called inside the user defined component, grabs its corresponding wrapper instance from the instance map, sets the partial state, and then calls performUpdate.
For perform update to work you will first need to add a line of code to the _performInitialMount
class method.
class ComponentWrapper {
...
_performInitialMount(componentInstance, container) {
const element = componentInstance.render();
const child = instantiateReactComponent(element);
this._childComponentWrapper = child;
child.mountComponent(container);
}
...
}
Think back to when we were building out the tree. As _performInitialMount got called over and over we want the child wrapped instance to be set in the current parent stack call. This gives us the link to scaling down the tree for updating purposes.
performUpdate() {
const inst = this._instance;
const nextState = Object.assign({}, inst.state, this._pendingPartialState);
inst.state = nextState;
this._pendingPartialState = null;
this._childComponentWrapper.performUpdate();
}
With the child component wrapper set from above we can proceed down the tree. The only thing left to do is create a performUpdate function in the ReactDOMComponent class if the childComponentWrapper is a ReactDOMComponent.
class ReactDOMComponent {
...
performUpdate() {
console.log("I was called")
}
}
The only reason we are creating that is so this React clone won't break when called upon. In this clone we aren't updating props during the update cycle, only changing state for the component that requested it. We could have ended there and stopped the update but I thought it would be important to show how to scale down the tree during this very shallow update. Once you know how to scale down, updating props on the way down would be the next step. finished codpen
This is obviously a very simplified version of React and I've taken some liberties to convey a basic flow on how to build and update a React DOM tree, but in reality there is so much more involved. This was meant as a small peak that hopefully clarified some concepts and shed a little light down your own journey.
We are directly calling mount component on both the new ComponentWrapper and ReactDOMComponent instances in the _performInitialMount function. You would instead pass these instances to the reconciler which would handle all the logic of what and how to call the various passed in instances.
In addition to this because we directly call onto the components we limit this version of React to only ever working as a web app. Decoupling that logic would enable the reuse that React benefits from.
Lets tackle create element first
could be "Implementing createElement. In that following paragraph you should describe what
createElementdoes and its API. Not every reader will be familiar with React, and even if they are, they might not have used
createElement` yet.Same applies to all other React concepts that you bring up.