A small vanilla JavaScript framework with:
- A virtual DOM abstraction
- A tiny renderer + diff algorithm
- A global store (shared state)
- A hash-based router
- A simple event system using
onhandlers in virtual nodes
The goal is clarity over complexity.
The framework has five core parts:
-
Virtual DOM helpers (
src/virtual-dom-helpers.js)createElement(tagName, props, children)creates an element nodecreateFragment(children)groups nodes without adding extra DOM wrapperscreateTextNode(value)creates text nodes
-
Renderer (
src/mount-dom.js,src/diff-algorithm.js,src/destroy-dom.js)mountDOMcreates real DOM from virtual nodespatchDOMupdates existing DOM by comparing old/new virtual nodesdestroyDOMremoves nodes and event handlers
-
Event handling (
src/dom-events.js)- User code defines events in virtual node
props.on - Example:
on: { click() { ... } } - Framework attaches and removes handlers internally
- User code defines events in virtual node
-
State management (
src/state.js)createStore(initialState, reducers)creates global shared statedispatch(actionName, payload)updates state through reducerssubscribe(listener)lets app re-render on state changes
-
Routing (
src/router.js)createRouter({ defaultPath })manages URL hash routes (#/,#/active)navigate(path)changes routesubscribe(listener)notifies on route change
All main APIs are exported from src/framework.js:
import {
createElement,
createFragment,
createTextNode,
createStore,
createRouter,
createApp,
} from './src/framework.js';const titleNode = createElement('h1', { class: 'title' }, ['Hello']);
const buttonNode = createElement('button', {
class: 'primary',
on: {
click() {
console.log('Clicked');
},
},
}, ['Click me']);
const pageNode = createFragment([titleNode, buttonNode]);Events are defined in props.on.
createElement('input', {
value: 'text',
on: {
input(event) {
console.log(event.target.value);
},
keypress(event) {
if (event.key === 'Enter') {
console.log('Submit');
}
},
},
});You never call addEventListener directly in app UI code.
const initialState = { count: 0 };
const reducers = {
increment(state) {
return { ...state, count: state.count + 1 };
},
setCount(state, value) {
return { ...state, count: value };
},
};
const store = createStore(initialState, reducers);
store.subscribe((nextState, actionName) => {
console.log('Action:', actionName, 'State:', nextState);
});
store.dispatch('increment');
store.dispatch('setCount', 5);const router = createRouter({ defaultPath: '/' });
router.subscribe((path) => {
console.log('Route changed:', path);
});
router.start();
router.navigate('/about'); // URL becomes #/aboutRoutes are hash-based so it works with static hosting.
const app = createApp({
store,
router,
view({ state, route, emit, navigate }) {
return createElement('div', {}, [
createElement('h1', {}, [`Route: ${route}`]),
createElement('button', {
on: {
click() {
emit('increment');
},
},
}, ['Increment']),
createElement('button', {
on: {
click() {
navigate('/about');
},
},
}, ['Go to About']),
]);
},
});
router.start();
app.mount(document.getElementById('root'));The root todo-app.js includes a TodoMVC-style app that demonstrates:
- Element creation with virtual nodes
- Event handling via
on - Shared state with reducers
- Route-based filtering (
#/,#/active,#/completed)
Current structure is already small and clear. If the project grows, use this:
mini-framework/
src/
core/
virtual-dom-helpers.js
mount-dom.js
diff-algorithm.js
destroy-dom.js
dom-attributes.js
dom-events.js
state/
store.js
router/
router.js
framework.js
examples/
todo-app.js
index.html
style.css
todo-app.js
README.md- No external framework is used.
- Everything is vanilla JavaScript.
- Keep reducers pure and return new state objects.
- Keep view functions focused: read state/route and return virtual nodes.