Building a Custom React-Like Library
We’re going to build Miniact (think “mini React”). It won’t have all of React’s features, but it will have the core ones:
- JSX support (using
Miniact.createElement) - Component rendering (functions that return JSX)
- State management (
Miniact.useState) - Effect system (
Miniact.useEffect) - Virtual DOM (with diffing)
- Event handling
Let’s start with the foundation: the createElement function (which Babel compiles JSX to).
The createElement Function
When you write JSX like <div className="hello">World</div>, Babel compiles it to:
Miniact.createElement('div', { className: 'hello' }, 'World');
The createElement function creates a virtual DOM node. It’s a simple data structure:
// The createElement function
function createElement(type, props, ...children) {
return {
type,
props: props || {},
children: children.map(child =>
typeof child === 'string' ? createTextElement(child) : child
)
};
}
// Helper: create a text element
function createTextElement(text) {
return {
type: 'TEXT_ELEMENT',
props: { nodeValue: text },
children: []
};
}
This is it. This is the foundation of your entire library. Every JSX element becomes a call to createElement, which returns a plain JavaScript object (the virtual DOM node).
Let’s verify this works:
// Test our createElement
const element = createElement(
'div',
{ className: 'container' },
createElement('h1', {}, 'Hello, Miniact!'),
createElement('p', {}, 'This is a paragraph.')
);
console.log(JSON.stringify(element, null, 2));
Output:
{
"type": "div",
"props": { "className": "container" },
"children": [
{
"type": "h1",
"props": {},
"children": [
{ "type": "TEXT_ELEMENT", "props": { "nodeValue": "Hello, Miniact!" }, "children": [] }
]
},
{
"type": "p",
"props": {},
"children": [
{ "type": "TEXT_ELEMENT", "props": { "nodeValue": "This is a paragraph." }, "children": [] }
]
}
]
}
This is your virtual DOM tree. Now you need to render it to real DOM.
Rendering to Real DOM
The render function takes a virtual DOM node and creates the corresponding real DOM node:
// Render a virtual DOM node to real DOM
function render(virtualNode, container) {
// Case 1: Text node
if (virtualNode.type === 'TEXT_ELEMENT') {
const textNode = document.createTextNode(
virtualNode.props.nodeValue
);
container.appendChild(textNode);
return textNode;
}
// Case 2: Regular element
const domNode = document.createElement(virtualNode.type);
// Set props (simplified - doesn't handle events, etc.)
updateProps(domNode, {}, virtualNode.props);
// Recursively render children
virtualNode.children.forEach(child => {
render(child, domNode);
});
// Append to container
container.appendChild(domNode);
return domNode;
}
// Update props on a DOM node
function updateProps(domNode, oldProps, newProps) {
// Remove old props
Object.keys(oldProps).forEach(propName => {
if (!(propName in newProps)) {
setProp(domNode, propName, null, oldProps[propName]);
}
});
// Set new/changed props
Object.keys(newProps).forEach(propName => {
if (oldProps[propName] !== newProps[propName]) {
setProp(domNode, propName, newProps[propName], oldProps[propName]);
}
});
}
// Set a single prop on a DOM node
function setProp(domNode, propName, newValue, oldValue) {
// Handle event handlers (onClick, etc.)
if (propName.startsWith('on')) {
const eventName = propName.toLowerCase().substring(2);
if (oldValue) {
domNode.removeEventListener(eventName, oldValue);
}
if (newValue) {
domNode.addEventListener(eventName, newValue);
}
return;
}
// Handle style
if (propName === 'style') {
if (newValue) {
Object.assign(domNode.style, newValue);
} else {
domNode.style = null;
}
return;
}
// Handle children (special case)
if (propName === 'children') {
return; // Handled separately
}
// Handle all other props
if (newValue == null) {
domNode.removeAttribute(propName);
} else {
domNode.setAttribute(propName, newValue);
}
}
This is a working (though simplified) rendering function. It handles:
- Text nodes
- Regular elements
- Props (including event handlers and style)
But it has a major limitation: it can only do initial renders. It can’t update the DOM when state changes. For that, we need the virtual DOM diffing algorithm.
We’ll implement that in Section 2. First, let’s add component support.
Component Support
In React, a component is a function that returns JSX (or a class with a render method). Our library should support function components.
The key insight: when the type of a virtual DOM node is a function (not a string like 'div'), we should call the function to get its virtual DOM representation.
// Render (updated to support components)
function render(virtualNode, container) {
// NEW: Check if type is a function (component)
if (typeof virtualNode.type === 'function') {
// Call the component function to get its virtual DOM
const componentVirtualNode = virtualNode.type(virtualNode.props);
// Recursively render the result
return render(componentVirtualNode, container);
}
// ... rest of the function (text nodes, regular elements) ...
}
Let’s test this:
// A component
function App(props) {
return createElement(
'div',
{ className: 'app' },
createElement('h1', {}, `Hello, ${props.name}!`),
createElement('p', {}, 'Welcome to Miniact.')
);
}
// Render the component
const appVirtualNode = createElement(App, { name: 'Alice' });
render(appVirtualNode, document.getElementById('root'));
This works! When render encounters a virtual node with type: App (a function), it calls App(props) to get the virtual DOM, then renders that.
But we’re still missing state management. Components need to be able to re-render when state changes. For that, we need our own version of useState.
State Management (Miniact.useState)
The useState hook is React’s primary state management tool. Let’s implement a simplified version.
The core idea: each component can have state, and when state changes, the component re-renders.
We need:
- A way to store state for each component
- A way to update state and trigger re-renders
Here’s a simplified implementation:
// Global state (simplified - in reality, you'd use a fiber-like structure)
let currentComponent = null;
let hookIndex = 0;
const hooks = [];
// The useState hook
function useState(initialValue) {
const index = hookIndex;
// Initialize hook if first render
if (!hooks[index]) {
hooks[index] = initialValue;
}
// The setState function
const setState = (newValue) => {
hooks[index] = newValue;
// Trigger re-render (simplified - just re-render everything)
rerender();
};
hookIndex++; // Move to next hook
return [hooks[index], setState];
}
// Render a component (tracking current component for hooks)
function renderComponent(Component, props) {
currentComponent = Component;
hookIndex = 0; // Reset hook index for this component
// Call the component function
const virtualNode = Component(props);
currentComponent = null;
return virtualNode;
}
This is extremely simplified. Real React uses a fiber linked list to track state per component, and it batches state updates for performance. But the core idea is here: hooks are stored in an array, indexed by call order.
Let’s see it in action:
// Counter component using our useState
function Counter() {
const [count, setCount] = useState(0);
return createElement(
'div',
{},
createElement('p', {}, `Count: ${count}`),
createElement('button', { onClick: () => setCount(count + 1) }, 'Increment')
);
}
When the button is clicked, setCount is called, which updates the hook state and triggers a re-render.
But we’re still re-rendering the entire app on every state change. That’s inefficient. What we need is the virtual DOM diffing algorithm—which we’ll implement in Section 2.
Bringing It All Together
Let’s create the Miniact object that exposes our API:
const Miniact = {
createElement,
render,
useState,
// ... other exports ...
};
// Configure Babel to use Miniact.createElement
// .babelrc:
// {
// "plugins": [
// ["@babel/plugin-transform-react-jsx", { "pragma": "Miniact.createElement" }]
// ]
// }
export default Miniact;
Now you can use JSX with your library:
import Miniact from './miniact';
function App() {
const [count, setCount] = Miniact.useState(0);
return (
<div className="app">
<h1>Count: {count}</h1>
<button onClick={() => setCount(count + 1)}>Increment</button>
</div>
);
}
Miniact.render(<App />, document.getElementById('root'));
This is a working (though very simplified) React-like library. In the next section, we’ll implement the virtual DOM diffing algorithm to make updates efficient.
Miniact Architecture
flowchart TD
A[JSX Code] -->|Babel compiles to| B[createElement calls]
B --> C[Virtual DOM Tree]
C --> D[Render to Real DOM]
D --> E[Browser Display]
E --> F[User Interaction]
F --> G[setState Called]
G --> H[Re-render Component]
H --> I[New Virtual DOM Tree]
I --> J[Diff with Old Tree]
J --> K[Update Only Changed Nodes]
K --> E
style A fill:#e1f5fe
style C fill:#c8e6c9
style J fill:#fff9c4This diagram shows the architecture of our Miniact library. The key insight: we don’t re-render the entire DOM on every state change—we diff the old and new virtual DOM trees, and update only what changed.



