@zahidala/mini-framework
v1.0.24
Published
A minimal framework inspired by React for building web apps with a virtual DOM, state management, routing, and rendering.
Downloads
71
Readme
@zahidala/mini-framework
A minimal framework inspired by React for building web apps with a virtual DOM, state management, routing, and rendering.
📦 Installation
npm install @zahidala/mini-framework
# or
yarn add @zahidala/mini-frameworkOr scaffold a new project with:
npx @zahidala/mini-framework🚀 Getting Started
After installing or creating a new project:
npm run devThis will start a local development server (using Parcel) so you can begin building your app.
✅ Example Usage
// main.ts
import { h, render } from "@zahidala/mini-framework";
const App = ({ getState, setState }) => {
const { count } = getState();
return h(
"div",
{},
h("h1", {}, `Count: ${count}`),
h("button", { onclick: () => setState({ count: count + 1 }) }, "Increment")
);
};
const root = document.getElementById("app");
if (root) render(App, { count: 0 }, root);🔧 API Reference
h(tag, props, ...children)
Used to create an element. Returns a virtual DOM node.
h("div", { id: "main" }, ["Hello"]);Parameters:
tag:string | Function
HTML tag name (e.g."div") or a functional component.props:object
Attributes, properties, or event handlers for the element.childrenArray<VNode | string | number | boolean | null | undefined> | VNode | string | number | boolean | null | undefined
Nestedh(...)nodes, strings, numbers, booleans, null, or undefined. You can pass a single child or an array of children.
Example:
const MyComponent = () => h("span", {}, ["Hi!"]);
const vnode = h("div", { class: "box" }, ["Hello", MyComponent()]);render(component, initialState, root, router?)
Renders a reactive component into a DOM root element. Ensure to add this at the end of your index ts file for the component to render.
type Component<T> = (state: {
getState: () => T;
setState: (value: T) => void;
}) => VNode;
render<T>(
component: Component<T>,
initialState: T,
root: HTMLElement,
router?: ReturnType<typeof createRouter>
): void;Parameters:
component: A function that takes getState and setState, and returns a virtual DOM (VNode).initialState: The initial state of the component. An empty object can be passed if state is not required.root: The HTML element to mount the app into.router (optional): A router instance created via createRouter(). Enables re-rendering on route change.
Behavior:
Initializes and mounts the virtual DOM.
Subscribes to state changes and applies DOM diffs.
If a router is provided, listens for navigation updates and re-renders accordingly.
Example:
import { createRouter, h, render } from "@zahidala/mini-framework";
const router = createRouter();
function App({}) {
return h("span", {}, ["Hello World"]);
}
const root = document.getElementById("app");
if (root) render(App, {}, root, router);createState<T>(initialValue: T)
Creates a simple reactive state container with getState, setState, and subscribe.
Note: You typically don't need to call
createStatemanually — it's automatically handled by therenderfunction when rendering a component.
const state = createState(0);
state.subscribe((value) => {
console.log("State changed to:", value);
});
state.setState(42); // Logs: State changed to: 42
const current = state.getState(); // 42Parameters:
initialValue: T— The starting state value.
Returns
An object with:
getState(): T: Returns the current state.setState(newValue: T): void: Updates the state and notifies all subscribers (if the value changes).subscribe(listener: (state: T) => void): () => void: Subscribes to state changes (listener is also called immediately once). Returns an unsubscribe function.
createRouter()
Creates a basic client-side router using the History API and supports subscribing to route changes.
This is a simple routing utility for SPAs (Single Page Applications). It does not handle hash-based routing or route matching — it focuses purely on tracking and reacting to path changes.
import { createRouter, h, render } from "@zahidala/mini-framework";
// Create the router instance
const router = createRouter();
// Simple routes-to-components mapping
function App({
getState,
setState,
}: {
getState: () => {};
setState: (v: {}) => void;
}) {
const path = router.getRoute();
return h("main", { class: "container" }, [
h("nav", {}, [
h("a", { href: "/", onclick: handleLinkClick("/") }, ["Home"]),
" | ",
h("a", { href: "/about", onclick: handleLinkClick("/about") }, ["About"]),
]),
h("hr", {}, []),
path === "/about"
? h("section", {}, ["This is the About page."])
: h("section", {}, ["Welcome to the Home page!"]),
]);
}
// Helper to intercept link clicks and update route
function handleLinkClick(path: string) {
return (e: Event) => {
e.preventDefault();
router.setRoute(path);
};
}
// Mount to DOM
const root = document.getElementById("app");
if (root) render(App, {}, root, router);Returns
An object with the following methods:
getRoute(): string: Returns the current full path (pathname + search).getSearchParams(): URLSearchParams: Returns a URLSearchParams object based on the current query string.setRoute(path: string): void: Pushes a new route to the browser history and notifies subscribers.subscribe(listener: (path: string) => void): () => void: Subscribes to route changes. The listener is called immediately with the current path and on each subsequent change. Returns an unsubscribe function.
Why Things Work the Way They Work
render() and App State — How It Works & Why
This mini-framework is built around a simple but powerful idea:
A UI is just a function of state and routing, rendered into the DOM using a virtual DOM diff.
Instead of complex abstractions, we intentionally keep the model flat, reactive, and easy to understand.
✅ One Function to Render It All
The render(component, initialState, root, router?) function does three things:
Initial render – Turns your component into virtual DOM, builds real DOM from it, and inserts it into the page.
Reactivity – Whenever you update state using setState(...), it re-runs your component and updates only the changed parts of the DOM.
Routing (optional) – If a router is passed, the UI also re-renders on route changes.
💡 Why One Big State Object?
Instead of using isolated, per-component state (like React’s useState), we keep a single global state object.
Why this design?
🧠 Mental simplicity: There’s only one source of truth —
getState(). You don’t have to chase local state in nested components.🧪 Predictable updates: You can always inspect the full app state. Great for debugging or adding dev tools.
🧩 Functional rendering: The component function becomes a pure-ish transformation of state → UI. It’s testable and side-effect free.
🔄 Easy re-renders: When state or route changes, the component is re-run with fresh state, and only the changed DOM parts are updated.
Think of it like:
UI = render(state, path)
✍️ Example: Full-State Updates
function Counter(state: {
getState: () => { count: number };
setState: (newState: { count: number }) => void;
}) {
const { count } = state.getState();
return h("div", {}, [
h("p", {}, [`Count is ${count}`]),
h("button", { onClick: () => state.setState({ count: count + 1 }) }, [
"Increment",
]),
]);
}
render(Counter, { count: 0 }, document.getElementById("app")!);🔁 Why Not Partial setState?
We don’t support merging partial state like React's old this.setState(). Why?
It keeps state updates explicit — every update is a full replacement.
Encourages immutable patterns — which are easier to diff and reason about.
Less internal complexity — no diffing or merging of nested structures.
Of course, helpers like updateState(draft => { draft.count++ }) could be added for convenience later but the core stays simple.
📦 Summary
State is a single object — updated completely via
setState(newState)Your app is a pure function of state and routing
Renders are triggered by:
setStateRoute changes (if router is passed)
It's like React, but flatter, simpler, and easier to reason about
🔧 How the Virtual DOM Works (And Why)
The VDOM is a lightweight, tree-like structure that mirrors the real DOM. Instead of modifying the DOM directly (which is slow and error-prone), we:
Build a virtual representation of what the UI should look like
Compare it with the previous version
Apply minimal updates to the real DOM to match the new version
This strategy is known as diffing and allows us to avoid full DOM re-renders.
🏗 What is a VNode?
A VNode (Virtual Node) looks like this:
{
tag: "div",
props: { id: "header", class: "dark" },
children: [
{ tag: "h1", props: {}, children: ["Hello"] },
"Just a text node"
]
}We define VNode using a function called h():
const v = h("div", { class: "box" }, [
h("p", {}, ["Text content"]),
h("button", { onclick: handleClick }, ["Click me"]),
]);This is inspired by JSX but works with plain JavaScript.
⚙️ createElement(): Building the Real DOM
The createElement() function turns a VNode into a real DOM element, recursively:
It creates the correct tag
Assigns props like id, class, or style
Attaches event listeners (delegated)
Appends all child nodes
This is used during initial rendering.
🔁 updateElement(): Smart DOM Updating
On state or route changes, the UI is re-rendered virtually. We don’t replace the entire DOM — we diff and patch:
If text changes, we replace the text node
If tag changes (
div→span), we replace the elementIf props or styles change, we update them
If child order changes (especially with keys), we rearrange the DOM
This is what makes your updates fast.
You can think of this like a simplified React
reconciliationengine.
🪝 Event Delegation: Why and How
Instead of attaching event listeners to each node directly (which is slow), we use delegated event handling:
All listeners are attached to the root (once per event type)
We register event handlers by assigning unique data-event-id-* attributes
When an event bubbles up, we match it and call the correct handler
This means fewer listeners, less memory use, and easy updates.
🗝️ Keys: Efficient Child Updates
When rendering lists, you should always give items a key prop:
items.map((item) => h("li", { key: item.id }, [item.name]));This helps the framework know which DOM node maps to which data — avoiding full re-renders when order changes.
🧠 Why This VDOM Design?
Simple: No class-based components, no lifecycle headaches
Fast enough: Minimal DOM touches, smart diffs
Transparent: You can follow every update manually
Hackable: Easy to extend, since the logic is clean and unabstracted
🌐 Routing System
This framework includes a minimal, client-side router to manage page navigation and keep the UI in sync with the browser URL — without reloading the page.
✅ Why Not a Full Router?
This router avoids hash-based routing and heavy abstractions. Instead, it:
Uses the browser’s native
pushStateandpopstateKeeps the current path in internal state
Notifies subscribers of route changes (reactive pattern)
Supports search param parsing
No separate "router state" is needed — it's all handled inside a singleton returned by createRouter().
🧱 createRouter(): What It Does
const router = createRouter();This function sets up the routing logic and returns methods to control or react to navigation.
🧭 router.getRoute()
Returns the current full path (/path?query=1).
const path = router.getRoute(); // "/about?section=team"🔎 router.getSearchParams()
Returns a URLSearchParams object for reading query parameters:
const params = router.getSearchParams();
const userId = params.get("user");This is useful for parsing things like ?page=2 or ?tab=profile.
🚀 router.setRoute(path: string)
Changes the URL without reloading the page, and notifies all subscribers.
router.setRoute("/home"); // updates the URL and triggers all listenersIf the path is already the current one, it does nothing.
📡 router.subscribe(callback: (path: string) => void)
Lets you listen for route changes, including manual back/forward navigation.
const unsubscribe = router.subscribe((path) => {
console.log("Route changed to", path);
});Immediately calls the listener with the current path
Returns an
unsubscribe()function
This is how your app’s rendering stays in sync with the URL.
📜 History + popstate Support
The router also listens for the browser's native popstate event:
window.addEventListener("popstate", ...)This ensures Back and Forward buttons correctly trigger route updates.
🧠 Design Philosophy
This router intentionally avoids:
Complex route matching (like
"/posts/:id")Middleware, guards, loaders, etc.
Instead, it gives you direct control over routing — minimal but functional.
This approach works well for:
Small- to medium-scale SPAs
Apps that want full control over their routes
Developers who value simplicity and explicit behavior
