Create your own React — Didact
Build Your Own React (pomb.us)
We’ll follow the React source architecture, but without all the optimizations and non-essential features, and rewrite React from scratch step by step. The author named this simplified version of React Didact.
React 16.8 This project is based on React 16.8 and can be written in hook format without any class components.
To rewrite a React of our own, there are several steps:
- CreateElement function implementation
- Render function implementation
- Concurrent Mode implementation
- Fibers implementation
- Render and Commit phases are implemented
- The Reconciliation takes place
- Function Components implementation
- Hooks to realize
Step Zero: Review
First, let’s review some basic concepts. Figure out how React, JSX, and DOM elements work.
Here are three lines of React code:
const element = <h1 title="foo">Hello</h1>
const container = document.getElementById("root")
ReactDOM.render(element, container)
Copy the code
The first line defines a React element; The second line gets the DOM node; The third line of code renders the React element into the DOM.
Let’s do it in plain JavaScript. First, the first line, which is not valid JS syntax at all, JSX is converted to JS through build tools like Babel. The transformation is as simple as replacing the code in the tag with a call to the createElement function to generate the tag element, its attributes, child elements, and so on, as shown below.
const element = React.createElement(
"h1",
{ title: "foo" },
"Hello"
)
Copy the code
React.createElement can create an object, as shown below, and some validation may be required before creation. Therefore, we can safely replace function calls with function outputs.
const element = {
type: "h1".props: {
title: "foo".children: "Hello",}}Copy the code
This is an element, an object with multiple properties, and now we’re going to focus on the type and props properties.
type
Is a string that specifies the type of DOM node we want to create and is passed todocument.createElement
Function of thetagName
Parameters. It can also be a function, which will be discussed later in the function component.props
Is an object that contains all the keys and values of a JSX property. It also has a special propertychildren
.children
In this case, it’s a string, which is usually an array with more elements. That’s why elements are trees.
The second line of code is simple JS code, which we’ll skip.
The third line, reactdom.render, needs to be implemented. Render is where React modifies the DOM, so let’s implement it.
- First, use elements created by React to generate DOM nodes.
- Then, set the node properties.
- Next, we create a child node, which is a string. (We don’t use innerText here because createTextNode is more applicable to handling non-string child nodes.)
- Finally, the created child node and node are added to their parent node and container, respectively
const node = document.createElement(element.type)
node["title"] = element.props.title
const text = document.createTextNode("")
text["nodeValue"] = element.props.children
node.appendChild(text)
container.appendChild(node)
Copy the code
Now we have the same application as before, but without React.
The complete code is shown below:
const element = {
type: "h1".props: {
title: "foo".children: "Hello",}}const node = document.createElement(element.type)
node["title"] = element.props.title
const text = document.createTextNode("")
text["nodeValue"] = element.props.children
node.appendChild(text)
container.appendChild(node)
Copy the code
Step I createElement function
Take this JSX code as an example:
const element = (
<div id="foo">
<a>bar</a>
<b />
</div>
)
const container = document.getElementById("root")
ReactDOM.render(element, container)
Copy the code
Babel converts the above JSX code into JS code (we don’t need to care how Babel converts the above code into the following code) as follows:
const element = React.createElement(
"div",
{ id: "foo" },
React.createElement("a".null."bar"),
React.createElement("b"))const container = document.getElementById("root")
ReactDOM.render(element, container)
Copy the code
Let’s implement the createElement function.
As you can see from the above example, the createElement function actually generates an object at the end, so I can use the following code to represent our input and return conditions.
// From the third argument, both are children
function createElement(type, props, ... children) {
return {
type,
props: {
...props,
children,
},
}
}
Copy the code
I also need to do some work with the Children array because we also need to wrap non-objects like strings or numbers that might be contained in the child elements, so we need to wrap them in a special type, TEXT_ELEMENT.
React doesn’t wrap these non-object values or create an empty array when there are no children, but we do, because we want simple code, not high performance.
function createElement(type, props, ... children) {
return {
type,
props: {
...props,
children: children.map(child= >
typeof child === "object"
? child
: createTextElement(child)
),
},
}
}
function createTextElement(text) {
return {
type: "TEXT_ELEMENT".props: {
nodeValue: text,
children: [],},}}Copy the code
So far we’ve been using the React createElement function, so let’s rename it Didact. Add a comment when writing JSX, and Babel will now use Didact’s createElement function when parsing the following code.
/ * *@jsx Didact.createElement */
const element = (
<div id="foo">
<a>bar</a>
<b />
</div>
)
Copy the code
The new parsed complete code looks like this:
function createElement(type, props, ... children) {
return {
type,
props: {
...props,
children: children.map(child= >
typeof child === "object"
? child
: createTextElement(child)
),
},
}
}
function createTextElement(text) {
return {
type: "TEXT_ELEMENT".props: {
nodeValue: text,
children: [],},}}const Didact = {
createElement,
}
const element = Didact.createElement(
"div",
{ id: "foo" },
Didact.createElement("a".null."bar"),
Didact.createElement("b"))const container = document.getElementById("root")
ReactDOM.render(element, container)
Copy the code
Step II Render function
Let’s write our own reactdom. render function.
For now, we’ll just focus on the ability to add something to the DOM and deal with updates and deletions later.
The steps are as follows:
- First, create the DOM node and add it to the root node.
- We then recursively do the same for each child node.
- And then we have to deal with it
TEXT_ELEMENT
Type node. - Finally, we need to configure the properties for each node.
The implementation code is as follows:
function render(element, container) {
const dom = document.createElement(element.type)
const dom =
element.type === "TEXT_ELEMENT"
? document.createTextNode("")
: document.createElement(element.type)
const isProperty = key= >key ! = ="children"
Object.keys(element.props)
.filter(isProperty)
.forEach(name= > {
dom[name] = element.props[name]
})
/ / recursion
element.props.children.forEach(child= >
render(child, dom)
)
container.appendChild(dom)
}
const Didact = {
createElement,
render,
}
Copy the code
At this point, we have implemented a library that can render JSX as DOM. You can go to CodesandBox and see what it looks like.
Step III Concurrent Mode
At this point, we need to do a refactoring of the previous code before adding more.
Because of the recursion, once we start rendering recursively, we can’t stop until the entire element tree is rendered. If the element tree is large, it can block the main thread. If the browser needs to do high-priority things, such as processing user input or keeping the animation smooth, it must wait until the rendering is complete.
So we need to break down the rendering process into small units, and after each unit is done, we’ll ask the browser to break the rendering if we need to do something else.
We use the requestIdleCallback (MDN link) to implement the loop. You can think of requestIdleCallback as a setTimeout, but unlike requestIdleCallback, it doesn’t require us to set a timeout. The browser will run the callback when the main thread is idle.
React doesn’t actually use this API anymore. Instead, it uses scheduler, but it’s conceptually the same in our case.
RequestIdleCallback also gives us a deadline parameter. We can use it to check how much time we can spend manipulating the browser.
The feature is not yet stable, and the official word is that it will have to wait until React 18 for a stable version.
Let’s write the function related to the task unit loop:
To start the loop, we need to set up the first task unit, and then write a performUnitOfWork function that not only performs the task, but also returns the next task unit.
let nextUnitOfWork = null
function workLoop(deadline) {
let shouldYield = false
while(nextUnitOfWork && ! shouldYield) { nextUnitOfWork = performUnitOfWork( nextUnitOfWork )// Interrupt the task when time is running out
shouldYield = deadline.timeRemaining() < 1
}
requestIdleCallback(workLoop)
}
requestIdleCallback(workLoop)
function performUnitOfWork(nextUnitOfWork) {
// TODO
}
Copy the code
Step IV Fibers
To make it easier to manage task units, we need a special data structure called Fiber Tree, which is essentially a linked list.
Each element is encapsulated in fiber, and each fiber is a task unit.
For example, render the following element tree:
Didact.render(
<div>
<h1>
<p />
<a />
</h1>
<h2 />
</div>,
container
)
Copy the code
To render, we first create a fiber and set it to Next Tone of work. The rest of the tasks will be performed in the performUnitOfWork function. Each fiber needs to do three things:
- Add elements to the DOM;
- Created for children of the element
fiber
Object; - Switch to the next task unit;
Fiber is a data structure designed to make it easier to find the next task unit. This is why each fiber is associated with its first child, next sibling, and parent.
-
When we execute a fiber, if it has a child, then the child fiber becomes the next task unit. From our example above, when we are done with fiber for div, the next task unit will be FIBER for H1.
-
If fiber currently has no children, we will use its sibling element as the next task unit. Fiber of P has no sub-, SO I will execute fiber of A.
-
If Fiber has no children and no brothers, it will go to its “uncle” (its brother’s father), just like A and H2 do.
-
If the parent fiber doesn’t have any siblings, we’ll keep going up to the parent until we get to the root node. This also means that all rendering tasks have been completed.
Let’s refactor the code, starting with the render function createDom, which creates the DOM.
function createDom(fiber) {
const dom =
fiber.type === "TEXT_ELEMENT"
? document.createTextNode("")
: document.createElement(fiber.type)
const isProperty = key= >key ! = ="children"
Object.keys(fiber.props)
.filter(isProperty)
.forEach(name= > {
dom[name] = fiber.props[name]
})
return dom
}
Copy the code
The render function is called to assign the global nextUnitOfWork variable so that the workLoop function in Step III can be executed.
function render(element, container) {
nextUnitOfWork = {
dom: container,
props: {
children: [element],
},
}
}
Copy the code
Let’s complete Step III’s performUnitOfWork function for TODO
function performUnitOfWork(fiber) {
// add dom node
if(! fiber.dom) { fiber.dom = createDom(fiber) }if (fiber.parent) {
fiber.parent.dom.appendChild(fiber.dom)
}
// create new fibers
const elements = fiber.props.children
let index = 0
let prevSibling = null
while (index < elements.length) {
const element = elements[index]
const newFiber = {
type: element.type,
props: element.props,
parent: fiber,
dom: null,}if (index === 0) {
fiber.child = newFiber
} else {
prevSibling.sibling = newFiber
}
prevSibling = newFiber
index++
}
// return next unit of work
if (fiber.child) {
return fiber.child
}
let nextFiber = fiber
while (nextFiber) {
if (nextFiber.sibling) {
return nextFiber.sibling
}
nextFiber = nextFiber.parent
}
}
Copy the code
Step V Render and Commit phase
There is a problem with this sentence in the code above. Each time we process an element, we add a new node to the DOM. Also, the browser may interrupt our task before we finish rendering the entire tree. In this case, the user will see an incomplete UI, which we do not expect.
// This code needs to be deleted
if (fiber.parent) {
fiber.parent.dom.appendChild(fiber.dom)
}
Copy the code
So we need to remove the code that modifies the DOM. Then, we need to keep track of the root node of the Fiber tree. We call this approach Work in Progress root, or wipRoot.
Once we have completed all the tasks (and we know that there is no next task unit), we commit the entire Fiber tree to the DOM. We do this in the commitRoot function, recursively adding all nodes to the DOM.
function commitRoot() {
// add nodes to dom
commitWork(wipRoot.child)
wipRoot = null
}
function commitWork(fiber) {
if(! fiber) {return
}
const domParent = fiber.parent.dom
domParent.appendChild(fiber.dom)
commitWork(fiber.child)
commitWork(fiber.sibling)
}
function render(element, container) {
wipRoot = {
dom: container,
props: {
children: [element],
},
}
nextUnitOfWork = wipRoot
}
let nextUnitOfWork = null
let wipRoot = null
function workLoop(deadline) {
let shouldYield = false
while(nextUnitOfWork && ! shouldYield) { nextUnitOfWork = performUnitOfWork( nextUnitOfWork )// Interrupt the task when time is running out
shouldYield = deadline.timeRemaining() < 1
}
if(! nextUnitOfWork && wipRoot) { commitRoot() } requestIdleCallback(workLoop) } requestIdleCallback(workLoop)Copy the code
Step VI Reconciliation
So far, we’ve only added a few things to the DOM, but how do you update and remove nodes? .
That’s all we need to do at this stage, we need to compare the elements received on the Render function to our latest Fiber tree.
Therefore, we need to save the latest fiber tree reference at commit time. We’ll call it currentRoot.
We also need to add an alternate property for each fiber, which records the reference of the old fiber, the same fiber we submitted last time.
Deletions are an array that keeps track of the nodes we want to delete.
function commitRoot() {
// add nodes to dom
commitWork(wipRoot.child)
// Add this sentence
currentRoot = wipRoot
wipRoot = null
}
function commitWork(fiber) {
// as above, omit
}
function render(element, container) {
wipRoot = {
dom: container,
props: {
children: [element],
},
// Add attributes
alternate: currentRoot,
}
/ / new
deletions = []
nextUnitOfWork = wipRoot
}
let nextUnitOfWork = null
// Add a new definition
let currentRoot = null
let wipRoot = null
// Add a new definition
let deletions = null
Copy the code
Next, the code for creating new Fibers in the performUnitOfWork function is extracted into the reconcileChildren function.
function performUnitOfWork(fiber) {
if(! fiber.dom) { fiber.dom = createDom(fiber) }const elements = fiber.props.children
// Replace the original code with this function
reconcileChildren(fiber, elements)
if (fiber.child) {
return fiber.child
}
let nextFiber = fiber
while (nextFiber) {
if (nextFiber.sibling) {
return nextFiber.sibling
}
nextFiber = nextFiber.parent
}
}
Copy the code
In the reconcileChildren function, we reconcile the old fiber with the new elements.
We simultaneously iterate over the children of old Fiber (wipFiber.alternate) and the array of elements we want to coordinate.
Instead of comparing the old Fiber to the new element in every detail, we use types (which is the original Diff algorithm used by React). Specific methods are as follows:
- If the old
fiber
As with the new element type, we can keep the DOM node and just update it with the new attribute - If the type is different and there are new element types that are different, that means we need to create a new DOM node
- If the type is different and there is an old
fiber
, we need to remove the old node
The second and third may be both or only one of them.
React also uses keys to optimize the reconciliation process. For example, it detects when a child element changes position in an array of elements.
Here’s how to complete the three methods:
- When the old
fiber
As soon as the new element type is the same, we create a new onefiber
And keep the oldfiber
The DOM node and attributes of the new element. We also need to add tofiber
Add a new attribute to this data structure:effectTag
, we will be incommit
Phase uses this property. Here our property value is zeroUPDATE
. - Then, if we need to add a new DOM node, we give new
fiber
Mark onePLACEMENT
. - In cases where nodes need to be deleted, we give the old
fiber
Add aDELETION
The tag.
function reconcileChildren(wipFiber, elements) {
let index = 0
let oldFiber = wipFiber.alternate && wipFiber.alternate.child
let prevSibling = null
while(index < elements.length || oldFiber ! = =null) {
const element = elements[index]
let newFiber = null
// compare oldFiber to element
const sameType =
oldFiber &&
element &&
element.type == oldFiber.type
if (sameType) {
// update the node
newFiber = {
type: oldFiber.type,
props: element.props,
dom: oldFiber.dom,
parent: wipFiber,
alternate: oldFiber,
effectTag: "UPDATE",}}if(element && ! sameType) {// add this node
newFiber = {
type: element.type,
props: element.props,
dom: null.parent: wipFiber,
alternate: null.effectTag: "PLACEMENT",}}if(oldFiber && ! sameType) {// delete the oldFiber's node
oldFiber.effectTag = "DELETION"
deletions.push(oldFiber)
}
if (oldFiber) {
oldFiber = oldFiber.sibling
}
if (index === 0) {
fiber.child = newFiber
} else if (element) {
prevSibling.sibling = newFiber
}
prevSibling = newFiber
index++
}
}
Copy the code
Next, we need to transform the COMMIT phase to handle the new effectTags property.
effectTag
forPLACEMENT
Let’s do what we did beforefiber.dom
Add to its parent nodeeffectTag
forUPDATE
, we need to update the properties of the existing DOM nodeseffectTag
forDELETION
, we delete the child node
function commitRoot() {
// Add processing for nodes to be removed
deletions.forEach(commitWork)
commitWork(wipRoot.child)
currentRoot = wipRoot
wipRoot = null
}
function commitWork(fiber) {
if(! fiber) {return
}
const domParent = fiber.parent.dom
if (fiber.effectTag === "PLACEMENT"&& fiber.dom ! =null) {
domParent.appendChild(fiber.dom)
} else if (fiber.effectTag === "UPDATE"&& fiber.dom ! =null) {
updateDom(fiber.dom, fiber.alternate.props, fiber.props)
} else if (fiber.effectTag === "DELETION") {
domParent.removeChild(fiber.dom)
}
commitWork(fiber.child)
commitWork(fiber.sibling)
}
Copy the code
So let’s implement updateDom.
We will compare the properties of the old fiber with those of the new fiber, remove the properties that no longer exist and set new or changed properties.
Note: Special properties like event listeners that start with “on” need to be handled differently.
function createDom(fiber) {
const dom =
fiber.type === "TEXT_ELEMENT"
? document.createTextNode("")
: document.createElement(fiber.type)
updateDom(dom, {}, fiber.props)
return dom
}
const isEvent = key= > key.startsWith("on")
const isProperty = key= >key ! = ="children" && !isEvent(key)
const isNew = (prev, next) = > key= >prev[key] ! == next[key]const isGone = (prev, next) = > key= >! (keyin next)
function updateDom(dom, prevProps, nextProps) {
// Remove old or changed event listeners
Object.keys(prevProps)
.filter(isEvent)
.filter(
key= >! (keyin nextProps) ||
isNew(prevProps, nextProps)(key)
)
.forEach(name= > {
const eventType = name
.toLowerCase()
.substring(2)
dom.removeEventListener(
eventType,
prevProps[name]
)
})
// Remove old properties
Object.keys(prevProps)
.filter(isProperty)
.filter(isGone(prevProps, nextProps))
.forEach(name= > {
dom[name] = ""
})
// Set new or changed properties
Object.keys(nextProps)
.filter(isProperty)
.filter(isNew(prevProps, nextProps))
.forEach(name= > {
dom[name] = nextProps[name]
})
// Add event listeners
Object.keys(nextProps)
.filter(isEvent)
.filter(isNew(prevProps, nextProps))
.forEach(name= > {
const eventType = name
.toLowerCase()
.substring(2)
dom.addEventListener(
eventType,
nextProps[name]
)
})
}
Copy the code
See CodesandBox for the complete code and usage examples
One final note: Reconciliation refers to the process of synchronizing the Virtual AND real DOM through libraries such as ReactDOM.
Step VII Function Components
This step is to get our framework to support functional components.
First, let’s change the example. We will write a simple function component that returns an H1 element.
/ * *@jsx Didact.createElement */
function App(props) {
return <h1>Hi {props.name}</h1>
}
const element = <App name="foo" />
const container = document.getElementById("root")
Didact.render(element, container)
Copy the code
The above code is JSX, and when converted to JS it looks like this;
function App(props) {
return Didact.createElement(
"h1".null."Hi ",
props.name
)
}
const element = Didact.createElement(App, {
name: "foo",})const container = document.getElementById("root")
Didact.render(element, container)
Copy the code
Function components are different in two ways:
- Function component
fiber
No DOM nodes - Children of function components come from the result of function execution, not directly
props
To get them
Now, we need to check whether the fiber type is a function and proceed accordingly.
The updateHostComponent function is the same as before.
In the updateFunctionComponent function, we need to run the function to get the contents of the child node.
In the example above, fiber.type is the App function that returns the h1 element when we run it.
Then, once we get the child nodes, reconciliation can proceed as before.
function performUnitOfWork(fiber) {
const isFunctionComponent = fiber.type instanceof Function
if (isFunctionComponent) {
updateFunctionComponent(fiber)
} else {
updateHostComponent(fiber)
}
if (fiber.child) {
return fiber.child
}
let nextFiber = fiber
while (nextFiber) {
if (nextFiber.sibling) {
return nextFiber.sibling
}
nextFiber = nextFiber.parent
}
}
function updateHostComponent(fiber) {
if(! fiber.dom) { fiber.dom = createDom(fiber) } reconcileChildren(fiber, fiber.props.children) }function updateFunctionComponent(fiber) {
const children = [fiber.type(fiber.props)]
reconcileChildren(fiber, children)
}
Copy the code
Another change that needs to be made is the commitWork function.
Because of the function component, we might have cases where Fiber doesn’t have a native DOM node (e.g. App is not a native HTML tag).
Thus, we need to go up the Fiber tree until we find a fiber with a native DOM node.
To delete it, I also need to go down the Fiber tree until I find a child node with a native DOM node.
function commitWork(fiber) {
if(! fiber) {return
}
// Modify the logic here
let domParentFiber = fiber.parent
while(! domParentFiber.dom) { domParentFiber = domParentFiber.parent }const domParent = fiber.parent.dom
if (fiber.effectTag === "PLACEMENT"&& fiber.dom ! =null) {
domParent.appendChild(fiber.dom)
} else if (fiber.effectTag === "UPDATE"&& fiber.dom ! =null) {
updateDom(fiber.dom, fiber.alternate.props, fiber.props)
} else if (fiber.effectTag === "DELETION") {
/ / modify
commitDeletion(fiber, domParent)
}
commitWork(fiber.child)
commitWork(fiber.sibling)
}
function commitDeletion(fiber, domParent) {
if (fiber.dom) {
domParent.removeChild(fiber.dom)
} else {
commitDeletion(fiber.child, domParent)
}
}
Copy the code
Step VIII Hooks
The final step is to add state to the function component. Modify the above example.
const Didact = {
createElement,
render,
useState,
}
/ * *@jsx Didact.createElement */
function Counter() {
const [state, setState] = Didact.useState(1)
return (
<h1 onClick={()= > setState(c => c + 1)}>
Count: {state}
</h1>)}const element = <Counter />
const container = document.getElementById("root")
Didact.render(element, container)
Copy the code
The Counter component content calls useState.
Before calling the function component, we need to initialize some global variables so that we can use them in the useState function.
First, let’s set up work in Progress Fiber, also known as wipFiber.
We also need to add a hooks array in Fiber to enable multiple calls to useState in the same component. We track the index of the current hooks.
let wipFiber = null
let hookIndex = null
function updateFunctionComponent(fiber) {
wipFiber = fiber
hookIndex = 0
wipFiber.hooks = []
const children = [fiber.type(fiber.props)]
reconcileChildren(fiber, children)
}
Copy the code
When the function component calls useState, we check to see if we already have a hook. We use the hook index to check the alternate property of Fiber.
If we already have a hook, we copy state from the original hook to the new hook, if not, we initialize state.
We then add the new hook to Fiber, increase the index of the hook by 1, and return state.
function useState(initial) {
const oldHook =
wipFiber.alternate &&
wipFiber.alternate.hooks &&
wipFiber.alternate.hooks[hookIndex]
const hook = {
state: oldHook ? oldHook.state : initial,
}
wipFiber.hooks.push(hook)
hookIndex++
return [hook.state]
}
Copy the code
UseState should also return a function to update state, so we define a setState function that receives an operation (for the Counter example, this operation is a function that increments state by one).
We put this operation in the queue property of the hook.
We then do something similar to what we did in the Render function, setting a new Work in Progress root as the next task unit so that the task loop can start a new render phase.
We do this the next time we render the component, we take all the action functions from the original hook queue and apply them one by one to the new hook state, so when we return state, it will be updated.
function useState(initial) {
const oldHook =
wipFiber.alternate &&
wipFiber.alternate.hooks &&
wipFiber.alternate.hooks[hookIndex]
const hook = {
state: oldHook ? oldHook.state : initial,
queue: [],}const actions = oldHook ? oldHook.queue : []
actions.forEach(action= > {
hook.state = action(hook.state)
})
const setState = action= > {
hook.queue.push(action)
wipRoot = {
dom: currentRoot.dom,
props: currentRoot.props,
alternate: currentRoot,
}
nextUnitOfWork = wipRoot
deletions = []
}
wipFiber.hooks.push(hook)
hookIndex++
return [hook.state, setState]
}
Copy the code
So far, we’ve built our own React.
The full code can be viewed at CodesandBox or Github.
conclusion
First off, hats off to the original author for creating this article to help us understand how React works. One of the goals of this article is to make it easier to dive into deeper React code. That’s why we use the same variable and function names almost everywhere.
For example, if you add a breakpoint to a function component in a real responding application, the call stack should display:
- workLoop
- performUnitOfWork
- updateFunctionComponent
We didn’t include many React features and optimizations. For example, these are different from React:
- We walked through the entire tree during the rendering phase. while
React
Follow some rules and skip the entire subtree without changing - I also walked through the entire tree during the commit phase. while
React
It keeps a linked list, and the whole linked list has changedfiber
, will only access thesefiber
. - We build a new one at a time
work in progress tree
When we will give eachfiber
Create a new object. whileReact
It’s going to recycle from the previous treefiber
. - When we receive new updates during the render phase, they will be discarded
work in progress tree
And again fromroot
Start. whileReact
Each update is marked with a timestamp, which is used to determine which update has a higher priority.
Of course, there is more to it than that.
We can also easily add some features:
- Use an object to set it
style
attribute - flatten children arrays
- useEffect hook
- reconciliation by key