Build your own React, part 1: DOM generator
Index:
1. Part 1: DOM generator
2. Part 2: Class and Functional Component
In this series, I would like to make myself a library that behave like React. In this post, I will start with generate DOM from JSX. Later on, other topics such as class, functional component, reconciliation, fiber, concurrent mode and hooks will be covered. As I am very bad at naming, I will just call this library React.
Before we begin, let’s review some of browser API to manipulate DOM node.
Basic DOM node manipulation
We just need to know some of the basic DOM manipulation functions:
- document.createElement(tagName[, options]) ref
- document.createTextNode(data) ref
- Node.appendChild(child) ref
- Node.replaceChild(newChild, oldChild) ref
- Node.removeChild(child) ref
No need to explain as the function name is quite clear but you can follow the reference url to read more about each functions. As an example, let’s convert this HTML to the equivalent JS
<div class="search-form">
<label>Search</label>
<input name="keyword" type="text"/>
</div>
our Javascript version will be
let divNode = document.createElement('div');
divNode.setAttribute('class', 'search-form');
let labelNode = document.createElement('label');
let labelText = document.createTextNode('Search');
labelNode.appendChild(labelText);
divNode.appendChild(labelNode);
let inputNode = document.createElement('input');
inputNode.setAttribute('name', 'keyword');
inputNode.setAttribute('type', 'text');
divNode.appendChild(inputNode);
JSX
JSX is just syntatic sugar for Javascript(specifically React) that ease the pain of writing HTML code in Javascript, but in fact, we can use React without any JSX.
We can use Babel repl(at https://babeljs.io/repl) to see what JSX will be translated to, for example:
<div class="search-form">
<label>Search</label>
<input name="keyword" type="text"/>
</div>
will be translate into this JS snippet
React.createElement(
"div",
{
class: "search-form"
},
React.createElement("label", null, "Search"),
React.createElement("input", {
name: "keyword",
type: "text"
})
);
Here we can see our first exposed function from React which is createElement
. Another one is render
and these two functions are all we need to implement.
Implement createElement
createElement
is a simple function that take a node type, its properties and return an object to represent it. We will call this object an element. So what will our element looks like?
It will have a type
key, indicate type of this element, whether it’s a DOM or a class component, etc.
It also needs a props
containing all of its properties and children.
let createElement = (type, options, ...children) => {
return {
type,
props: Object.assign({ children }, options)
}
}
Now, if we look at children
part, we can see there are two types of children:
- The one created by
React.createElement
- A simple string such as the string “Search” in
React.createElement("label", null, "Search"),
So, to unite these two types, we need to change our function a little bit.
If it’s a string, we will return an element with type of TEXT_NODE
and only one props which is its text content.
Of course, there are more types in React, in fact, 2 more: Class component and functional component. We will implement these types in later posts.
let createElement = (type, options, ...children) => {
return {
type,
props: Object.assign(
{ children: children.map(child =>
typeof child == 'string'
? {type: 'TEXT_NODE', props: {value: child}}
: child
)},
options
)
}
}
Some example output of createElement
React.createElement("label", null, "Search")
// {
// type: 'label',
// props: {
// children: [
// {type: 'TEXT_NODE', value: 'Search'}
// ]
// }
// }
React.createElement("input", { name: "keyword", type: "text" })
// {
// type: 'input',
// props: {
// name: 'keyword',
// type: 'text'
// children: []
// }
// }
React.createElement(
"div",
{
class: "search-form"
},
React.createElement("label", null, "Search"),
React.createElement("input", {
name: "keyword",
type: "text"
})
);
// {
// type: 'div',
// props: {
// class: 'search-form',
// chldren: [
// { type: 'label', props: {
// children: [
// {type: 'TEXT_NODE', value: 'Search'}
// ]
// }
// },
// { type: 'input'
// props: {
// name: 'keyword',
// type: 'text',
// children: []
// }
// }
// ]
// }
// }
Implement render
The element created by createElement
will be passed to render
function in order to actually create a DOM node and add it to DOM tree.
For now, our render will be as simple as:
let render = (element, parentNode) => {
let node = createInstance(element.type, element.props);
parentNode.appendChild(node);
}
In props
, there are three types of keys:
- attribute
- event
children
We will have a convention such that event is any thing that start with on
(eg: onClick, onBlur), otherwise it’s a HTML attribute.
// Some helper functions
let isAttribute = attrName => attrName != 'children' && !isEvent(attrName);
let isEvent = attrName => attrName.startsWith('on');
let eventName = event => event.substring(2).toLowerCase();
let createInstance = (type, props) => {
// now we will handle our first and most simple element type, ie: TEXT_NODE
if (type == 'TEXT_NODE') {
return document.createTextNode(props.value);
}
// This is our second type,
// for now it's just a string indicate a DOM node's type
let node = document.createElement(type);
for(let key in props) {
if (isEvent(key))
node.addEventListener(eventName(key), props[key])
else if (isAttribute(key))
node.setAttribute(key, props[key])
}
// loop through children and recursivelly create them
let children = props.children || [];
children.forEach(child => {
let childNode = createInstance(child.type, child.props);
// then append to it's parent
node.appendChild(childNode);
})
return node;
}
That is all that we need to render a JSX. Here are the full code of this first part:
In my next post, I will implement the remaining types of element, which is Class Component and Functional Component.