Skip to content

Instantly share code, notes, and snippets.

@lmuntaner
Last active August 22, 2023 13:57
Show Gist options
  • Save lmuntaner/06c9f2d45a1d208f989a8014f0feea27 to your computer and use it in GitHub Desktop.
Save lmuntaner/06c9f2d45a1d208f989a8014f0feea27 to your computer and use it in GitHub Desktop.
Building a React-Like Library - Part III: useState
const createElement = (tag, props, children ) => ({
tag,
props,
children,
element: null,
currentNode: null,
});
const hooks = [];
let useStateCalls = -1;
const useState = (initialState) => {
useStateCalls += 1;
if (hooks[useStateCalls] === undefined) {
const hook = [initialState]
hooks[useStateCalls] = hook;
hook[1] = (updatedState) => {
hook[0] = updatedState;
renderApp();
}
}
return hooks[useStateCalls];
}
// Step 1: Rename `render` and change signature
const diff = (prevVnode, vnode, parent) => {
// Step 3: Start using prevVnode
if (prevVnode === null) {
if (typeof vnode === "string" || typeof vnode === "number") {
return parent.appendChild(document.createTextNode(vnode));
}
if (typeof vnode.tag === "function") {
const nextVNode = vnode.tag(vnode.props);
// Step 9: Keep reference of the node
vnode.currentNode = nextVNode;
// Step 1: Pass `null`
return diff(null, nextVNode, parent);
}
const element = document.createElement(vnode.tag);
// Step 8: save the element in the vnode
vnode.element = element;
if (vnode.props) {
Object.keys(vnode.props).forEach(key => {
const value = vnode.props[key];
// Add event listeners from props `on<Event>`
if (key.startsWith("on")) {
const event = key.slice(2).toLowerCase();
element.addEventListener(event, value);
} else {
element.setAttribute(key, value);
}
});
}
if (vnode.children) {
// Step 1: Pass `null`
vnode.children.forEach(child => diff(null, child, element));
}
return parent.appendChild(element);
} else {
// Step 5: Create branches for each type of node
if (typeof vnode === "string" || typeof vnode === "number") {
// Step 6: Replace old node with the new text node if they differ
if (vnode !== prevVnode) {
// Search for the old node in the parent
const selectedNode = Array.from(parent.childNodes).find((node) => node.textContent == prevVnode);
// Change content of the node
selectedNode.textContent = vnode;
}
}
if (typeof vnode.tag === "string") {
// Assume that the tags are the same
// Assume attributes are the same
// Step 8: Pass the element from the old vnode
vnode.element = prevVnode.element;
if (vnode.props) {
Object.keys(vnode.props).forEach(key => {
const value = vnode.props[key];
const prevValue = prevVnode.props[key];
// Step 10: Reset event listeners
if (key.startsWith("on")) {
const event = key.slice(2).toLowerCase();
vnode.element.removeEventListener(event, prevValue);
vnode.element.addEventListener(event, value);
}
});
}
// Step 7: Diff children
if (vnode.children) {
// Assume that the number of children is the same
vnode.children.forEach((child, index) => {
const oldChild = prevVnode.children[index] ?? null;
diff(oldChild, child, vnode.element);
});
}
}
if (typeof vnode.tag === "function") {
// diff functional components
const newVnode = vnode.tag(vnode.props);
// Step 9: Keep reference of the node
vnode.currentNode = newVnode;
// Step 9: Pass the prevNode from the old vnode
diff(prevVnode.currentNode, newVnode, parent);
}
}
};
const Title = () => createElement("h1", {}, ["Hello from dynamic app!"]);
const Counter = () => {
const [count, setCount] = useState(0);
const increment = () => {
setCount(count + 1);
};
const decrement = () => {
setCount(count - 1);
};
return createElement("div", {}, [
createElement("button", { onClick: increment }, ["+"]),
createElement("h3", {}, [count]),
createElement("button", { onClick: decrement }, ["-"]),
])
}
const createApp = () => createElement(
"div",
{},
[
createElement(Title, {}, []),
createElement(Counter, {}, []),
createElement("p", {}, ["This was build with `createElement`"])
]
);
let oldVApp = null;
const renderApp = () => {
useStateCalls = -1;
const parent = document.getElementById("root");
// Step 4: Do not clear the parent. We'll performing the diff
// parent.innerHTML = "";
// Step 1: Pass `null`
// diff(null, createApp(), parent);
// Step 2: Store oldVApp and use it in diff
const currentApp = createApp();
diff(oldVApp, currentApp, parent);
oldVApp = currentApp;
};
renderApp();
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Document</title>
</head>
<body>
<div id="root"></div>
<script src="./app-final.js"></script> -->
</body>
</html>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment