Skip to content

Instantly share code, notes, and snippets.

@bradparker
Last active November 14, 2023 12:11
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save bradparker/97d4beb090ab37c80a422e57384a2386 to your computer and use it in GitHub Desktop.
Save bradparker/97d4beb090ab37c80a422e57384a2386 to your computer and use it in GitHub Desktop.
React?
import { useState, createElement as h, render, type Element } from "./index.js";
const Counters = () => {
const [count, setCount] = useState(0);
return h(
"div",
{ style: "display: flex; flex-direction: column; gap: 1rem" },
h(
"div",
{ style: "display: flex; gap: 1rem" },
h(
"button",
{
onClick() {
setCount(count - 1);
},
},
"-",
),
h(
"button",
{
onClick() {
setCount(count + 1);
},
},
"+",
),
),
h(
"ul",
{
style: `
list-style: none;
margin: 0;
padding: 0;
display: flex;
flex-direction: column;
gap: 1rem;
`,
},
...Array.from({ length: count }).map((_, i) =>
h("li", {}, h(Counter, {})),
),
),
);
};
const Counter = (props = {}) => {
const [count, setCount] = useState(0);
const style = `
font-family: sans-serif;
display: flex;
flex-flow: column;
gap: 1rem;
padding: 1rem;
border: solid thin lightgrey;
border-radius: 0.5rem;
`;
return h(
"div",
{ style },
h("p", { style: "margin: 0" }, "Count stuff"),
h(
"div",
{ style: "display: flex; gap: 1rem" },
h(
"button",
{
onClick() {
setCount(count - 1);
},
},
"-",
),
count.toString(),
h(
"button",
{
onClick() {
setCount(count + 1);
},
},
"+",
),
),
count > 0
? h(
"ul",
{},
...Array.from({ length: count }).map((_, i) =>
h("li", {}, (i + 1).toString()),
),
)
: h("p", { style: "margin: 0" }, "Nothing yet"),
);
};
const main = () => {
const root = document.getElementById("root");
if (!root) return;
render(
root,
h(
"div",
{ style: "display: flex; flex-flow: column; gap: 1rem; padding: 1rem;" },
h(Counters, {}),
),
);
};
main();
<!doctype html>
<html>
<head>
<title>Toy React</title>
</head>
<body>
<main id="root"></main>
<script src="./dist/app.js" type="module"></script>
</body>
</html>
type NodeName = string;
type Component<T> = (props: T) => string | Element;
type Type<T> = NodeName | Component<T>;
export type Element<T = any> = {
type: Type<T>;
props: T & { children: (string | Element)[] };
};
export function createElement<T>(
type: Type<T>,
props: T,
...children: (string | Element)[]
): Element<T> {
return {
type,
props: {
...props,
children,
},
};
}
type Ref<T> = { current: T };
const newRef = <T>(initialValue: T): Ref<T> => ({ current: initialValue });
type Instance<T = any, S = any> = {
element: string | Element<T>;
state: S;
node: null | Node;
listeners: { click?: EventListener };
parent: Node;
children: Instance[];
};
const currentReference = newRef<Instance | null>(null);
export function useState<T>(initial: T): [T, (next: T) => void] {
if (!currentReference?.current) throw new Error("No current reference!");
const { current } = currentReference;
return [
current.state || initial,
(newState) => {
requestIdleCallback(() => {
renderElement(current.parent, current.element, newState, current);
});
},
];
}
type EvaluatedElement = Element & {
type: string;
};
function evaluateElement<T>(element: string | Element<T>):
| string
| {
type: NodeName;
props: T & { children: (string | Element)[] };
} {
if (typeof element === "string") return element;
const { type, props } = element;
if (typeof type === "string") {
return { type, props };
} else {
return evaluateElement(type(props));
}
}
const getNode = (
existing: null | { node: Node; element: string | Element },
evaluatedElement: string | EvaluatedElement,
): Node => {
if (typeof evaluatedElement === "string") {
if (typeof existing?.element === "string" && existing?.node) {
return existing.node;
} else {
return document.createTextNode(evaluatedElement);
}
} else {
if (
typeof existing?.element !== "string" &&
existing?.element.type === evaluatedElement.type &&
existing?.node
) {
return existing.node;
} else {
return document.createElement(evaluatedElement.type);
}
}
};
function renderElement<T, S>(
parent: Node,
element: string | Element<T>,
state: null | S = null,
existing: null | Instance = null,
): Instance<T> {
const existingElement = existing?.element;
const existingListeners = existing?.listeners;
const existingNode = existing?.node;
const existingChildren = existing?.children;
const existingParent = existing?.parent;
const current: Instance = Object.assign(
existing || {
element,
state,
node: null,
listeners: {},
parent,
children: [],
},
{
parent,
element,
state,
children: [],
},
);
currentReference.current = current;
const evaluatedElement = evaluateElement(element);
const node = getNode(
existingElement && existingNode
? { element: existingElement, node: existingNode }
: null,
evaluatedElement,
);
current.node = node;
if (typeof evaluatedElement !== "string") {
const { children = [], ...props } = evaluatedElement.props;
if (node instanceof HTMLElement) {
Object.entries(props).forEach(([key, value]) => {
if (key === "onClick" && typeof value === "function") {
if (existingListeners?.click) {
node.removeEventListener("click", existingListeners.click);
}
node.addEventListener("click", value as EventListener);
current.listeners.click = value as EventListener;
}
if (typeof value === "string") {
node.setAttribute(key, value);
}
});
if (existing && typeof existing.element !== "string") {
Object.keys(existing.element.props).forEach((key) => {
if (key !== "children" && !(key in props)) {
node.removeAttribute(key);
}
});
}
}
children.forEach((child, index) => {
const existingChild = existingChildren?.[index];
const nextChild = renderElement(
node,
child,
existingChild?.state ?? null,
existingChild,
);
current.children.push(nextChild);
});
if (
existingChildren &&
node === existingNode &&
existingChildren.length > children.length
) {
existingChildren.slice(children.length).forEach((extraChild) => {
extraChild.node && node.removeChild(extraChild.node);
});
}
}
if (node === existingNode && typeof evaluatedElement === "string") {
node.nodeValue = evaluatedElement;
return current;
}
if (existingParent !== current.parent) {
current.parent.appendChild(node);
} else {
if (existingNode?.parentNode === null) {
parent.appendChild(node);
} else if (existingNode && existingNode !== node) {
parent.replaceChild(node, existingNode);
}
}
return current;
}
export function render<T>(root: HTMLElement, element: Element<T>): void {
while (root.lastChild) {
root.removeChild(root.lastChild);
}
renderElement(root, element);
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment