Skip to content

Juris FluentState

jurisauthor edited this page Aug 26, 2025 · 2 revisions

Fluent State Management for the Juris Reactive Framework

A headless state management component that provides an intuitive, proxy-based interface for reactive state operations. Designed for Juris's "Object-First Architecture" with automatic reactivity, subscription management, and batch processing capabilities.

Features

  • Direct Property Access: Natural JavaScript object syntax for state operations
  • Object-Only Architecture: Consistent object-based state management
  • Automatic Reactivity: Seamless integration with Juris's reactive system
  • Non-Reactive Mode: Access state without triggering subscriptions via .x
  • Intelligent Auto-Creation: Automatically creates object/array structures as needed
  • Subscription System: Built-in watch, subscribe, and onChange methods
  • Batch Processing: Efficient batch updates for multiple state changes
  • Lazy Proxy Loading: Creates state paths on-demand for optimal performance
  • Deep Path Support: Handles deeply nested object structures automatically
  • Temporal Independence: Works with any component lifecycle pattern

Installation

CDN (Instant Deployment)

<!-- Core Juris Framework -->
<script src="https://unpkg.com/juris@0.9.0/juris.js"></script>
<!-- Headless Component Support -->
<script src="https://unpkg.com/juris@0.9.0/juris-headless.js"></script>
<!-- FluentState Component -->
<script src="https://unpkg.com/juris@0.9.0/headless/juris-fluentstate.js"></script>

NPM Installation

npm install juris@0.9.0
import { Juris } from 'juris/juris';
import { HeadlessManager } from 'juris/juris-headless';
import { createFluentStateHeadless } from 'juris/headless/juris-fluentstate';

Quick Start

Basic Setup

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>Juris FluentState App</title>
    <script src="https://unpkg.com/juris@0.9.0/juris.js"></script>
    <script src="https://unpkg.com/juris@0.9.0/juris-headless.js"></script>
    <script src="https://unpkg.com/juris@0.9.0/headless/juris-fluentstate.js"></script>
</head>
<body>
    <div id="app"></div>
    <script>
        const Counter = (props, context) => {
            const { count, user } = context.fluentState.getFluentStates();
            
            // Initialize state - note: primitives must be object properties
            count.val = count.val || 0;
            user.name = user.name || 'Guest';
            
            return {
                div: {
                    children: [
                        {
                            h1: { text: () => `Hello, ${user.name}!` }
                        },
                        {
                            div: {
                                children: [
                                    { span: { text: () => `Count: ${count.val}` } },
                                    {
                                        button: {
                                            text: 'Increment',
                                            onclick: () => count.val++
                                        }
                                    },
                                    {
                                        button: {
                                            text: 'Reset',
                                            onclick: () => count.val = 0
                                        }
                                    }
                                ]
                            }
                        }
                    ]
                }
            };
        };

        const juris = new Juris({
            features: { headless: HeadlessManager },
            headlessComponents: {
                fluentState: {
                    fn: createFluentStateHeadless,
                    options: { autoInit: true }
                }
            },
            components: { Counter },
            layout: { Counter: {} }
        });

        juris.render();
    </script>
</body>
</html>

Core Concepts

Object-Only State Management

Important: FluentState only supports object-based state management. Primitive values (numbers, strings, booleans) cannot be stored directly as state roots. This is why counters and simple values must be wrapped in objects:

const Counter = (props, context) => {
    const { count, user, settings } = context.fluentState.getFluentStates();
    
    // ✅ Correct - primitive values wrapped in objects
    count.val = count.val || 0;          // or count.value = 0
    count.step = 1;                      // can add more properties later
    user.name = user.name || 'Guest';    // strings work as object properties
    user.isActive = true;                // booleans work as object properties
    settings.theme = 'dark';             // strings as properties
    
    // ❌ Incorrect - cannot assign primitives directly to state roots
    // count = 5;        // This won't work
    // user = "John";    // This won't work
    // settings = true;  // This won't work
    
    return {
        div: {
            children: [
                { span: { text: () => `Count: ${count.val}` } },
                { span: { text: () => `Step: ${count.step}` } },
                { button: { text: 'Increment', onclick: () => count.val += count.step } }
            ]
        }
    };
};

Why Object-Only?

This design provides several benefits:

  • Consistent API: All state operations follow the same object-property pattern
  • Extensibility: Simple values can easily be extended with metadata
  • Performance: Proxy operations are optimized for object structures
  • Auto-Creation: Objects can be automatically created when accessing nested paths
// Example showing how simple values can be extended
const { counter, settings } = context.fluentState.getFluentStates();

// Start simple
counter.value = 0;

// Later, easily extend with metadata
counter.step = 1;
counter.min = 0;
counter.max = 100;
counter.lastUpdated = new Date();

// Settings can grow organically
settings.theme = 'dark';
settings.notifications = { email: true, push: false };
settings.user = { preferences: { autoSave: true } };

Direct State Access

FluentState provides direct access to state properties through destructuring:

const MyComponent = (props, context) => {
    const { user, todos, settings } = context.fluentState.getFluentStates();
    
    // Direct assignment - creates state automatically
    user.name = 'John';
    user.age = 25;
    todos.splice(0); // Initialize as empty array
    settings.theme = 'dark';
    settings.notifications = true;
    
    // Natural property access
    const userName = user.name;
    const todoCount = todos.length;
    
    // Increment operations
    user.age++;
    
    return {
        div: {
            text: () => `${user.name} has ${todos.length} todos`
        }
    };
};

Non-Reactive Mode

Access and modify state without triggering reactivity using the .x property:

const DataProcessor = (props, context) => {
    const { largeDataset, processedData, ui } = context.fluentState.getFluentStates();
    
    const processData = () => {
        // Non-reactive access - won't trigger re-renders
        const rawData = largeDataset.x.raw();
        const processed = rawData.map(item => ({
            ...item,
            processed: true
        }));
        
        // Update state reactively when done
        processedData.items = processed;
        ui.processing = false;
    };
    
    return {
        button: {
            text: 'Process Data',
            onclick: () => {
                ui.processing = true;
                setTimeout(processData, 100);
            }
        }
    };
};

Auto-Creation and Lazy Loading

FluentState automatically creates object and array structures as needed:

const SmartCreation = (props, context) => {
    const { app, todos, analytics } = context.fluentState.getFluentStates();
    
    // Auto-creates nested structure
    app.user.preferences.theme = 'dark';
    
    // Auto-creates as array when array methods are used
    todos.push({ id: 1, text: 'Learn FluentState', done: false });
    todos.push({ id: 2, text: 'Build awesome apps', done: false });
    
    // Auto-creates parent objects when setting deep properties
    analytics.events.pageViews.today = 42;
    
    return {
        div: {
            children: [
                { div: { text: () => `Theme: ${app.user.preferences.theme}` } },
                { div: { text: () => `Todos: ${todos.length}` } },
                { div: { text: () => `Page views: ${analytics.events.pageViews.today}` } }
            ]
        }
    };
};

Subscription System

Watch for Changes

const SubscriptionExample = (props, context) => {
    const { user, messages } = context.fluentState.getFluentStates();
    
    // Initialize data
    user.name = 'John';
    user.status = 'online';
    messages.splice(0); // Initialize as empty array
    
    // Subscribe to user status changes
    user.watch((newUser, oldUser, changedPath) => {
        console.log(`User changed: ${changedPath}`, newUser);
        
        if (newUser.status !== oldUser?.status) {
            messages.push({
                id: Date.now(),
                text: `Status changed to: ${newUser.status}`,
                timestamp: new Date()
            });
        }
    }, { deep: true });
    
    // Subscribe to specific property
    user.status.onChange((newStatus, oldStatus) => {
        document.title = `App - User is ${newStatus}`;
    });
    
    return {
        div: {
            children: [
                {
                    div: {
                        children: [
                            { span: { text: () => `${user.name} is ` } },
                            { 
                                span: { 
                                    text: () => user.status,
                                    style: () => ({
                                        color: user.status === 'online' ? 'green' : 'red'
                                    })
                                }
                            }
                        ]
                    }
                },
                {
                    div: {
                        children: [
                            {
                                button: {
                                    text: 'Go Online',
                                    onclick: () => user.status = 'online'
                                }
                            },
                            {
                                button: {
                                    text: 'Go Offline',
                                    onclick: () => user.status = 'offline'
                                }
                            }
                        ]
                    }
                },
                {
                    ul: {
                        children: () => messages.map(msg => ({
                            li: { 
                                text: `${msg.timestamp.toLocaleTimeString()}: ${msg.text}`,
                                key: msg.id
                            }
                        }))
                    }
                }
            ]
        }
    };
};

Subscription Options

const AdvancedSubscriptions = (props, context) => {
    const { data } = context.fluentState.getFluentStates();
    
    data.items = [];
    data.loading = false;
    
    // Immediate callback
    data.subscribe((newData) => {
        console.log('Data changed:', newData);
    }, { immediate: true });
    
    // One-time subscription
    data.loading.onChange((loading) => {
        if (!loading) {
            console.log('Loading completed!');
        }
    }, { once: true });
    
    // Deep watching (default)
    data.watch((dataObj, oldData, changedPath) => {
        console.log(`Deep change at ${changedPath}:`, dataObj);
    }, { deep: true });
    
    // Unsubscribe manually
    const unsubscribe = data.items.onChange((items) => {
        console.log(`Items count: ${items.length}`);
    });
    
    // Later... unsubscribe()
    
    return {
        div: { text: 'Check console for subscription logs' }
    };
};

Batch Operations

Efficiently handle multiple state changes:

const BatchExample = (props, context) => {
    const { stats, ui, lastUpdate, batch } = context.fluentState;
    
    // Initialize with object properties for primitives
    stats.views = 0;
    stats.clicks = 0;
    stats.conversions = 0;
    ui.updating = false;
    
    const updateAllStats = () => {
        ui.updating = true;
        
        // Batch multiple updates for optimal performance
        batch(() => {
            stats.views += 100;
            stats.clicks += 25;
            stats.conversions += 5;
            lastUpdate.time = new Date().toISOString();
        });
        
        ui.updating = false;
    };
    
    return {
        div: {
            children: [
                {
                    div: {
                        children: [
                            { div: { text: () => `Views: ${stats.views}` } },
                            { div: { text: () => `Clicks: ${stats.clicks}` } },
                            { div: { text: () => `Conversions: ${stats.conversions}` } },
                            { div: { text: () => `Last Update: ${lastUpdate.time || 'Never'}` } }
                        ]
                    }
                },
                {
                    button: {
                        text: () => ui.updating ? 'Updating...' : 'Update Stats',
                        disabled: () => ui.updating,
                        onclick: updateAllStats
                    }
                }
            ]
        }
    };
};

Array Operations

FluentState provides natural array manipulation with object-based state:

const TodoList = (props, context) => {
    const { todos, newTodo, stats } = context.fluentState.getFluentStates();
    
    // Initialize - remember to use object properties for primitives
    todos.splice(0); // Initialize as empty array
    newTodo.text = '';
    stats.total = 0;
    stats.completed = 0;
    
    const addTodo = () => {
        if (newTodo.text.trim()) {
            todos.push({
                id: Date.now(),
                text: newTodo.text.trim(),
                completed: false,
                createdAt: new Date()
            });
            newTodo.text = '';
            stats.total++;
        }
    };
    
    const toggleTodo = (id) => {
        const todo = todos.find(t => t.id === id);
        if (todo) {
            const wasCompleted = todo.completed;
            todo.completed = !todo.completed;
            
            // Update stats using object properties
            if (wasCompleted && !todo.completed) {
                stats.completed--;
            } else if (!wasCompleted && todo.completed) {
                stats.completed++;
            }
        }
    };
    
    const removeTodo = (id) => {
        const index = todos.findIndex(t => t.id === id);
        if (index !== -1) {
            const wasCompleted = todos[index].completed;
            todos.splice(index, 1);
            stats.total--;
            if (wasCompleted) stats.completed--;
        }
    };
    
    return {
        div: {
            class: 'todo-app',
            children: [
                {
                    div: {
                        class: 'todo-input',
                        children: [
                            {
                                input: {
                                    type: 'text',
                                    placeholder: 'Enter a todo...',
                                    value: () => newTodo.text,
                                    oninput: (e) => newTodo.text = e.target.value,
                                    onkeypress: (e) => e.key === 'Enter' && addTodo()
                                }
                            },
                            {
                                button: {
                                    text: 'Add',
                                    onclick: addTodo,
                                    disabled: () => !newTodo.text.trim()
                                }
                            }
                        ]
                    }
                },
                {
                    div: {
                        class: 'todo-stats',
                        children: [
                            { span: { text: () => `Total: ${stats.total}` } },
                            { span: { text: () => `Completed: ${stats.completed}` } },
                            { span: { text: () => `Remaining: ${stats.total - stats.completed}` } }
                        ]
                    }
                },
                {
                    ul: {
                        class: 'todo-list',
                        children: () => todos.map(todo => ({
                            li: {
                                class: todo.completed ? 'todo-item completed' : 'todo-item',
                                children: [
                                    {
                                        input: {
                                            type: 'checkbox',
                                            checked: todo.completed,
                                            onchange: () => toggleTodo(todo.id)
                                        }
                                    },
                                    {
                                        span: {
                                            class: 'todo-text',
                                            text: todo.text
                                        }
                                    },
                                    {
                                        button: {
                                            class: 'remove-btn',
                                            text: '×',
                                            onclick: () => removeTodo(todo.id)
                                        }
                                    }
                                ]
                            }
                        }))
                    }
                }
            ]
        }
    };
};

Utility Methods

Exists and Raw Access

const UtilityExample = (props, context) => {
    const { user, settings, tempData } = context.fluentState.getFluentStates();
    
    const checkData = () => {
        // Check if state exists
        if (user.exists()) {
            console.log('User exists');
        }
        
        if (!settings.exists()) {
            settings.theme = 'default';
        }
        
        // Get raw state value
        const rawUser = user.raw();
        console.log('Raw user data:', rawUser);
        
        // Clear specific state
        tempData.clear();
    };
    
    return {
        div: {
            children: [
                { div: { text: () => `User exists: ${user.exists()}` } },
                { div: { text: () => `Settings exist: ${settings.exists()}` } },
                {
                    button: {
                        text: 'Check Data',
                        onclick: checkData
                    }
                }
            ]
        }
    };
};

Update Operations

const UpdateExample = (props, context) => {
    const { user } = context.fluentState.getFluentStates();
    
    user.name = 'John';
    user.age = 25;
    user.email = 'john@example.com';
    
    const updateUser = () => {
        // Merge update with existing data
        user.update({
            age: 26,
            lastLogin: new Date().toISOString(),
            preferences: { theme: 'dark' }
        });
    };
    
    return {
        div: {
            children: [
                { div: { text: () => `Name: ${user.name}` } },
                { div: { text: () => `Age: ${user.age}` } },
                { div: { text: () => `Email: ${user.email}` } },
                { div: { text: () => `Last Login: ${user.lastLogin || 'Never'}` } },
                {
                    button: {
                        text: 'Update User',
                        onclick: updateUser
                    }
                }
            ]
        }
    };
};

Advanced Patterns

Computed Properties

const ComputedExample = (props, context) => {
    const { cart, tax } = context.fluentState.getFluentStates();
    
    cart.items = [];
    tax.rate = 0.08;
    
    // Computed properties using reactive functions
    const computedValues = {
        subtotal: () => cart.items.reduce((sum, item) => sum + (item.price * item.quantity), 0),
        taxAmount: () => computedValues.subtotal() * tax.rate,
        total: () => computedValues.subtotal() + computedValues.taxAmount()
    };
    
    const addItem = () => {
        cart.items.push({
            id: Date.now(),
            name: 'Sample Item',
            price: Math.floor(Math.random() * 100) + 1,
            quantity: 1
        });
    };
    
    return {
        div: {
            children: [
                {
                    div: {
                        children: [
                            { div: { text: () => `Items: ${cart.items.length}` } },
                            { div: { text: () => `Subtotal: $${computedValues.subtotal().toFixed(2)}` } },
                            { div: { text: () => `Tax: $${computedValues.taxAmount().toFixed(2)}` } },
                            { div: { text: () => `Total: $${computedValues.total().toFixed(2)}` } }
                        ]
                    }
                },
                {
                    button: {
                        text: 'Add Random Item',
                        onclick: addItem
                    }
                },
                {
                    ul: {
                        children: () => cart.items.map(item => ({
                            li: {
                                text: `${item.name} - $${item.price} x ${item.quantity}`,
                                key: item.id
                            }
                        }))
                    }
                }
            ]
        }
    };
};

State Persistence

const PersistentState = (props, context) => {
    const { user, preferences, lastSaved } = context.fluentState.getFluentStates();
    
    // Load from localStorage on initialization
    const loadFromStorage = () => {
        const saved = localStorage.getItem('appState');
        if (saved) {
            try {
                const data = JSON.parse(saved);
                Object.assign(user, data.user || {});
                Object.assign(preferences, data.preferences || {});
                lastSaved.time = data.lastSaved;
            } catch (e) {
                console.error('Failed to load state from storage:', e);
            }
        }
    };
    
    // Save to localStorage
    const saveToStorage = () => {
        try {
            const stateToSave = {
                user: user.raw(),
                preferences: preferences.raw(),
                lastSaved: new Date().toISOString()
            };
            localStorage.setItem('appState', JSON.stringify(stateToSave));
            lastSaved.time = stateToSave.lastSaved;
        } catch (e) {
            console.error('Failed to save state to storage:', e);
        }
    };
    
    // Auto-save on changes
    user.watch(() => saveToStorage(), { deep: true });
    preferences.watch(() => saveToStorage(), { deep: true });
    
    // Initialize
    if (!user.exists()) {
        loadFromStorage();
        if (!user.exists()) {
            user.name = 'New User';
            preferences.theme = 'light';
            preferences.autoSave = true;
        }
    }
    
    return {
        div: {
            children: [
                { div: { text: () => `User: ${user.name}` } },
                { div: { text: () => `Theme: ${preferences.theme}` } },
                { div: { text: () => `Last Saved: ${lastSaved.time || 'Never'}` } },
                {
                    input: {
                        type: 'text',
                        placeholder: 'Change user name...',
                        oninput: (e) => user.name = e.target.value
                    }
                },
                {
                    button: {
                        text: 'Clear Storage',
                        onclick: () => {
                            localStorage.removeItem('appState');
                            location.reload();
                        }
                    }
                }
            ]
        }
    };
};

Configuration and Debug

Debug Information

const DebugComponent = (props, context) => {
    const { debug } = context.fluentState;
    
    const showStats = () => {
        const stats = debug.getStats();
        console.log('FluentState Stats:', stats);
        alert(`Subscriptions: ${stats.subscriptions}, Cache: ${stats.cache}`);
    };
    
    return {
        div: {
            children: [
                {
                    button: {
                        text: 'Show Debug Stats',
                        onclick: showStats
                    }
                },
                {
                    button: {
                        text: 'Clear Cache',
                        onclick: debug.clearCache
                    }
                },
                {
                    button: {
                        text: 'Clear Subscriptions',
                        onclick: debug.clearSubscriptions
                    }
                }
            ]
        }
    };
};

Best Practices

1. Initialize State Early with Object Properties

const BestPracticeInit = (props, context) => {
    const { app, user, ui, data } = context.fluentState.getFluentStates();
    
    // Initialize state structure early - use object properties for primitives
    app.version = '1.0.0';
    app.name = 'MyApp';
    user.profile = null;
    ui.loading = false;
    ui.error = null;
    data.items = [];
    
    return { div: { text: 'State initialized' } };
};

2. Use Batch for Multiple Updates

const BestPracticeBatch = (props, context) => {
    const { ui, stats, lastUpdate, batch } = context.fluentState;
    
    const updateMultiple = () => {
        batch(() => {
            ui.loading = true;
            stats.requests = (stats.requests || 0) + 1;
            lastUpdate.timestamp = Date.now();
        });
    };
    
    return {
        button: { text: 'Batch Update', onclick: updateMultiple }
    };
};

3. Leverage Non-Reactive Mode

const BestPracticeNonReactive = (props, context) => {
    const { largeDataset, processedData } = context.fluentState.getFluentStates();
    
    const processLargeData = () => {
        // Use .x for non-reactive access during processing
        const data = largeDataset.x.raw();
        const processed = data.map(item => ({ ...item, processed: true }));
        
        // Update reactively when done
        processedData.items = processed;
    };
    
    return {
        button: { text: 'Process Data', onclick: processLargeData }
    };
};

4. Clean Up Subscriptions

const BestPracticeCleanup = (props, context) => {
    const { data } = context.fluentState.getFluentStates();
    
    let unsubscribe;
    
    return {
        hooks: {
            onMount: () => {
                unsubscribe = data.watch((dataObj) => {
                    console.log('Data changed:', dataObj);
                });
            },
            onUnmount: () => {
                if (unsubscribe) unsubscribe();
            }
        },
        render: () => ({ div: { text: 'Component with cleanup' } })
    };
};

API Reference

Core Methods

  • context.fluentState.getFluentStates() - Returns destructurable state objects
  • state.x - Non-reactive proxy for accessing state without triggering subscriptions
  • batch(callback) - Execute multiple state changes in a single batch

Subscription Methods

  • state.watch(callback, options) - Subscribe to changes with deep watching
  • state.subscribe(callback, options) - Subscribe to changes
  • state.onChange(callback, options) - Subscribe to changes (alias for subscribe)
  • unsubscribe(id) - Remove specific subscription

Utility Methods

  • state.exists() - Check if state path exists
  • state.raw() - Get raw state value without proxy
  • state.clear() - Clear/reset state path
  • state.update(object) - Merge update with existing state

Array Methods

Standard JavaScript array methods work naturally:

  • stateArray.push(...items)
  • stateArray.pop()
  • stateArray.shift()
  • stateArray.unshift(...items)
  • stateArray.splice(start, deleteCount, ...items)

Debug Methods

  • debug.getStats() - Get statistics about subscriptions and cache
  • debug.clearCache() - Clear internal cache
  • debug.clearSubscriptions() - Clear all subscriptions

Troubleshooting

Common Issues

State not updating: Ensure you're using object properties, not direct primitive assignment

Performance issues: Use batch() for multiple updates and .x for heavy processing

Memory leaks: Clean up subscriptions in component lifecycle hooks

Primitive values: Remember that counters need count.val = 0 not count = 0

Debug Mode

FluentState provides comprehensive logging. Check browser console for detailed state operation logs.

Framework Integration

FluentState is specifically designed for Juris's architecture:

  • Object-First: Works seamlessly with Juris's object-based components
  • Temporal Independence: State persists across component lifecycles
  • Reactive Integration: Automatic UI updates through Juris's reactive system
  • Progressive Enhancement: Can enhance existing applications without breaking changes

Resources

License

MIT License - Part of the Juris JavaScript Unified Reactive Interface Solution

Clone this wiki locally