Build Your Own React-like Library From Scratch

Josh Lin
JavaScript in Plain English
5 min readApr 14, 2021

--

React

Nearly every software developer more-or-less knows React. Even some backend developers or machine learning developers — and even some non-developers. Yes, it’s a good library, elegant and promising. Every frontend developer talks about its API, its optimization, its inner mechanisms.

Inner Mechanism

The inner mechanisms are the most important part, but how to understand them? Reading React source code is certainly an option but can feel overwhelming. So, do we read some digests from someone who has read all the source code? I wonder how many digests we can read to get a complete understanding. So, my suggestion is to build React on your own!

Build your own React

Sounds crazy? Just a little, maybe. We just need to code some essential parts, which can make a tiny program work — that’s all. You can try to build the parts you are interested in yourself. If you don’t have any idea how React works and how to build one, you can just follow me. My source code can be found here:

Step 1. Render components

Suppose we have such a component:

<p>hello world
<span style={{color:'red'}}>!</span>
</p>

It’s in JSX format, so in React, the code would be:

React.createElement('p',null,'hello world',
React.createElement('span',{style:{color:'red'}},'!')
)

React.createElement is not convenient for me. I just use a class D instead, so I need to use the new keyword. And I changed the children into the props parameter with a fixed key children . So, the component definition and usage shall be like this:

import { D, render } from '../lib'let el = document.createElement('div')
document.body.append(el)
render(el, new D('p', {
children: [
'hello world',
new D('span', {
children: '!',
style: { color: 'red' }
})
]
}))

And we expect it to render like this:

So, we just need to recursively translate D instances into elements and append them to the DOM tree.

// lib/d.jsexport class D {
...
init(parent) {
this.parent = parent
if (!this.component) {
// text
this.el = document.createTextNode(this.props.children)
} else if (typeof this.component == 'string') {
// html element
this.el = document.createElement(this.component)
let { children, ...rest } = this.props
assignProps(this.el, rest)
this.children = normalizeDArr(children)
} else {
// component
setREACT_LIKE_CUR_COMPONENT(this)
setREACT_LIKE_CUR_COMPONENT_STATEI(0)
this.children = normalizeDArr(this.component(this.props))
}
// append to DOM tree
if (this.el) this.parent.append(this.el)
// init children recursively
this.children.forEach(x => x.init(this.el || this.parent))
}
...
}

If you check out my code and run npm run dev1 , you can get this result:

This represents Virtual DOM in some way, and each HTML element has a D instance bound to it.

Step 2. Render with state

We now render the hello world literally or with a variable containing the same value. Then, the only thing we need to do is store it. useState stores data in array.

You will need to know which component is rendering when calling useState and the corresponding index of the state array.

// lib/hooks.js
export function useState(initalValue) {
let d = getREACT_LIKE_CUR_COMPONENT() // get component
let i = getREACT_LIKE_CUR_COMPONENT_STATEI() // get index
if (d.states[i] === undefined) d.states[i] = initalValue
let v = d.states[i]
setREACT_LIKE_CUR_COMPONENT_STATEI(i + 1)
return [v, v1 => {
d.states[i] = v1
d.updateState()
}]
}

useState is called inside the Appcomponent. So before the App component is called, we need to store component and reset state array index to 0 .

// lib/d.js
export class D {
constructor(component, props = {}) {
...
this.states = []
...
}
init(parent) {
...
// render component
setREACT_LIKE_CUR_COMPONENT(this)
setREACT_LIKE_CUR_COMPONENT_STATEI(0)
this.children = normalizeDArr(this.component(this.props))
...
}
}

Now, you can useuseState and use the state in render.

Step 3. Handle event

React event is globally managed, so we mimic it with delegation:

// lib/event.js
const EVENT_TYPES = ['click']
// delegate from mount point
export function listen(el) {
return EVENT_TYPES.map(eventType => {
let listener = e => {
let key = 'on' + capitalize(eventType)
bubble(e.target, e, key)
}
el.addEventListener(eventType, listener)
})
}
// bubble event until it's handled
function bubble(el, e, key) {
if (!el) return
if (el[key]) return el[key](e)
bubble(el.parentElement, e, key)
}

Then add theonClick prop to button:

// step2/App.jsimport { D, useState } from "../lib"const App = () => {
let [i, setI] = useState(0)
let [str, setStr] = useState('hello counter!')
return [
str,
new D('div', {
children: [
new D('button', {
children: '-',
// onClick assigned to element as prop
onClick: () => setI(i - 1)
}),
i + '',
new D('button', {
children: '+',
// onClick assigned to element as prop
onClick: () => setI(i + 1)
})
]
})
]
}
export default App

React is more a scheduler than a reactor, so how can it merge state/prop changes and handle them at once? Here, I used Promise.resolve .

// lib/d.js
export class D {
...
updateState() {
this.stateChangeCount++
// wait for changes to merge
Promise.resolve().then(() => {
if (!this.stateChangeCount) return
this.stateChangeCount = 0
if (this.el) {
let { children, ...rest } = this.props
assignProps(this.el, rest)
if (!this.component) {
return this.el.data = children
}
}
let children = this.getChildren(this.props)
...
}
...
}

It’s not that hard a problem, right? If you run npm run dev with my code, you get this result. Try to click on the buttons.

An Important Note

React can do many more things. For example, React can delay renders to the next frame if some render takes too much time, so that you don’t get a non-responsive site. It can also do list comparison, transition, suspending, and so on. But you can not expect 200 lines of code to do so much. It’s just a guide to make a React-like library and an easy explanation of the underlying React mechanism.

More content at plainenglish.io

--

--