Skip to content

Navigation Menu

Sign in
Appearance settings

Search code, repositories, users, issues, pull requests...

Provide feedback

We read every piece of feedback, and take your input very seriously.

Saved searches

Use saved searches to filter your results more quickly

Sign up
Appearance settings

Reactivity

jurisauthor edited this page Jun 29, 2025 · 1 revision

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

Remember: Async reactivity in Juris is about making asynchronous operations as simple and natural as synchronous ones, while maintaining excellent performance and user experience.

Clone this wiki locally

AltStyle によって変換されたページ (->オリジナル) /