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:8000
The 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.