# SigPro 🚀 A minimalist reactive library for building web interfaces with signals, effects, and native web components. No compilation, no virtual DOM, just pure JavaScript and intelligent reactivity. [![npm version](https://img.shields.io/npm/v/sigpro.svg)](https://www.npmjs.com/package/sigpro) [![bundle size](https://img.shields.io/bundlephobia/minzip/sigpro)](https://bundlephobia.com/package/sigpro) [![license](https://img.shields.io/npm/l/sigpro)](https://github.com/yourusername/sigpro/blob/main/LICENSE) ## ❓ Why? After years of building applications with React, Vue, and Svelte—investing countless hours mastering their unique mental models, build tools, and update cycles—I kept circling back to the same realization: no matter how sophisticated the framework, it all eventually compiles down to HTML, CSS, and vanilla JavaScript. The web platform has evolved tremendously, yet many libraries continue to reinvent the wheel, creating parallel universes with their own rules, their own syntaxes, and their own steep learning curves. **SigPro is my answer to a simple question:** Why fight the platform when we can embrace it? Modern browsers now offer powerful primitives—Custom Elements, Shadow DOM, CSS custom properties, and microtask queues—that make true reactivity possible without virtual DOM diffing, without compilers, and without lock-in. SigPro strips away the complexity, delivering a reactive programming model that feels familiar but stays remarkably close to vanilla JS. No JSX transformations, no template compilers, no proprietary syntax to learn—just functions, signals, and template literals that work exactly as you'd expect. What emerged is a library that proves we've reached a turning point: the web is finally mature enough that we don't need to abstract it anymore. We can build reactive, component-based applications using virtually pure JavaScript, leveraging the platform's latest advances instead of working against them. SigPro isn't just another framework—it's a return to fundamentals, showing that the dream of simple, powerful reactivity is now achievable with the tools browsers give us out of the box. ## 📦 Installation Copy sigpro.js where you want to use it. ## 🎯 Philosophy SigPro (Signal Professional) embraces the web platform. Built on top of Custom Elements and reactive proxies, it offers a development experience similar to modern frameworks but with a minimal footprint and zero dependencies. **Core Principles:** - 📡 **True Reactivity** - Automatic dependency tracking, no manual subscriptions - ⚡ **Surgical Updates** - Only the exact nodes that depend on changed values are updated - 🧩 **Web Standards** - Built on Custom Elements, no custom rendering engine - 🎨 **Intuitive API** - Learn once, use everywhere - 🔬 **Predictable** - No magic, just signals and effects ## 📚 API Reference --- ### `$(initialValue)` - Signals Creates a reactive value that notifies dependents when changed. #### Basic Signal (Getter/Setter) ```typescript import { $ } from 'sigpro'; // Create a signal const count = $(0); // Read value (outside reactive context) console.log(count()); // 0 // Write value count(5); count(prev => prev + 1); // Use function for previous value // Read with dependency tracking (inside effect) $$(() => { console.log(count()); // Will be registered as dependency }); ``` #### Computed Signal ```typescript import { $, $$ } from 'sigpro'; const firstName = $('John'); const lastName = $('Doe'); // Computed signal - automatically updates when dependencies change const fullName = $(() => `${firstName()} ${lastName()}`); console.log(fullName()); // "John Doe" firstName('Jane'); console.log(fullName()); // "Jane Doe" // Computed signals cache until dependencies change const expensiveComputation = $(() => { console.log('Computing...'); return firstName().length + lastName().length; }); console.log(expensiveComputation()); // "Computing..." 7 console.log(expensiveComputation()); // 7 (cached, no log) ``` #### Signal with Custom Equality ```typescript import { $ } from 'sigpro'; const user = $({ id: 1, name: 'John' }); // Signals use Object.is comparison user({ id: 1, name: 'John' }); // Won't trigger updates (same values, new object) user({ id: 1, name: 'Jane' }); // Will trigger updates ``` **Parameters:** - `initialValue`: Initial value or getter function for computed signal **Returns:** Function that acts as getter/setter with the following signature: ```typescript type Signal = { (): T; // Getter (value: T | ((prev: T) => T)): void; // Setter } ``` --- ### `$$(effect)` - Effects Executes a function and automatically re-runs it when its dependencies change. #### Basic Effect ```typescript import { $, $$ } from 'sigpro'; const count = $(0); const name = $('World'); // Effect runs immediately and on dependency changes $$(() => { console.log(`Count is: ${count()}`); // Only depends on count }); // Log: "Count is: 0" count(1); // Log: "Count is: 1" name('Universe'); // No log (name is not a dependency) ``` #### Effect with Cleanup ```typescript import { $, $$ } from 'sigpro'; const userId = $(1); $$(() => { const id = userId(); let isSubscribed = true; // Simulate API subscription const subscription = api.subscribe(id, (data) => { if (isSubscribed) { console.log('New data:', data); } }); // Return cleanup function return () => { isSubscribed = false; subscription.unsubscribe(); }; }); userId(2); // Previous subscription cleaned up, new one created ``` #### Nested Effects ```typescript import { $, $$ } from 'sigpro'; const show = $(true); const count = $(0); $$(() => { if (!show()) return; // This effect is nested inside the conditional // It will only be active when show() is true $$(() => { console.log('Count changed:', count()); }); }); show(false); // Inner effect is automatically cleaned up count(1); // No log (inner effect not active) show(true); // Inner effect recreated, logs "Count changed: 1" ``` #### Manual Effect Control ```typescript import { $, $$ } from 'sigpro'; const count = $(0); // Stop effect manually const stop = $$(() => { console.log('Effect running:', count()); }); count(1); // Log: "Effect running: 1" stop(); count(2); // No log ``` **Parameters:** - `effect`: Function to execute. Can return a cleanup function **Returns:** Function to stop the effect --- ### `html` - Template Literal Tag Creates reactive DOM fragments using template literals with intelligent binding. #### Basic Usage ```typescript import { $, html } from 'sigpro'; const count = $(0); const name = $('World'); const fragment = html`

Hello ${name}

Count: ${count}

`; document.body.appendChild(fragment); ``` #### Directive Reference ##### `@event` - Event Listeners ```typescript import { html } from 'sigpro'; const handleClick = (event) => console.log('Clicked!', event); const handleInput = (value) => console.log('Input:', value); html` console.log(e.target.value)} /> ` ``` ##### `:property` - Two-way Binding Automatically syncs between signal and DOM element. ```typescript import { $, html } from 'sigpro'; const text = $(''); const checked = $(false); const selected = $('option1'); html`

You typed: ${text}

Checkbox is: ${() => checked() ? 'checked' : 'unchecked'}

` ``` ##### `?attribute` - Boolean Attributes ```typescript import { $, html } from 'sigpro'; const isDisabled = $(true); const isChecked = $(false); const hasError = $(false); html`
!hasError()} class="error"> An error occurred
` ``` ##### `.property` - Property Binding Directly binds to DOM properties, not attributes. ```typescript import { $, html } from 'sigpro'; const scrollTop = $(0); const user = $({ name: 'John', age: 30 }); const items = $([1, 2, 3]); html`
Content...
` ``` ##### Regular Attributes ```typescript import { $, html } from 'sigpro'; const className = $('big red'); const href = $('#section'); const style = $('color: blue'); // Static attributes html`
` // Dynamic attributes (non-directive) html`
` // Mix of static and dynamic html`Link` // Reactive attributes update when signal changes $$(() => { // The attribute updates automatically console.log('Class changed:', className()); }); ``` #### Conditional Rendering ```typescript import { $, html } from 'sigpro'; const show = $(true); const user = $({ name: 'John', role: 'admin' }); // Using ternary html` ${() => show() ? html`
Content is visible
` : html`
Content is hidden
`} ` // Using logical AND html` ${() => user().role === 'admin' && html` `} ` // Complex conditions html` ${() => { if (!show()) return null; if (user().role === 'admin') { return html`
Admin view
`; } return html`
User view
`; }} ` ``` #### List Rendering ```typescript import { $, html } from 'sigpro'; const items = $([1, 2, 3, 4, 5]); const todos = $([ { text: 'Learn SigPro', done: true }, { text: 'Build an app', done: false } ]); // Basic list html` ` // List with keys (for efficient updates) html` ` // Nested lists const matrix = $([[1, 2], [3, 4], [5, 6]]); html` ${() => matrix().map(row => html` ${() => row.map(cell => html` `)} `)}
${cell}
` ``` #### Dynamic Tag Names ```typescript import { $, html } from 'sigpro'; const tagName = $('h1'); const level = $(1); html`
This will be wrapped in ${tagName} tags
${() => { const Tag = `h${level()}`; return html` <${Tag}>Level ${level()} Heading `; }} ` ``` #### Template Composition ```typescript import { $, html } from 'sigpro'; const Header = () => html`
Header
`; const Footer = () => html``; const Layout = ({ children }) => html` ${Header()}
${children}
${Footer()} ` const Page = () => html` ${Layout({ children: html`

Page Content

Some content here

` })} ` ``` --- ### `$component(tag, setup, observedAttributes)` - Web Components Creates Custom Elements with automatic reactive properties. #### Basic Component ```typescript import { $, $component, html } from 'sigpro'; $component('my-counter', (props, context) => { // props contains signals for each observed attribute // context provides component utilities const increment = () => { props.value(v => v + 1); }; return html`

Count: ${props.value}

`; }, ['value']); // Observed attributes ``` Usage: ```html Additional content ``` #### Component with Complex Props ```typescript import { $, $component, html } from 'sigpro'; $component('user-profile', (props, context) => { // Transform string attributes to appropriate types const user = $(() => ({ id: parseInt(props.id()), name: props.name(), age: parseInt(props.age()), active: props.active() === 'true' })); return html`

${user().name}

ID: ${user().id}

Age: ${user().age}

Status: ${() => user().active ? 'Active' : 'Inactive'}

`; }, ['id', 'name', 'age', 'active']); ``` #### Component Lifecycle & Context ```typescript import { $, $component, html } from 'sigpro'; $component('lifecycle-demo', (props, { select, // Query selector scoped to component selectAll, // Query selector all scoped to component slot, // Access slots emit, // Dispatch custom events host, // Reference to the host element onMount, // Register mount callback onUnmount, // Register unmount callback onAttribute, // Listen to attribute changes getAttribute, // Get raw attribute value setAttribute, // Set raw attribute value }) => { // Access slots const defaultSlot = slot(); // Unnamed slot const headerSlot = slot('header'); // Named slot // Query internal elements const button = select('button'); const allSpans = selectAll('span'); // Lifecycle hooks onMount(() => { console.log('Component mounted'); // Access DOM after mount button?.classList.add('mounted'); }); onUnmount(() => { console.log('Component unmounting'); // Cleanup resources }); // Listen to specific attribute changes onAttribute('value', (newValue, oldValue) => { console.log(`Value changed from ${oldValue} to ${newValue}`); }); // Emit custom events const handleClick = () => { emit('button-click', { timestamp: Date.now() }); emit('value-change', props.value(), { bubbles: true }); }; // Access host directly host.style.display = 'block'; return html`
${headerSlot} ${defaultSlot}
`; }, ['value']); ``` #### Component with Methods ```typescript import { $, $component, html } from 'sigpro'; $component('timer-widget', (props, { host }) => { const seconds = $(0); let intervalId; // Expose methods to the host element Object.assign(host, { start() { if (intervalId) return; intervalId = setInterval(() => { seconds(s => s + 1); }, 1000); }, stop() { clearInterval(intervalId); intervalId = null; }, reset() { seconds(0); }, get currentTime() { return seconds(); } }); return html`

${seconds} seconds

`; }, []); ``` Usage: ```html ``` #### Component Inheritance ```typescript import { $, $component, html } from 'sigpro'; // Base component $component('base-button', (props, { slot }) => { return html` `; }, ['disabled']); // Extended component $component('primary-button', (props, context) => { // Reuse base component return html` ${context.slot()} `; }, ['disabled']); ``` --- ### `$router(routes)` - Router Hash-based router for SPAs with reactive integration. #### Basic Routing ```typescript import { $router, html } from 'sigpro'; const router = $router([ { path: '/', component: () => html`

Home Page

About ` }, { path: '/about', component: () => html`

About Page

Home ` } ]); document.body.appendChild(router); ``` #### Route Parameters ```typescript import { $router, html } from 'sigpro'; const router = $router([ { path: '/user/:id', component: (params) => html`

User Profile

User ID: ${params.id}

Edit ` }, { path: '/user/:id/posts/:postId', component: (params) => html`

Post ${params.postId} by User ${params.id}

` }, { path: /^\/product\/(?\w+)\/(?\d+)$/, component: (params) => html`

Product ${params.id} in ${params.category}

` } ]); ``` #### Nested Routes ```typescript import { $router, html, $ } from 'sigpro'; const router = $router([ { path: '/', component: () => html`

Home

` }, { path: '/dashboard', component: () => { // Nested router const subRouter = $router([ { path: '/', component: () => html`

Dashboard Home

` }, { path: '/settings', component: () => html`

Dashboard Settings

` }, { path: '/profile/:id', component: (params) => html`

Profile ${params.id}

` } ]); return html`

Dashboard

${subRouter}
`; } } ]); ``` #### Route Guards ```typescript import { $router, html, $ } from 'sigpro'; const isAuthenticated = $(false); const requireAuth = (component) => (params) => { if (!isAuthenticated()) { $router.go('/login'); return null; } return component(params); }; const router = $router([ { path: '/', component: () => html`

Public Home

` }, { path: '/dashboard', component: requireAuth((params) => html`

Protected Dashboard

`) }, { path: '/login', component: () => html`

Login

` } ]); ``` #### Navigation ```typescript import { $router } from 'sigpro'; // Navigate to path $router.go('/user/42'); // Navigate with replace $router.go('/dashboard', { replace: true }); // Go back $router.back(); // Go forward $router.forward(); // Get current path const currentPath = $router.getCurrentPath(); // Listen to navigation $router.listen((path, oldPath) => { console.log(`Navigated from ${oldPath} to ${path}`); }); ``` #### Route Transitions ```typescript import { $router, html, $$ } from 'sigpro'; const router = $router([ { path: '/', component: () => html`
Home
` }, { path: '/about', component: () => html`
About
` } ]); // Add transitions $$(() => { const currentPath = router.getCurrentPath(); const pages = document.querySelectorAll('.page'); pages.forEach(page => { page.style.opacity = '0'; page.style.transition = 'opacity 0.3s'; setTimeout(() => { page.style.opacity = '1'; }, 50); }); }); ``` --- ## 🎮 Complete Examples ### Real-time Todo Application ```typescript import { $, $$, html, $component } from 'sigpro'; // Styles const styles = html` `; $component('todo-app', () => { // State const todos = $(() => { const saved = localStorage.getItem('todos'); return saved ? JSON.parse(saved) : []; }); const newTodo = $(''); const filter = $('all'); // 'all', 'active', 'completed' const editingId = $(null); const editText = $(''); // Save to localStorage on changes $$(() => { localStorage.setItem('todos', JSON.stringify(todos())); }); // Filtered todos const filteredTodos = $(() => { const currentFilter = filter(); const allTodos = todos(); switch (currentFilter) { case 'active': return allTodos.filter(t => !t.completed); case 'completed': return allTodos.filter(t => t.completed); default: return allTodos; } }); // Stats const stats = $(() => { const all = todos(); return { total: all.length, completed: all.filter(t => t.completed).length, active: all.filter(t => !t.completed).length }; }); // Actions const addTodo = () => { const text = newTodo().trim(); if (!text) return; todos([ ...todos(), { id: Date.now(), text, completed: false, createdAt: new Date().toISOString() } ]); newTodo(''); }; const toggleTodo = (id) => { todos(todos().map(todo => todo.id === id ? { ...todo, completed: !todo.completed } : todo )); }; const deleteTodo = (id) => { todos(todos().filter(todo => todo.id !== id)); if (editingId() === id) { editingId(null); } }; const startEdit = (todo) => { editingId(todo.id); editText(todo.text); }; const saveEdit = (id) => { const text = editText().trim(); if (!text) { deleteTodo(id); } else { todos(todos().map(todo => todo.id === id ? { ...todo, text } : todo )); } editingId(null); }; const clearCompleted = () => { todos(todos().filter(t => !t.completed)); }; return html` ${styles}

📝 Todo App

e.key === 'Enter' && addTodo()} />
${() => filteredTodos().map(todo => html`
${editingId() === todo.id ? html` { if (e.key === 'Enter') saveEdit(todo.id); if (e.key === 'Escape') editingId(null); }} @blur=${() => saveEdit(todo.id)} autofocus /> ` : html` toggleTodo(todo.id)} /> startEdit(todo)}> ${todo.text} `}
`)}
${() => { const s = stats(); return html` Total: ${s.total} | Active: ${s.active} | Completed: ${s.completed} ${s.completed > 0 ? html` ` : ''} `; }}
`; }, []); ``` ### Data Dashboard with Real-time Updates ```typescript import { $, $$, html, $component } from 'sigpro'; // Simulated WebSocket connection class DataStream { constructor() { this.listeners = new Set(); this.interval = setInterval(() => { const data = { timestamp: Date.now(), value: Math.random() * 100, category: ['A', 'B', 'C'][Math.floor(Math.random() * 3)] }; this.listeners.forEach(fn => fn(data)); }, 1000); } subscribe(listener) { this.listeners.add(listener); return () => this.listeners.delete(listener); } destroy() { clearInterval(this.interval); } } $component('data-dashboard', () => { const stream = new DataStream(); const dataPoints = $([]); const selectedCategory = $('all'); const timeWindow = $(60); // seconds // Subscribe to data stream $$(() => { const unsubscribe = stream.subscribe((newData) => { dataPoints(prev => { const updated = [...prev, newData]; const maxAge = timeWindow() * 1000; const cutoff = Date.now() - maxAge; return updated.filter(d => d.timestamp > cutoff); }); }); return unsubscribe; }); // Filtered data const filteredData = $(() => { const data = dataPoints(); const category = selectedCategory(); if (category === 'all') return data; return data.filter(d => d.category === category); }); // Statistics const statistics = $(() => { const data = filteredData(); if (data.length === 0) return null; const values = data.map(d => d.value); return { count: data.length, avg: values.reduce((a, b) => a + b, 0) / values.length, min: Math.min(...values), max: Math.max(...values), last: values[values.length - 1] }; }); // Cleanup on unmount onUnmount(() => { stream.destroy(); }); return html`

📊 Real-time Dashboard

Time window: ${timeWindow}s
${() => { const stats = statistics(); if (!stats) return html`

Waiting for data...

`; return html`
Points: ${stats.count}
Average: ${stats.avg.toFixed(2)}
Min: ${stats.min.toFixed(2)}
Max: ${stats.max.toFixed(2)}
Last: ${stats.last.toFixed(2)}
`; }}
${() => filteredData().map(point => html`
`)}
`; }, []); ``` ## đź”§ Advanced Patterns ### Custom Hooks ```typescript import { $, $$ } from 'sigpro'; // useLocalStorage hook function useLocalStorage(key, initialValue) { const stored = localStorage.getItem(key); const signal = $(stored ? JSON.parse(stored) : initialValue); $$(() => { localStorage.setItem(key, JSON.stringify(signal())); }); return signal; } // useDebounce hook function useDebounce(signal, delay) { const debounced = $(signal()); let timeout; $$(() => { const value = signal(); clearTimeout(timeout); timeout = setTimeout(() => { debounced(value); }, delay); }); return debounced; } // useFetch hook function useFetch(url) { const data = $(null); const error = $(null); const loading = $(true); const fetchData = async () => { loading(true); error(null); try { const response = await fetch(url()); const json = await response.json(); data(json); } catch (e) { error(e); } finally {