Version 0.8.0 - The only Non-Blocking Reactive Framework for JavaScript
⚠️ Important: Follow This Reference Guide
This guide contains the canonical patterns and conventions for Juris development. Following these patterns is essential to:
- Prevent spaghetti code - Juris's flexibility can lead to inconsistent patterns if not properly structured
- Maintain code readability - Consistent VDOM syntax, component structure, and state organization
- Ensure optimal performance - Proper use of reactivity, batching, and the
"ignore"
pattern - Enable team collaboration - Standardized approaches that all developers can understand and maintain
- Leverage framework features - Correct usage of headless components, DOM enhancement, and state propagation
Key Rules:
- Use labeled closing brackets for nested structures (
}//div
,}//button
) - Prefer semantic HTML tags over unnecessary CSS classes (
div
,button
,ul
notdiv.wrapper
unless styling needed) - Use either
text
ORchildren
- the last defined property wins - Structure state paths logically (
user.profile.name
oruserName
- both are fine, use nesting when it makes sense) - Use services for stateless utilities, headless components for stateful business logic
- Leverage headless components for business logic, regular components for UI
- Use DOM enhancement when integrating with existing HTML/libraries
Anti-patterns to avoid:
- Mixing business logic in UI components
- Deeply nested state objects
- Unnecessary re-renders (use
"ignore"
pattern) - Complex conditional logic in reactive functions
- Manual DOM manipulation outside Juris
Follow these patterns religiously to build maintainable, performant Juris applications.
Quick Start
Basic Setup
<!DOCTYPE html> <html> <head> <script src="https://unpkg.com/juris@0.8.0/juris.js"></script> </head> <body> <div id="app"></div> <script> const app = new Juris({ states: { count: 0 }, layout: { div: { text: () => app.getState('count', 0), children: [ { button: { text: '+', onclick: () => app.setState('count', app.getState('count') + 1) } }, { button: { text: '-', onclick: () => app.setState('count', app.getState('count') - 1) } } ] }//div } }); app.render(); </script> </body> </html>
Component-Based App
const app = new Juris({ states: { todos: [] }, components: { TodoApp: (props, { getState, setState }) => ({ div: { children: [ { TodoInput: {} }, { TodoList: {} } ] }//div }), TodoInput: (props, { getState, setState }) => ({ input: { type: 'text', placeholder: 'Add todo...', onkeypress: (e) => { if (e.key === 'Enter' && e.target.value.trim()) { const todos = getState('todos', []); setState('todos', [...todos, { id: Date.now(), text: e.target.value.trim() }]); e.target.value = ''; } } }//input }), TodoList: (props, { getState }) => ({ ul: { children: () => getState('todos', []).map(todo => ({ li: { text: todo.text, key: todo.id } })) }//ul }) }, layout: { TodoApp: {} } }); app.render();
Core Concepts
Reactivity
Juris uses automatic dependency detection. When you call getState()
inside reactive functions, it automatically subscribes to changes:
// Reactive text - updates when 'user.name' changes text: () => getState('user.name', 'Anonymous') // Reactive children - updates when 'items' changes children: () => getState('items', []).map(item => ({ li: { text: item.name } })) // Reactive attributes className: () => getState('isActive') ? 'active' : 'inactive'
State Change Propagation
Juris implements hierarchical state propagation - when you change a state path, it automatically notifies all related subscribers:
// Given this state structure: const state = { user: { profile: { name: 'John', email: 'john@example.com' }, settings: { theme: 'dark' } } }; // When you call: setState('user.profile.name', 'Jane'); // Juris automatically triggers updates for subscribers to: // 1. 'user.profile.name' (exact match) // 2. 'user.profile' (parent path) // 3. 'user' (grandparent path) // 4. Any child paths (if they existed)
Dependency Re-discovery: Reactive functions can discover new dependencies as they run:
const ConditionalComponent = (props, { getState }) => ({ div: { text: () => { const showDetails = getState('ui.showDetails', false); if (showDetails) { // When showDetails becomes true, Juris automatically // subscribes to 'user.name' for future updates return getState('user.name', 'No name'); } return 'Click to show details'; } }//div }); // Initially subscribed to: ['ui.showDetails'] // After showDetails becomes true: ['ui.showDetails', 'user.name'] // Juris handles this subscription change automatically
Propagation in Action:
// Multiple components can subscribe to different levels const UserProfile = (props, { getState }) => ({ div: { // Subscribes to 'user.profile.name' text: () => `Name: ${getState('user.profile.name', '')}` }//div }); const UserCard = (props, { getState }) => ({ div: { // Subscribes to entire 'user.profile' object text: () => { const profile = getState('user.profile', {}); return `${profile.name} (${profile.email})`; } }//div }); const UserSection = (props, { getState }) => ({ div: { // Subscribes to entire 'user' object children: () => { const user = getState('user', {}); return [ { UserProfile: {} }, { UserCard: {} }, { div: { text: `Theme: ${user.settings?.theme}` } } ]; } }//div }); // When you call setState('user.profile.name', 'Alice'): // - UserProfile updates (direct subscription) // - UserCard updates (parent subscription) // - UserSection updates (grandparent subscription) // All automatically, no manual event handling needed!
VDOM Structure
Juris uses a lean object syntax for virtual DOM:
// Single element { tagName: { prop: value } } // With children { div: { children: [ { h1: { text: 'Title' } }, { p: { text: 'Content' } } ] }//div } // Use CSS selectors only when you need specific classes/IDs { 'div.container': { children: [ { 'h1#main-title': { text: 'Main Title' } }, { 'p.highlight': { text: 'Important content' } } ] }//div.container } // Reactive properties use functions { div: { text: () => getState('message'), style: () => ({ color: getState('theme.color') }), children: () => getState('items', []).map(item => ({ span: { text: item.name } })) }//div }
Text vs Children Precedence
Important: When both text
and children
are defined on the same element, the last one defined wins:
// Text wins (defined last) { div: { children: [{ button: { text: 'Click me' } }], text: 'Override text' // This wins - shows "Override text" }//div } // Children win (defined last) { div: { text: 'Will be replaced', children: [{ button: { text: 'Click me' } }] // This wins - shows button }//div } // Reactive example - dynamic switching { div: { children: [ { p: { text: 'Default content' } }, { button: { text: 'Action' } } ], text: () => { const loading = getState('isLoading', false); if (loading) { return 'Loading...'; // Replaces children when loading } // Return undefined to keep children return undefined; } }//div } // Best practice: Use either text OR children consistently { div: { children: () => { const loading = getState('isLoading', false); if (loading) { return [{ span: { text: 'Loading...' } }]; } return [ { p: { text: 'Content loaded' } }, { button: { text: 'Action' } } ]; } }//div }
State Management
Basic State Operations
// Initialize with default state const app = new Juris({ states: { user: { name: 'John', age: 30 }, settings: { theme: 'dark' }, items: [] } }); // Get state with default fallback const name = app.getState('user.name', 'Unknown'); const theme = app.getState('settings.theme', 'light'); // Set state (triggers reactivity) app.setState('user.name', 'Jane'); app.setState('settings.theme', 'light'); app.setState('items', [...app.getState('items', []), newItem]);
State Subscriptions
// Subscribe to specific path const unsubscribe = app.subscribe('user.name', (newValue, oldValue, path) => { console.log(`${path} changed from ${oldValue} to ${newValue}`); }); // Subscribe to exact path only (no children) const unsubscribeExact = app.subscribeExact('user', (newValue, oldValue) => { console.log('User object changed', newValue); }); // Unsubscribe unsubscribe();
Batched Updates
// Manual batching for performance app.stateManager.beginBatch(); app.setState('user.name', 'John'); app.setState('user.age', 31); app.setState('user.email', 'john@example.com'); app.stateManager.endBatch(); // Single render update // Check if batching is active if (app.stateManager.isBatchingActive()) { console.log('Currently batching updates'); }
Non-Reactive State Access
// Skip reactivity subscription (3rd parameter = false) const value = getState('some.path', defaultValue, false);
Component System
Component Registration
// Register individual component app.registerComponent('MyButton', (props, context) => ({ button: { text: props.label || 'Click me', className: props.variant || 'default', onclick: props.onClick || (() => {}) }//button })); // Register multiple components const app = new Juris({ components: { Header: (props, { getState }) => ({ header: { h1: { text: () => getState('app.title', 'My App') } }//header }), Counter: (props, { getState, setState }) => { const count = () => getState('counter.value', 0); return { div: { children: [ { span: { text: () => `Count: ${count()}` } }, { button: { text: '+', onclick: () => setState('counter.value', count() + 1) } }, { button: { text: '-', onclick: () => setState('counter.value', count() - 1) } } ] }//div }; } } });
Component Props
// Component with props const UserCard = (props, { getState }) => ({ div: { children: [ { img: { src: props.avatar, alt: 'Avatar' } }, { h3: { text: props.name } }, { p: { text: props.email } }, { button: { text: 'Follow', onclick: props.onFollow, disabled: props.isFollowing }}//button ] }//div }); // Usage with props { UserCard: { name: 'John Doe', email: 'john@example.com', avatar: '/avatar.jpg', isFollowing: () => getState('following.includes', false), onFollow: () => setState('following', [...getState('following', []), userId]) }//UserCard }
Component Lifecycle
const LifecycleComponent = (props, context) => { return { // Component lifecycle hooks hooks: { onMount: () => { console.log('Component mounted'); // Setup event listeners, timers, etc. }, onUpdate: (oldProps, newProps) => { console.log('Props changed', { oldProps, newProps }); }, onUnmount: () => { console.log('Component unmounting'); // Cleanup resources } }, // Component API (accessible to parent) api: { focus: () => element.querySelector('input')?.focus(), getValue: () => getState('local.value', '') }, // Render function render: () => ({ div: { text: 'Lifecycle component' } }) }; };
Local Component State
const StatefulComponent = (props, { newState }) => { const [getCount, setCount] = newState('count', 0); const [getText, setText] = newState('text', ''); return { div: { children: [ { input: { value: () => getText(), oninput: (e) => setText(e.target.value) }},//input { button: { text: () => `Clicked ${getCount()} times`, onclick: () => setCount(getCount() + 1) }}//button ] }//div }; };
DOM Enhancement
Basic Enhancement
// Enhance existing DOM elements app.enhance('.my-button', { className: () => getState('theme') === 'dark' ? 'btn-dark' : 'btn-light', onclick: () => setState('clicks', getState('clicks', 0) + 1), text: () => `Clicked ${getState('clicks', 0)} times` }); // Enhance with function-based definition app.enhance('.counter', (context) => { const { getState, setState, element } = context; return { text: () => getState('counter.value', 0), style: () => ({ color: getState('counter.value', 0) > 10 ? 'red' : 'blue' }), onclick: () => setState('counter.value', getState('counter.value', 0) + 1) }; }); // Enhancement with services access app.enhance('.api-button', (context) => { const { api, storage, setState } = context; // Services from config return { text: 'Load Data', onclick: async () => { setState('loading', true); try { const data = await api.get('/api/users'); storage.save('users', data); setState('users', data); } catch (error) { setState('error', error.message); } finally { setState('loading', false); } }, disabled: () => getState('loading', false) }; }); // Enhancement with headless component access (direct from context) app.enhance('.notification-trigger', (context) => { // Headless APIs are available directly from context const { NotificationManager } = context; return { text: 'Show Notification', onclick: () => { NotificationManager.show({ type: 'success', message: 'Enhancement triggered!', duration: 3000 }); } }; });
Selector-Based Enhancement
// Enhance containers with multiple selectors app.enhance('.dashboard', { selectors: { '.metric': { text: () => getState('metrics.revenue', '$0'), className: () => getState('metrics.trend') === 'up' ? 'positive' : 'negative' }, '.chart': (context) => ({ innerHTML: () => `<canvas data-value="${getState('metrics.data', [])}"></canvas>` }), '.refresh-btn': { onclick: () => { setState('loading', true); fetchMetrics().then(data => { setState('metrics', data); setState('loading', false); }); }, disabled: () => getState('loading', false) } } }); // Selector enhancement with services and headless components app.enhance('.user-dashboard', { // Container-level enhancement className: () => getState('user.role', 'guest'), selectors: { '.user-avatar': (context) => { const { api, storage } = context; // Services return { src: () => getState('user.avatar', '/default-avatar.png'), onclick: async () => { const newAvatar = await api.uploadAvatar(); storage.save('userAvatar', newAvatar); setState('user.avatar', newAvatar); } }; }, '.notification-bell': (context) => { // Direct access to headless component API const { NotificationManager } = context; return { text: () => { const count = NotificationManager.getUnreadCount(); return count > 0 ? count.toString() : ''; }, className: () => NotificationManager.hasUnread() ? 'has-notifications' : '', onclick: () => NotificationManager.markAllAsRead() }; }, '.sync-status': (context) => { const { SyncManager, api } = context; return { text: () => { const status = SyncManager.getStatus(); return status === 'syncing' ? 'Syncing...' : status === 'error' ? 'Sync Failed' : 'Synced'; }, className: () => `sync-${SyncManager.getStatus()}`, onclick: () => SyncManager.forceSync() }; }, '.data-export': (context) => { const { api, storage, DataManager } = context; return { text: 'Export Data', onclick: async () => { setState('exporting', true); try { const data = DataManager.getAllData(); const blob = await api.exportToCSV(data); const url = URL.createObjectURL(blob); // Create download link const a = document.createElement('a'); a.href = url; a.download = 'data-export.csv'; a.click(); storage.save('lastExport', Date.now()); } finally { setState('exporting', false); } }, disabled: () => getState('exporting', false) }; } } });
Enhancement Options
app.enhance('.auto-update', definition, { debounceMs: 100, // Debounce DOM mutations batchUpdates: true, // Batch multiple updates observeSubtree: true, // Watch for nested changes observeChildList: true, // Watch for added/removed elements observeNewElements: true, // Auto-enhance new elements onEnhanced: (element, context) => { console.log('Enhanced:', element); } });
Template System
Template Syntax
<template data-component="UserProfile" data-context="setState, getState"> <script> const user = () => getState('user', {}); const updateUser = (field, value) => setState(`user.${field}`, value); </script> <div class="profile"> <img src={()=>user().avatar} alt="Avatar" /> <h2>{()=>user().name}</h2> <input value={()=>user().email} oninput={(e)=>updateUser('email', e.target.value)} /> <div class={()=>user().isOnline ? 'online' : 'offline'}> {text: ()=>user().isOnline ? 'Online' : 'Offline'} </div> </div> </template>
Reactive Template Elements
<template data-component="TodoList" data-context="getState, setState"> <script> const todos = () => getState('todos', []); const addTodo = (text) => setState('todos', [...todos(), { id: Date.now(), text }]); </script> <div> <input onkeypress={(e)=>{ if(e.key==='Enter') { addTodo(e.target.value); e.target.value = ''; } }} /> <ul> {children: ()=>todos().map(todo => ({ li: { text: todo.text, key: todo.id } }))} </ul> </div> </template>
Auto-Compilation
// Templates auto-compile by default const app = new Juris({ autoCompileTemplates: true, // default states: { user: { name: 'John' } } }); // Manual compilation app.compileTemplates(); // Disable auto-compilation const app = new Juris({ autoCompileTemplates: false });
Headless Components
Basic Headless Component
// Register headless component (no DOM) app.registerHeadlessComponent('DataManager', (props, context) => { const { getState, setState, subscribe } = context; // Background logic const fetchData = async () => { setState('loading', true); try { const data = await fetch('/api/data').then(r => r.json()); setState('data', data); } finally { setState('loading', false); } }; return { // Lifecycle hooks hooks: { onRegister: () => { console.log('DataManager registered'); fetchData(); // Initial load // Auto-refresh every 30 seconds setInterval(fetchData, 30000); }, onUnregister: () => { console.log('DataManager cleanup'); } }, // Public API api: { refresh: fetchData, getData: () => getState('data', []), isLoading: () => getState('loading', false) } }; }); // Initialize headless component app.initializeHeadlessComponent('DataManager'); // Access headless API in regular components const MyComponent = (props, { components }) => ({ div: { children: [ { button: { text: 'Refresh Data', onclick: () => components.getHeadlessAPI('DataManager').refresh() } }, { div: { text: () => components.getHeadlessAPI('DataManager').isLoading() ? 'Loading...' : 'Ready' } } ] } });
Auto-Init Headless Components
const app = new Juris({ headlessComponents: { // Auto-initialize on startup AuthManager: { fn: (props, context) => ({ api: { login: (credentials) => { /* login logic */ }, logout: () => { /* logout logic */ }, isAuthenticated: () => context.getState('auth.isLoggedIn', false) } }), options: { autoInit: true } }, // Simple function (no auto-init) CacheManager: (props, context) => ({ api: { set: (key, value) => context.setState(`cache.${key}`, value), get: (key) => context.getState(`cache.${key}`) } }) } });
Async Handling
Async Props
// Components handle async props automatically const AsyncComponent = (props, context) => ({ div: { // Async text - shows loading state automatically text: fetch('/api/message').then(r => r.text()), // Async children children: fetch('/api/items').then(r => r.json()).then(items => items.map(item => ({ li: { text: item.name } })) ), // Async styles style: fetch('/api/theme').then(r => r.json()) } }); // Mixed sync/async props { div: { className: 'container', // sync text: () => getState('title'), // reactive style: fetchUserTheme(), // async promise children: [ { span: { text: 'Static content' } }, { span: { text: fetchDynamicContent() } } // async ] } }
Async State Updates
// Async setState setState('user', fetchUserData()); // Promise resolves automatically // Manual async handling const loadUser = async (userId) => { setState('loading', true); try { const user = await fetch(`/api/users/${userId}`).then(r => r.json()); setState('user', user); } catch (error) { setState('error', error.message); } finally { setState('loading', false); } };
Promise Tracking
// Track all promises for SSR/hydration startTracking(); // Render with async content app.render(); // Wait for all promises to resolve onAllComplete(() => { console.log('All async operations completed'); stopTracking(); });
Advanced Features
Server-Side Rendering (SSR)
// SSR-ready configuration const app = new Juris({ states: { isHydration: true }, layout: { App: {} } }); // Render with hydration mode app.render(); // Automatically handles SSR hydration
Render Modes
// Fine-grained reactivity (default) - immediate updates app.setRenderMode('fine-grained'); // Batch mode - batched updates for performance app.setRenderMode('batch'); // Check current mode if (app.isFineGrained()) { console.log('Using fine-grained rendering'); } // Set mode in constructor const app = new Juris({ renderMode: 'batch' // or 'fine-grained' });
Middleware
const app = new Juris({ middleware: [ // Logging middleware ({ path, oldValue, newValue, context }) => { console.log(`State change: ${path}`, { oldValue, newValue }); return newValue; // Return undefined to use original value }, // Validation middleware ({ path, newValue }) => { if (path === 'user.age' && newValue < 0) { console.warn('Age cannot be negative'); return 0; // Override with valid value } return newValue; }, // Persistence middleware ({ path, newValue }) => { if (path.startsWith('user.')) { localStorage.setItem('user', JSON.stringify(newValue)); } return newValue; } ] });
Service Injection
const app = new Juris({ services: { api: { get: (url) => fetch(url).then(r => r.json()), post: (url, data) => fetch(url, { method: 'POST', body: JSON.stringify(data), headers: { 'Content-Type': 'application/json' } }) }, storage: { save: (key, value) => localStorage.setItem(key, JSON.stringify(value)), load: (key) => JSON.parse(localStorage.getItem(key) || 'null') } } }); // Use services in components const MyComponent = (props, { api, storage }) => ({ button: { text: 'Save Data', onclick: async () => { const data = await api.get('/api/data'); storage.save('backup', data); } } });
Performance Optimization
Element Recycling
// DOM renderer automatically recycles elements // No configuration needed - handles pool management // Clear caches when needed app.domRenderer.clearAsyncCache(); app.componentManager.clearAsyncPropsCache();
Batched DOM Updates
// Manual batching for multiple state changes app.stateManager.beginBatch(); // Multiple state updates setState('user.name', 'John'); setState('user.email', 'john@example.com'); setState('user.age', 30); // Single DOM update app.stateManager.endBatch();
Efficient List Rendering
// Use keys for efficient list updates children: () => getState('items', []).map(item => ({ li: { text: item.name, key: item.id // Important for performance } })) // Avoid recreating objects in render functions const renderItem = (item) => ({ li: { text: item.name, key: item.id } }); children: () => getState('items', []).map(renderItem) // Use "ignore" pattern for structural optimization app.registerComponent('ListItem', (props, { getState }) => ({ li: { className: () => getState(`items.${props.itemId}.status`, 'active'), children: [ { span: { text: () => getState(`items.${props.itemId}.name`, '') } }, { small: { text: () => getState(`items.${props.itemId}.updatedAt`, '') } } ] } })); const OptimizedList = (props, { getState }) => { let lastItemIds = []; return { ul: { children: () => { const items = getState('itemsList', []); // Just the list of IDs const currentItemIds = items.map(item => item.id); // If the list structure (IDs) hasn't changed, skip re-rendering // Individual ListItem components will still update when their data changes if (JSON.stringify(currentItemIds) === JSON.stringify(lastItemIds)) { return "ignore"; } lastItemIds = currentItemIds; return items.map(item => ({ ListItem: { itemId: item.id, key: item.id } })); } } }; };
API Reference
Core Instance Methods
const app = new Juris(config); // State Management app.getState(path, defaultValue, track = true) app.setState(path, value, context = {}) app.subscribe(path, callback, hierarchical = true) app.subscribeExact(path, callback) // Component Management app.registerComponent(name, componentFn) app.registerHeadlessComponent(name, componentFn, options = {}) app.getComponent(name) app.getHeadlessComponent(name) app.initializeHeadlessComponent(name, props = {}) // Rendering app.render(container = '#app') app.setRenderMode('fine-grained' | 'batch') app.getRenderMode() app.isFineGrained() app.isBatchMode() // DOM Enhancement app.enhance(selector, definition, options = {}) app.configureEnhancement(options) app.getEnhancementStats() // Template System app.compileTemplates() // Utilities app.cleanup() app.destroy() app.getHeadlessStatus()
Context Object
// Available in all components and enhancement functions const context = { // State operations getState(path, defaultValue, track = true), setState(path, value, context = {}), subscribe(path, callback), // Local state (components only) newState(key, initialValue), // Returns [getter, setter] // Services (if configured) ...services, // Headless APIs ...headlessAPIs, // Component utilities components: { register(name, component), registerHeadless(name, component, options), get(name), getHeadless(name), initHeadless(name, props), reinitHeadless(name, props), getHeadlessAPI(name), getAllHeadlessAPIs() }, // Utilities utils: { render(container), cleanup(), forceRender(), setRenderMode(mode), getRenderMode(), isFineGrained(), isBatchMode(), getHeadlessStatus() }, // Framework access juris: app, // Current element (enhancement only) element: domElement, // Environment isSSR: boolean, // Logging logger: { log, warn, error, info, debug, subscribe, unsubscribe } };
Configuration Options
const config = { // Initial state states: {}, // State middleware middleware: [], // Root layout component layout: {}, // Component definitions components: {}, // Headless components headlessComponents: {}, // Services for dependency injection services: {}, // Render mode renderMode: 'auto' | 'fine-grained' | 'batch', // Template compilation autoCompileTemplates: true, // Logging level logLevel: 'debug' | 'info' | 'warn' | 'error' };
Common Patterns
Form Handling
const LoginForm = (props, { getState, setState }) => { const [getEmail, setEmail] = newState('email', ''); const [getPassword, setPassword] = newState('password', ''); const handleSubmit = (e) => { e.preventDefault(); setState('auth.isLoading', true); fetch('/api/login', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ email: getEmail(), password: getPassword() }) }) .then(r => r.json()) .then(data => { setState('auth.user', data.user); setState('auth.token', data.token); }) .finally(() => { setState('auth.isLoading', false); }); }; return { form: { onsubmit: handleSubmit, children: [ { input: { type: 'email', placeholder: 'Email', value: () => getEmail(), oninput: (e) => setEmail(e.target.value) } }, { input: { type: 'password', placeholder: 'Password', value: () => getPassword(), oninput: (e) => setPassword(e.target.value) } }, { button: { type: 'submit', text: () => getState('auth.isLoading') ? 'Logging in...' : 'Login', disabled: () => getState('auth.isLoading') } } ] } }; };
Modal Management
// Headless modal manager app.registerHeadlessComponent('ModalManager', (props, { getState, setState }) => ({ api: { open: (id, props = {}) => setState(`modals.${id}`, { open: true, ...props }), close: (id) => setState(`modals.${id}.open`, false), isOpen: (id) => getState(`modals.${id}.open`, false), getProps: (id) => getState(`modals.${id}`, {}) } })); // Modal component const Modal = (props, { components }) => { const modalManager = components.getHeadlessAPI('ModalManager'); return { div: { className: () => modalManager.isOpen(props.id) ? 'modal open' : 'modal', onclick: (e) => { if (e.target === e.currentTarget) { modalManager.close(props.id); } }, children: () => modalManager.isOpen(props.id) ? [ { div: { className: 'modal-content', children: [ { button: { className: 'close', text: '×', onclick: () => modalManager.close(props.id) } }, props.children ] } } ] : [] } }; };
Data Fetching Pattern
// Generic data fetcher headless component app.registerHeadlessComponent('DataFetcher', (props, { getState, setState }) => ({ api: { fetch: async (key, url, options = {}) => { setState(`data.${key}.loading`, true); setState(`data.${key}.error`, null); try { const response = await fetch(url, options); if (!response.ok) throw new Error(`HTTP ${response.status}`); const data = await response.json(); setState(`data.${key}.data`, data); setState(`data.${key}.lastFetch`, Date.now()); } catch (error) { setState(`data.${key}.error`, error.message); } finally { setState(`data.${key}.loading`, false); } }, getData: (key) => getState(`data.${key}.data`), isLoading: (key) => getState(`data.${key}.loading`, false), getError: (key) => getState(`data.${key}.error`), shouldRefetch: (key, maxAge = 300000) => { // 5 minutes const lastFetch = getState(`data.${key}.lastFetch`, 0); return Date.now() - lastFetch > maxAge; } } })); // Usage in component const UserList = (props, { components }) => { const dataFetcher = components.getHeadlessAPI('DataFetcher'); // Auto-fetch on mount useEffect(() => { if (dataFetcher.shouldRefetch('users')) { dataFetcher.fetch('users', '/api/users'); } }); return { div: { children: () => { if (dataFetcher.isLoading('users')) { return [{ div: { text: 'Loading users...' } }]; } if (dataFetcher.getError('users')) { return [{ div: { className: 'error', text: `Error: ${dataFetcher.getError('users')}` } }]; } const users = dataFetcher.getData('users') || []; return users.map(user => ({ div: { key: user.id, className: 'user-card', children: [ { h3: { text: user.name } }, { p: { text: user.email } } ] } })); } } }; };
Debugging and Development
State Inspection
// Debug state in console console.log('Current state:', app.stateManager.state); // Monitor all state changes using middleware const debugMiddleware = ({ path, oldValue, newValue }) => { console.log(`State changed: ${path}`, { oldValue, newValue }); return newValue; }; const app = new Juris({ middleware: [debugMiddleware], // ... other config }); // Or subscribe to specific top-level paths app.subscribe('user', (newValue, oldValue, path) => { console.log(`User state changed: ${path}`, { oldValue, newValue }); }); app.subscribe('app', (newValue, oldValue, path) => { console.log(`App state changed: ${path}`, { oldValue, newValue }); }); // Get enhancement statistics console.log('Enhancement stats:', app.getEnhancementStats()); // Get headless component status console.log('Headless status:', app.getHeadlessStatus());
Performance Monitoring
// Monitor render performance const startTime = performance.now(); app.render(); console.log(`Render took: ${performance.now() - startTime}ms`); // Monitor state update frequency let updateCount = 0; app.subscribe('', () => { updateCount++; console.log(`State updates: ${updateCount}`); });
Error Handling
// Global error handling in middleware const errorHandlingMiddleware = ({ path, oldValue, newValue, context }) => { try { // Validate state updates if (path === 'user.age' && typeof newValue !== 'number') { throw new Error('Age must be a number'); } return newValue; } catch (error) { console.error(`State validation error for ${path}:`, error); // Return old value to prevent invalid change return oldValue; } }; const app = new Juris({ middleware: [errorHandlingMiddleware] });
Best Practices
- Use keys for list items to enable efficient DOM reconciliation
- Batch state updates when making multiple changes
- Prefer headless components for complex business logic
- Use middleware for cross-cutting concerns like logging and validation
- Structure state paths logically (e.g.,
user.profile.name
notuserProfileName
) - Avoid deep nesting in state objects when possible
- Use local component state for UI-only state that doesn't need to be shared
- Enhance existing DOM rather than rebuilding when integrating with other libraries
- Implement error boundaries in components that fetch data
- Use templates for complex HTML structures with light JavaScript logic
Juris v0.8.0 - Built for performance, designed for simplicity.
Top comments (0)