A lightweight, React-inspired JavaScript framework built from scratch without using any existing libraries or frameworks. This framework implements core frontend concepts including Virtual DOM, component-based architecture, state management with hooks, and client-side routing. The project demonstrates a deep understanding of how modern JavaScript frameworks work under the hood.
- Features
- Architecture
- Getting Started
- Core Concepts
- API Reference
- TodoMVC Demo
- What I Learned
- Technologies
- Efficient Diffing Algorithm: Smart DOM reconciliation that updates only what changed
- Fragment Support: Render multiple elements without wrapper nodes
- Keyed Elements: Optimized list rendering with key-based reconciliation
- Text Node Optimization: Efficient handling of text content updates
useState: Manage component state with automatic re-renderinguseEffect: Handle side effects with dependency tracking and cleanupuseRef: Create mutable references that persist across renders
- Functional Components: Pure functions that return virtual DOM nodes
- Props System: Pass data and callbacks between components
- Conditional Rendering: Dynamic UI based on application state
- List Rendering: Efficiently render dynamic lists with proper key handling
- Hash-based Navigation: URL synchronization with application state
- Route Handling: Navigate between views without page reloads
- History Integration: Browser back/forward button support
- Minimal DOM Manipulation: Only update changed elements
- Batched Updates: Group state changes for efficient rendering
- Effect Scheduling: Run effects asynchronously to avoid blocking
The framework follows a unidirectional data flow pattern similar to React:
User Action → State Update → Virtual DOM Creation → Diffing → DOM Update
src/
├── core/
│ ├── vdom.js # Virtual DOM creation and rendering
│ ├── diffing.js # Reconciliation algorithm
│ └── hooks.js # State management hooks
└── index.js # Public API exports
import { render, createElement, useState } from './src/index.js';
function Counter() {
const [count, setCount] = useState(0);
return {
tag: 'div',
attrs: {},
children: [
{
tag: 'h1',
attrs: {},
children: [`Count: ${count}`]
},
{
tag: 'button',
attrs: {
onClick: () => setCount(count + 1)
},
children: ['Increment']
}
]
};
}
// Mount the app
render(Counter, document.getElementById('app'));# Navigate to the TodoMVC directory
cd todoMVC
# Open index.html in your browser
# Or use a local server (recommended)
python -m http.server 8000
# Navigate to http://localhost:8000The framework represents UI as JavaScript objects:
// HTML: <div class="container"><h1>Hello</h1></div>
// Virtual DOM representation:
{
tag: 'div',
attrs: { class: 'container' },
children: [
{
tag: 'h1',
attrs: {},
children: ['Hello']
}
]
}// Simple element
{
tag: 'button',
attrs: {
class: 'btn',
onClick: handleClick
},
children: ['Click me']
}
// Nested elements
{
tag: 'ul',
attrs: { class: 'list' },
children: [
{ tag: 'li', attrs: {}, children: ['Item 1'] },
{ tag: 'li', attrs: {}, children: ['Item 2'] }
]
}Events are attached using camelCase attribute names:
{
tag: 'input',
attrs: {
type: 'text',
onKeyDown: (e) => {
if (e.key === 'Enter') {
console.log('Enter pressed!');
}
},
onChange: (e) => console.log(e.target.value)
},
children: []
}Use JavaScript expressions to conditionally render elements:
function Component({ isLoggedIn }) {
return {
tag: 'div',
attrs: {},
children: [
isLoggedIn ? {
tag: 'p',
attrs: {},
children: ['Welcome back!']
} : {
tag: 'p',
attrs: {},
children: ['Please log in']
}
]
};
}For dynamic lists, use the key attribute for optimal performance:
function TodoList({ todos }) {
return {
tag: 'ul',
attrs: {},
children: todos.map(todo => ({
tag: 'li',
attrs: {
key: todo.id // Important for efficient updates
},
children: [todo.text]
}))
};
}Mounts the root component to a DOM element.
render(App, document.getElementById('root'));Parameters:
component: Function that returns a virtual DOM nodecontainer: DOM element to mount the app
Creates a stateful value that triggers re-renders when updated.
const [count, setCount] = useState(0);
// Update state
setCount(count + 1);
// Functional update
setCount(prevCount => prevCount + 1);Returns: Array with current state and setter function
Runs side effects after render. Cleanup functions are called before re-running or unmounting.
// Run on every render
useEffect(() => {
console.log('Component rendered');
});
// Run once on mount
useEffect(() => {
console.log('Component mounted');
}, []);
// Run when dependencies change
useEffect(() => {
console.log('Count changed:', count);
}, [count]);
// With cleanup
useEffect(() => {
const timer = setInterval(() => console.log('tick'), 1000);
return () => clearInterval(timer); // Cleanup
}, []);Parameters:
callback: Function to run (can return cleanup function)dependencies: Array of values to watch (optional)
Creates a mutable reference that persists across renders.
const inputRef = useRef(null);
// Access the ref
useEffect(() => {
inputRef.current.focus();
}, []);
return {
tag: 'input',
attrs: { ref: inputRef },
children: []
};Returns: Object with current property
Unmounts the current app and cleans up effects.
import { unmount } from './src/index.js';
unmount();The framework includes a fully functional TodoMVC implementation showcasing:
- ✅ Add, edit, and delete todos
- ✅ Mark todos as complete/incomplete
- ✅ Filter todos (All, Active, Completed)
- ✅ Clear completed todos
- ✅ Toggle all todos
- ✅ Persistent todo count
- ✅ URL routing with hash-based navigation
- ✅ Double-click to edit functionality
- ✅ Keyboard shortcuts (Enter to save)
todoMVC/
├── app.js # Main application logic
├── components/
│ ├── todoHeader.js # Input for new todos
│ ├── todoList.js # List of todos with items
│ └── todoFooter.js # Filter buttons and counters
├── index.html # Entry point
└── styles.css # TodoMVC styles
This project provided deep insights into frontend framework internals:
- Virtual DOM Implementation: Built a complete diffing algorithm that efficiently updates the real DOM
- Reconciliation: Implemented keyed reconciliation for optimal list rendering performance
- Component Lifecycle: Created a system for mounting, updating, and unmounting components
- Closures: Leveraged closures for hook state management and effect tracking
- Event Delegation: Implemented efficient event handling without memory leaks
- Algorithm Design: Developed diffing algorithms to minimize DOM operations
- Module System: Organized code with ES6 modules and clean separation of concerns
- Hook Implementation: Built useState, useEffect, and useRef from scratch
- Dependency Tracking: Implemented dependency arrays for effect optimization
- Batch Updates: Grouped state changes to prevent unnecessary re-renders
- Effect Cleanup: Managed side effect lifecycle and cleanup functions
- Performance Optimization: Minimized reflows and repaints through smart diffing
- Fragment Handling: Implemented fragment rendering for cleaner component trees
- Attribute Management: Built efficient attribute updating without full replacement
- Text Node Optimization: Optimized text content updates
- Hash-based Routing: Implemented client-side navigation with URL synchronization
- State-URL Sync: Kept application state in sync with browser URL
- History API: Integrated with browser history for back/forward navigation
- Inversion of Control: Understood the framework-vs-library distinction
- API Design: Created intuitive, React-like APIs for developers
- Documentation: Wrote comprehensive documentation for framework users
- Testing: Debugged complex asynchronous rendering and state updates
- Edge Cases: Handled conditional rendering, null values, and nested fragments
- Performance: Optimized rendering for large lists with proper key handling
- Memory Management: Prevented memory leaks in event handlers and effects
- Browser Compatibility: Ensured cross-browser DOM manipulation
- Language: Vanilla JavaScript (ES6+)
- Module System: ES6 Modules
- DOM API: Native browser APIs
- No External Dependencies: Built entirely from scratch
- Minimal DOM operations through smart reconciliation
- Support for keyed lists for optimal performance
- Fragment support for cleaner component trees
- Efficient text node updates
- Proper cleanup of removed elements
- State persistence across renders
- Effect dependency tracking
- Cleanup function support
- Multiple hooks per component
- Predictable execution order
- Hash-based URL routing
- State synchronization with URL
- Browser history integration
- Programmatic navigation support
This framework uses Virtual DOM with Diffing because:
- Performance: Only updates changed parts of the DOM
- Declarative: Developers describe what the UI should look like, not how to change it
- Predictable: Unidirectional data flow makes debugging easier
- Familiar: API similar to React makes it approachable
Alternative approaches considered:
- Data Binding: Two-way synchronization (Angular-style)
- Template Literals: String-based templating
- Direct DOM: Imperative manipulation
Virtual DOM offered the best balance of performance, developer experience, and maintainability.