Reactivity - jurisjs/juris GitHub Wiki
Complete Guide: From Basic Reactivity to Advanced Async Patterns
Introduction
Juris provides a powerful async-first reactivity system that handles both synchronous and asynchronous state changes seamlessly. Unlike traditional frameworks that treat async as an afterthought, Juris is built from the ground up to handle promises, async functions, and time-dependent data naturally.
Key Concepts
- Function-based Reactivity: Properties become reactive when defined as functions
- Automatic Promise Handling: Async functions and promises are handled automatically
- Non-blocking Rendering: UI remains responsive during async operations
- Smart Placeholders: Automatic loading states and error handling
- Dependency Tracking: Automatic subscription to state changes
Basic Reactivity
A simple rule to Juris reactivity: Reactivity works when getState is called from intended functional attributes and children.
1. Synchronous Reactivity
// ❌ Static - Not reactive
juris.registerComponent('Counter', (props, context) => {
const count = context.getState('counter', 0); // Called once
return {
div: {
text: `Count: ${count}`, // Never updates
children: [
{ button: { text: 'Increment', onclick: () => context.setState('counter', count + 1) } }
]
}
};
});
// ✅ Reactive - Updates when state changes
juris.registerComponent('Counter', (props, context) => {
return {
div: {
text: () => `Count: ${context.getState('counter', 0)}`, // Reactive function
children: [
{
button: {
text: 'Increment',
onclick: () => {
const current = context.getState('counter', 0);
context.setState('counter', current + 1);
}
}
}
]
}
};
});
2. Reactive Properties
All component properties can be made reactive by using functions:
juris.registerComponent('ReactiveElement', (props, context) => {
return {
div: {
// Reactive text
text: () => context.getState('message', 'Hello'),
// Reactive styling
style: () => ({
color: context.getState('theme.color', 'black'),
backgroundColor: context.getState('theme.bg', 'white')
}),
// Reactive class names
className: () => {
const isActive = context.getState('ui.isActive', false);
return `element ${isActive ? 'active' : 'inactive'}`;
},
// Reactive children
children: () => {
const items = context.getState('items', []);
return items.map(item => ({
span: { text: item.name, key: item.id }
}));
}
}
};
});
3. Conditional Reactive Rendering
juris.registerComponent('ConditionalComponent', (props, context) => {
return {
div: {
children: () => {
const isLoggedIn = context.getState('user.isLoggedIn', false);
const userType = context.getState('user.type', 'guest');
if (!isLoggedIn) {
return [{ LoginForm: {} }];
}
switch (userType) {
case 'admin':
return [{ AdminDashboard: {} }];
case 'moderator':
return [{ ModeratorPanel: {} }];
default:
return [{ UserDashboard: {} }];
}
}
}
};
});
Understanding Async in Juris
1. How Juris Handles Promises
Juris automatically detects and handles promises in:
- Component functions
- Property functions
- State values
- Event handlers
// Automatic promise detection
const isPromise = value => value?.then;
// Juris wraps all potential promises
const promisify = result => {
const promise = result?.then ? result : Promise.resolve(result);
// ... tracking and handling logic
return promise;
};
2. Promise Tracking System
// Juris tracks active promises
const createPromisify = () => {
const activePromises = new Set();
let isTracking = false;
const subscribers = new Set();
const trackingPromisify = result => {
const promise = result?.then ? result : Promise.resolve(result);
if (isTracking && promise !== result) {
activePromises.add(promise);
promise.finally(() => {
activePromises.delete(promise);
setTimeout(checkAllComplete, 0);
});
}
return promise;
};
return { promisify: trackingPromisify, startTracking, stopTracking, onAllComplete };
};
3. Automatic Placeholder Management
When async operations are detected, Juris automatically:
- Creates loading placeholders
- Replaces placeholders with resolved content
- Handles errors with error placeholders
- Manages cleanup when components unmount
Async Component Patterns
1. Async Component Functions
// Component function returns a promise
juris.registerComponent('AsyncUserProfile', async (props, context) => {
// Async data fetching
const user = await fetch(`/api/users/${props.userId}`).then(r => r.json());
const preferences = await fetch(`/api/users/${props.userId}/preferences`).then(r => r.json());
return {
div: {
className: 'user-profile',
children: [
{ h2: { text: user.name } },
{ p: { text: user.email } },
{ UserPreferences: { preferences } }
]
}
};
});
// Usage - automatic loading placeholder
{ AsyncUserProfile: { userId: 123 } } // Shows "Loading AsyncUserProfile..." until resolved
2. Async Props
// Components can receive async props
juris.registerComponent('DataDisplay', (props, context) => {
return {
div: {
// Async props are automatically resolved
text: props.asyncData, // If this is a promise, shows loading then resolves
className: 'data-display'
}
};
});
// Usage with async props
const asyncData = fetch('/api/data').then(r => r.json().then(data => data.message));
{ DataDisplay: { asyncData } } // Automatic async prop handling
3. Async Reactive Properties
juris.registerComponent('LiveDataComponent', (props, context) => {
return {
div: {
// Async reactive text
text: async () => {
const userId = context.getState('user.currentId');
if (!userId) return 'No user selected';
try {
const response = await fetch(`/api/users/${userId}/status`);
const data = await response.json();
return `Status: ${data.status}`;
} catch (error) {
return `Error: ${error.message}`;
}
},
// Async reactive children
children: async () => {
const filter = context.getState('filter.current', 'all');
const response = await fetch(`/api/items?filter=${filter}`);
const items = await response.json();
return items.map(item => ({
ItemCard: { key: item.id, item }
}));
}
}
};
});
4. Async State Updates
juris.registerComponent('AsyncForm', (props, context) => {
return {
form: {
onsubmit: async (e) => {
e.preventDefault();
// Set loading state
context.setState('form.isSubmitting', true);
try {
const formData = new FormData(e.target);
const response = await fetch('/api/submit', {
method: 'POST',
body: formData
});
if (response.ok) {
const result = await response.json();
context.setState('form.result', result);
context.setState('form.success', true);
} else {
throw new Error('Submission failed');
}
} catch (error) {
context.setState('form.error', error.message);
} finally {
context.setState('form.isSubmitting', false);
}
},
children: () => {
const isSubmitting = context.getState('form.isSubmitting', false);
const error = context.getState('form.error', null);
const success = context.getState('form.success', false);
return [
{ input: { type: 'text', name: 'data', required: true } },
{
button: {
type: 'submit',
disabled: isSubmitting,
text: isSubmitting ? 'Submitting...' : 'Submit'
}
},
...(error ? [{ div: { className: 'error', text: error } }] : []),
...(success ? [{ div: { className: 'success', text: 'Success!' } }] : [])
];
}
}
};
});
Advanced Async Reactivity
1. Async State with Caching
// Advanced async state management with caching
juris.registerComponent('CachedDataComponent', (props, context) => {
return {
div: {
children: async () => {
const cacheKey = `data_${props.id}`;
const cached = context.getState(`cache.${cacheKey}`, null);
const cacheTime = context.getState(`cache.${cacheKey}_time`, 0);
const now = Date.now();
// Use cache if less than 5 minutes old
if (cached && (now - cacheTime) < 300000) {
return cached.map(item => ({ ItemCard: { key: item.id, item } }));
}
// Fetch new data
try {
const response = await fetch(`/api/data/${props.id}`);
const data = await response.json();
// Cache the result
context.setState(`cache.${cacheKey}`, data);
context.setState(`cache.${cacheKey}_time`, now);
return data.map(item => ({ ItemCard: { key: item.id, item } }));
} catch (error) {
// Return cached data on error, or empty array
return cached ? cached.map(item => ({ ItemCard: { key: item.id, item } })) : [];
}
}
}
};
});
2. Debounced Async Operations
// Debounced search with async operations
juris.registerComponent('SearchComponent', (props, context) => {
let searchTimeout;
return {
div: {
children: [
{
input: {
type: 'text',
placeholder: 'Search...',
oninput: (e) => {
const query = e.target.value;
// Clear previous timeout
clearTimeout(searchTimeout);
if (!query.trim()) {
context.setState('search.results', []);
context.setState('search.isSearching', false);
return;
}
// Set searching state immediately
context.setState('search.isSearching', true);
// Debounce the actual search
searchTimeout = setTimeout(async () => {
try {
const response = await fetch(`/api/search?q=${encodeURIComponent(query)}`);
const results = await response.json();
context.setState('search.results', results);
} catch (error) {
context.setState('search.error', error.message);
} finally {
context.setState('search.isSearching', false);
}
}, 300);
}
}
},
{
div: {
className: 'search-results',
children: () => {
const isSearching = context.getState('search.isSearching', false);
const results = context.getState('search.results', []);
const error = context.getState('search.error', null);
if (isSearching) {
return [{ div: { className: 'loading', text: 'Searching...' } }];
}
if (error) {
return [{ div: { className: 'error', text: `Error: ${error}` } }];
}
if (results.length === 0) {
return [{ div: { className: 'empty', text: 'No results found' } }];
}
return results.map(result => ({
SearchResult: { key: result.id, result }
}));
}
}
}
]
}
};
});
3. Parallel Async Operations
// Component handling multiple parallel async operations
juris.registerComponent('DashboardWidget', (props, context) => {
return {
div: {
className: 'dashboard-widget',
children: async () => {
const userId = context.getState('user.id');
if (!userId) return [{ div: { text: 'Please log in' } }];
try {
// Parallel async operations
const [userData, statsData, notificationsData] = await Promise.all([
fetch(`/api/users/${userId}`).then(r => r.json()),
fetch(`/api/users/${userId}/stats`).then(r => r.json()),
fetch(`/api/users/${userId}/notifications`).then(r => r.json())
]);
return [
{ UserInfo: { user: userData } },
{ StatsPanel: { stats: statsData } },
{ NotificationsList: { notifications: notificationsData } }
];
} catch (error) {
return [{
div: {
className: 'error',
text: `Failed to load dashboard: ${error.message}`
}
}];
}
}
}
};
});
4. Async Component with Custom Loading States
// Component with custom loading indicators
juris.registerComponent('CustomAsyncComponent', (props, context) => {
return {
render: async () => {
// Custom loading indicator
const loadingIndicator = {
div: {
className: 'custom-loading',
children: [
{ div: { className: 'spinner' } },
{ p: { text: 'Loading custom data...' } }
]
}
};
try {
const data = await fetch('/api/complex-data').then(r => r.json());
return {
div: {
className: 'loaded-content',
children: [
{ h2: { text: data.title } },
{ ComplexDataViz: { data: data.visualization } }
]
}
};
} catch (error) {
return {
div: {
className: 'error-state',
children: [
{ h3: { text: 'Oops! Something went wrong' } },
{ p: { text: error.message } },
{
button: {
text: 'Retry',
onclick: () => context.setState('forceRefresh', Date.now())
}
}
]
}
};
}
},
// Custom loading indicator while render function executes
indicator: {
div: {
className: 'custom-loading',
children: [
{ div: { className: 'spinner' } },
{ p: { text: 'Loading custom data...' } }
]
}
}
};
});
5. Real-time Async Updates
// Component with real-time updates using WebSocket
juris.registerComponent('LiveFeed', (props, context) => {
return {
hooks: {
onMount: () => {
const ws = new WebSocket('ws://localhost:8080/feed');
ws.onmessage = (event) => {
const data = JSON.parse(event.data);
const currentFeed = context.getState('feed.items', []);
context.setState('feed.items', [data, ...currentFeed.slice(0, 49)]); // Keep last 50
};
ws.onopen = () => context.setState('feed.connected', true);
ws.onclose = () => context.setState('feed.connected', false);
ws.onerror = (error) => context.setState('feed.error', error.message);
// Store ws reference for cleanup
context.setState('feed.websocket', ws);
},
onUnmount: () => {
const ws = context.getState('feed.websocket');
if (ws) {
ws.close();
context.setState('feed.websocket', null);
}
}
},
render: () => {
return {
div: {
className: 'live-feed',
children: () => {
const connected = context.getState('feed.connected', false);
const items = context.getState('feed.items', []);
const error = context.getState('feed.error', null);
return [
{
div: {
className: `status ${connected ? 'connected' : 'disconnected'}`,
text: connected ? '🟢 Live' : '🔴 Disconnected'
}
},
...(error ? [{
div: { className: 'error', text: `Error: ${error}` }
}] : []),
{
div: {
className: 'feed-items',
children: items.map(item => ({
FeedItem: { key: item.id, item }
}))
}
}
];
}
}
};
}
};
});
Performance & Best Practices
1. Async Caching Strategies
// Implement smart caching for async operations
const createAsyncCache = (context) => {
const CACHE_DURATION = 5 * 60 * 1000; // 5 minutes
return {
async get(key, fetcher) {
const cached = context.getState(`cache.${key}`, null);
const timestamp = context.getState(`cache.${key}_timestamp`, 0);
if (cached && (Date.now() - timestamp) < CACHE_DURATION) {
return cached;
}
const data = await fetcher();
context.setState(`cache.${key}`, data);
context.setState(`cache.${key}_timestamp`, Date.now());
return data;
},
invalidate(key) {
context.setState(`cache.${key}`, null);
context.setState(`cache.${key}_timestamp`, 0);
}
};
};
// Usage in component
juris.registerComponent('CachedComponent', (props, context) => {
const cache = createAsyncCache(context);
return {
div: {
children: async () => {
return await cache.get(`data_${props.id}`, async () => {
const response = await fetch(`/api/data/${props.id}`);
return response.json();
});
}
}
};
});
2. Error Boundaries for Async Components
// Async error boundary pattern
juris.registerComponent('AsyncErrorBoundary', (props, context) => {
return {
div: {
className: 'error-boundary',
children: async () => {
try {
// Wrap async children in error handling
const children = await Promise.resolve(props.children);
context.setState('errorBoundary.hasError', false);
return Array.isArray(children) ? children : [children];
} catch (error) {
console.error('AsyncErrorBoundary caught error:', error);
context.setState('errorBoundary.hasError', true);
context.setState('errorBoundary.error', error.message);
return [{
div: {
className: 'error-fallback',
children: [
{ h3: { text: 'Something went wrong' } },
{ p: { text: error.message } },
{
button: {
text: 'Try Again',
onclick: () => {
context.setState('errorBoundary.hasError', false);
context.setState('errorBoundary.error', null);
}
}
}
]
}
}];
}
}
}
};
});
3. Optimizing Async Rerenders
// Prevent unnecessary async operations
juris.registerComponent('OptimizedAsyncComponent', (props, context) => {
return {
div: {
children: async () => {
const dependencies = {
userId: context.getState('user.id'),
filter: context.getState('filter.current'),
sortBy: context.getState('sort.field')
};
// Create dependency hash to avoid redundant calls
const depHash = JSON.stringify(dependencies);
const lastHash = context.getState('component.lastDepHash', '');
if (depHash === lastHash) {
// Return cached result if dependencies haven't changed
return context.getState('component.lastResult', []);
}
const result = await fetch('/api/data', {
method: 'POST',
body: JSON.stringify(dependencies)
}).then(r => r.json());
// Cache result and dependencies
context.setState('component.lastResult', result);
context.setState('component.lastDepHash', depHash);
return result.map(item => ({ ItemCard: { key: item.id, item } }));
}
}
};
});
4. Memory Management for Async Operations
// Proper cleanup for async operations
juris.registerComponent('AsyncComponentWithCleanup', (props, context) => {
let abortController;
return {
hooks: {
onMount: () => {
abortController = new AbortController();
},
onUnmount: () => {
if (abortController) {
abortController.abort();
}
}
},
render: () => ({
div: {
children: async () => {
try {
const response = await fetch('/api/data', {
signal: abortController.signal
});
if (response.ok) {
const data = await response.json();
return data.map(item => ({ ItemCard: { item } }));
}
} catch (error) {
if (error.name === 'AbortError') {
return [{ div: { text: 'Request cancelled' } }];
}
throw error;
}
}
}
})
};
});
Troubleshooting
Common Issues and Solutions
1. Async Function Not Updating
Problem: Async function runs once but doesn't update when state changes.
// ❌ Wrong - getState called outside reactive function
juris.registerComponent('BrokenAsync', (props, context) => {
const userId = context.getState('user.id'); // Called once
return {
div: {
text: async () => {
const response = await fetch(`/api/users/${userId}`); // userId never updates
const user = await response.json();
return user.name;
}
}
};
});
// ✅ Correct - getState called inside reactive function
juris.registerComponent('WorkingAsync', (props, context) => {
return {
div: {
text: async () => {
const userId = context.getState('user.id'); // Called on every update
if (!userId) return 'No user';
const response = await fetch(`/api/users/${userId}`);
const user = await response.json();
return user.name;
}
}
};
});
2. Memory Leaks with Async Operations
Problem: Async operations continue after component unmount.
// ❌ Wrong - potential memory leak
juris.registerComponent('LeakyComponent', (props, context) => {
return {
div: {
children: async () => {
// This continues even if component unmounts
await new Promise(resolve => setTimeout(resolve, 5000));
const data = await fetch('/api/data').then(r => r.json());
return data.map(item => ({ ItemCard: { item } }));
}
}
};
});
// ✅ Correct - proper cleanup
juris.registerComponent('CleanComponent', (props, context) => {
let isMounted = true;
return {
hooks: {
onUnmount: () => {
isMounted = false;
}
},
render: () => ({
div: {
children: async () => {
await new Promise(resolve => setTimeout(resolve, 5000));
if (!isMounted) return []; // Check if still mounted
const data = await fetch('/api/data').then(r => r.json());
return data.map(item => ({ ItemCard: { item } }));
}
}
})
};
});
3. Infinite Async Loops
Problem: Async function triggers its own state changes causing loops.
// ❌ Wrong - infinite loop
juris.registerComponent('InfiniteLoop', (props, context) => {
return {
div: {
children: async () => {
const count = context.getState('count', 0);
context.setState('count', count + 1); // This triggers another render!
const data = await fetch('/api/data').then(r => r.json());
return data.map(item => ({ ItemCard: { item } }));
}
}
};
});
// ✅ Correct - separate state updates from reactive functions
juris.registerComponent('NoLoop', (props, context) => {
return {
div: {
children: [
{
button: {
text: 'Increment',
onclick: () => {
const count = context.getState('count', 0);
context.setState('count', count + 1); // State update in event handler
}
}
},
{
div: {
children: async () => {
const count = context.getState('count', 0); // Only reads state
const data = await fetch(`/api/data?count=${count}`).then(r => r.json());
return data.map(item => ({ ItemCard: { item } }));
}
}
}
]
}
};
});
Debugging Async Components
1. Enable Debug Logging
// Enable detailed logging for async operations
const juris = new Juris({
logLevel: 'debug',
// ... other config
});
// Subscribe to log messages
juris.logger.subscribe((message, category) => {
if (category === 'async') {
console.log('Async operation:', message);
}
});
2. Async State Inspection
// Add debug information to async components
juris.registerComponent('DebuggableAsync', (props, context) => {
return {
div: {
children: async () => {
const startTime = Date.now();
console.log('Async render started');
try {
const data = await fetch('/api/data').then(r => r.json());
const duration = Date.now() - startTime;
console.log(`Async render completed in ${duration}ms`);
// Store debug info in state
context.setState('debug.lastRenderTime', duration);
context.setState('debug.lastRenderData', data.length);
return data.map(item => ({ ItemCard: { item } }));
} catch (error) {
console.error('Async render failed:', error);
context.setState('debug.lastError', error.message);
throw error;
}
}
}
};
});
Real-World Examples
1. E-commerce Product List with Async Search
juris.registerComponent('ProductList', (props, context) => {
let searchTimeout;
return {
div: {
className: 'product-list',
children: [
// Search input with debouncing
{
div: {
className: 'search-bar',
children: [
{
input: {
type: 'text',
placeholder: 'Search products...',
oninput: (e) => {
const query = e.target.value;
clearTimeout(searchTimeout);
context.setState('search.isSearching', true);
searchTimeout = setTimeout(async () => {
if (query.trim()) {
try {
const response = await fetch(`/api/products/search?q=${encodeURIComponent(query)}`);
const products = await response.json();
context.setState('products.list', products);
context.setState('search.hasSearched', true);
} catch (error) {
context.setState('search.error', error.message);
}
} else {
context.setState('products.list', []);
context.setState('search.hasSearched', false);
}
context.setState('search.isSearching', false);
}, 300);
}
}
},
{
div: {
className: 'search-status',
text: () => {
const isSearching = context.getState('search.isSearching', false);
const hasSearched = context.getState('search.hasSearched', false);
const error = context.getState('search.error', null);
if (error) return `Error: ${error}`;
if (isSearching) return 'Searching...';
if (hasSearched) return 'Search complete';
return 'Type to search';
}
}
}
]
}
},
// Product grid with async loading
{
div: {
className: 'product-grid',
children: async () => {
const products = context.getState('products.list', []);
const sortBy = context.getState('sort.field', 'name');
const filterCategory = context.getState('filter.category', 'all');
if (products.length === 0) {
return [{ div: { className: 'empty', text: 'No products found' } }];
}
// Async sorting and filtering
const processedProducts = await new Promise(resolve => {
setTimeout(() => {
let filtered = products;
if (filterCategory !== 'all') {
filtered = products.filter(p => p.category === filterCategory);
}
const sorted = [...filtered].sort((a, b) => {
switch (sortBy) {
case 'price':
return a.price - b.price;
case 'rating':
return b.rating - a.rating;
default:
return a.name.localeCompare(b.name);
}
});
resolve(sorted);
}, 100); // Simulate processing time
});
return processedProducts.map(product => ({
ProductCard: { key: product.id, product }
}));
}
}
}
]
}
};
});
2. Real-time Chat Component
juris.registerComponent('ChatRoom', (props, context) => {
let ws;
let reconnectAttempts = 0;
const maxReconnectAttempts = 5;
const connectWebSocket = () => {
ws = new WebSocket(`ws://localhost:8080/chat/${props.roomId}`);
ws.onopen = () => {
console.log('Chat connected');
context.setState('chat.connected', true);
context.setState('chat.error', null);
reconnectAttempts = 0;
};
ws.onmessage = (event) => {
const message = JSON.parse(event.data);
const messages = context.getState('chat.messages', []);
context.setState('chat.messages', [...messages, message]);
};
ws.onclose = () => {
context.setState('chat.connected', false);
// Auto-reconnect
if (reconnectAttempts < maxReconnectAttempts) {
reconnectAttempts++;
setTimeout(connectWebSocket, 1000 * reconnectAttempts);
}
};
ws.onerror = (error) => {
context.setState('chat.error', 'Connection failed');
};
};
return {
hooks: {
onMount: connectWebSocket,
onUnmount: () => {
if (ws) ws.close();
}
},
render: () => ({
div: {
className: 'chat-room',
children: [
// Connection status
{
div: {
className: 'chat-status',
children: () => {
const connected = context.getState('chat.connected', false);
const error = context.getState('chat.error', null);
if (error) {
return [{
span: {
className: 'error',
text: `❌ ${error}`
}
}];
}
return [{
span: {
className: connected ? 'connected' : 'disconnected',
text: connected ? '🟢 Connected' : '🔴 Disconnected'
}
}];
}
}
},
// Messages list with auto-scroll
{
div: {
className: 'messages',
children: () => {
const messages = context.getState('chat.messages', []);
return messages.map(message => ({
ChatMessage: { key: message.id, message }
}));
}
}
},
// Message input
{
form: {
className: 'message-form',
onsubmit: async (e) => {
e.preventDefault();
const input = e.target.querySelector('input');
const text = input.value.trim();
if (text && ws && ws.readyState === WebSocket.OPEN) {
const message = {
id: Date.now(),
text,
userId: context.getState('user.id'),
timestamp: new Date().toISOString()
};
ws.send(JSON.stringify(message));
input.value = '';
}
},
children: () => {
const connected = context.getState('chat.connected', false);
return [
{
input: {
type: 'text',
placeholder: connected ? 'Type a message...' : 'Connecting...',
disabled: !connected
}
},
{
button: {
type: 'submit',
text: 'Send',
disabled: !connected
}
}
];
}
}
}
]
}
})
};
});
3. Advanced Data Visualization with Async Loading
juris.registerComponent('DataVisualization', (props, context) => {
return {
div: {
className: 'data-viz',
children: async () => {
const timeRange = context.getState('viz.timeRange', '7d');
const dataType = context.getState('viz.dataType', 'revenue');
const refreshTrigger = context.getState('viz.refreshTrigger', 0);
try {
// Parallel data fetching
const [mainData, comparativeData, metadata] = await Promise.all([
fetch(`/api/analytics/${dataType}?range=${timeRange}`).then(r => r.json()),
fetch(`/api/analytics/${dataType}/comparative?range=${timeRange}`).then(r => r.json()),
fetch(`/api/analytics/metadata?type=${dataType}`).then(r => r.json())
]);
// Process data for visualization
const processedData = await new Promise(resolve => {
// Simulate data processing
setTimeout(() => {
const processed = {
main: mainData.map(d => ({
...d,
trend: d.value > d.previousValue ? 'up' : 'down'
})),
comparative: comparativeData,
summary: {
total: mainData.reduce((sum, d) => sum + d.value, 0),
average: mainData.reduce((sum, d) => sum + d.value, 0) / mainData.length,
change: mainData[mainData.length - 1]?.value - mainData[0]?.value
}
};
resolve(processed);
}, 500);
});
return [
// Summary cards
{
div: {
className: 'summary-cards',
children: [
{
SummaryCard: {
title: 'Total',
value: processedData.summary.total,
format: metadata.format
}
},
{
SummaryCard: {
title: 'Average',
value: processedData.summary.average,
format: metadata.format
}
},
{
SummaryCard: {
title: 'Change',
value: processedData.summary.change,
format: metadata.format,
trend: processedData.summary.change > 0 ? 'positive' : 'negative'
}
}
]
}
},
// Main chart
{
ChartComponent: {
data: processedData.main,
type: metadata.chartType,
options: metadata.chartOptions
}
},
// Comparative chart
{
ComparativeChart: {
data: processedData.comparative,
timeRange
}
}
];
} catch (error) {
return [{
div: {
className: 'error-state',
children: [
{ h3: { text: 'Failed to load data' } },
{ p: { text: error.message } },
{
button: {
text: 'Retry',
onclick: () => {
const current = context.getState('viz.refreshTrigger', 0);
context.setState('viz.refreshTrigger', current + 1);
}
}
}
]
}
}];
}
}
}
};
});
Conclusion
Juris provides a comprehensive async reactivity system that makes handling asynchronous operations natural and performant. Key takeaways:
- Function-based reactivity ensures components update when dependencies change
- Automatic promise handling removes boilerplate for async operations
- Smart placeholders provide excellent user experience during loading
- Proper cleanup prevents memory leaks and stale updates
- Caching strategies optimize performance for repeated operations
By following these patterns and best practices, you can build highly responsive and efficient applications that handle complex async scenarios gracefully.
Additional Resources
- [Juris Official Documentation](https://jurisjs.com/)
- [GitHub Repository](https://github.com/jurisjs/juris)
- [Community Examples](https://github.com/jurisjs/examples)
- [Performance Benchmarks](https://jurisjs.com/benchmarks)
Remember: Async reactivity in Juris is about making asynchronous operations as simple and natural as synchronous ones, while maintaining excellent performance and user experience.