Last active
August 22, 2023 13:57
-
-
Save lmuntaner/06c9f2d45a1d208f989a8014f0feea27 to your computer and use it in GitHub Desktop.
Building a React-Like Library - Part III: useState
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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(); |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
<!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