Skip to content

Juris FluentState Plugin Developer's Guide

jurisauthor edited this page Aug 24, 2025 · 2 revisions

A powerful, intuitive reactive state management system for Juris applications. FluentState provides a fluent proxy-based API that makes working with state feel natural and effortless.

Table of Contents

  1. Quick Setup
  2. Core Concepts
  3. Usage Patterns
  4. Reactivity
  5. Advanced Features
  6. Best Practices
  7. API Reference
  8. Complete Examples

Quick Setup

Simple Configuration with Base State

FluentState integrates seamlessly with Juris through the headless component system. Always define your base global state structure in the Juris config:

const juris = new Juris({
    // ✅ Define base state structure here
    states: {
        user: {
            name: 'Guest',
            email: '',
            profile: {
                age: null,
                preferences: {
                    theme: 'light',
                    notifications: true
                }
            }
        },
        todos: {
            items: [],
            filter: 'all',
            newTodo: ''
        },
        ui: {
            loading: false,
            modals: {
                settings: { visible: false },
                help: { visible: false }
            },
            sidebar: { collapsed: false }
        },
        config: {
            version: '1.0.0',
            debugMode: false,
            api: {
                baseUrl: 'https://api.example.com',
                timeout: 5000
            }
        }
    },
    features: { headless: HeadlessManager },
    headlessComponents: {
        fluentState: {
            fn: createFluentStateHeadless,
            options: { autoInit: true }
        }
    },
    components: { /* your components */ },
    layout: { /* your layout */ }
});

Why Define Base State in Config?

1. Single Source of Truth

// ✅ Good - state structure is clear and centralized
const juris = new Juris({
    states: {
        app: {
            initialized: false,
            version: '1.0.0'
        },
        user: {
            authenticated: false,
            profile: { name: '', email: '' }
        }
    },
    // ... rest of config
});

// Components just use the pre-defined structure
const MyComponent = (props, context) => {
    const { app, user } = context.fluentState.getFluentStates();
    
    // State is already properly structured
    return {
        div: { text: () => `${user.profile.name} - v${app.version}` }
    };
};

2. Avoid Scattered Initialization

// ❌ Bad - state initialization scattered across components
const ComponentA = (props, context) => {
    const { user } = context.fluentState.getFluentStates();
    user.profile = user.profile || {}; // Initializing here
    user.profile.preferences = user.profile.preferences || {}; // And here
    // ...
};

const ComponentB = (props, context) => {
    const { user } = context.fluentState.getFluentStates();
    user.settings = user.settings || {}; // Different component initializing different parts
    // ...
};

// ✅ Good - all initialization in config
const juris = new Juris({
    states: {
        user: {
            profile: {
                preferences: { theme: 'light', lang: 'en' }
            },
            settings: { notifications: true }
        }
    }
    // ...
});

Component Access

With base state defined in config, components become much cleaner:

const UserProfile = (props, context) => {
    const { user } = context.fluentState.getFluentStates();
    
    // No initialization needed - state structure already exists
    return {
        div: { class: 'user-profile',
            children: [
                { input: {
                    type: 'text',
                    value: () => user.profile.name,
                    oninput: (e) => user.profile.name = e.target.value
                }},
                { select: {
                    value: () => user.profile.preferences.theme,
                    onchange: (e) => user.profile.preferences.theme = e.target.value,
                    children: [
                        { option: { value: 'light', text: 'Light' } },
                        { option: { value: 'dark', text: 'Dark' } }
                    ]
                }}
            ]
        }
    };
};

Core Concepts

FluentState vs Props: Objects vs Primitives

Understanding the fundamental difference between FluentState and props is crucial for effective usage:

FluentState = Objects (References)

const MyComponent = (props, context) => {
    const { user, config } = context.fluentState.getFluentStates();
    
    // user and config are OBJECT REFERENCES
    // When you modify them, you're modifying the actual state object
    user.name = 'John';           // ✅ Modifies the state object
    user.profile.age = 25;        // ✅ Modifies nested object
    config.theme = 'dark';        // ✅ Modifies the state object
    
    // All components sharing this state see the changes immediately
    return {
        div: { text: () => user.name }  // Shows 'John'
    };
};

Props = Primitives (Values)

const ChildComponent = (props, context) => {
    // props contains PRIMITIVE VALUES (copies)
    console.log(props.userName);    // 'John' (a copy of the value)
    console.log(props.userAge);     // 25 (a copy of the value)
    
    // You CANNOT modify parent state through props
    props.userName = 'Jane';        // ❌ Only changes local copy, not parent state
    
    // To modify parent state, use FluentState
    const { user } = context.fluentState.getFluentStates();
    user.name = 'Jane';            // ✅ Modifies actual state
    
    return {
        div: { text: () => `Props: ${props.userName}, State: ${user.name}` }
    };
};

Why This Matters

1. State Mutations Work

const TodoApp = (props, context) => {
    const { todos } = context.fluentState.getFluentStates();
    
    todos.items = todos.items || [];
    
    // ✅ This works - you're modifying the state object
    const addTodo = (text) => {
        todos.items.push({ id: Date.now(), text, done: false });
    };
    
    // ✅ This works - you're modifying array elements
    const toggleTodo = (index) => {
        todos.items[index].done = !todos.items[index].done;
    };
    
    return {
        div: {
            children: [
                () => todos.items.map((todo, index) => ({
                    div: {
                        key: todo.id,
                        children: [
                            { span: { text: todo.text } },
                            { button: {
                                text: 'Toggle',
                                onclick: () => toggleTodo(index)
                            }}
                        ]
                    }
                }))
            ]
        }
    };
};

2. Props Are Read-Only Values

const UserCard = (props, context) => {
    // props.user is a COPY of primitive values, not the object reference
    const userName = props.user?.name || 'Unknown';
    const userAge = props.user?.age || 0;
    
    // ❌ This doesn't work - props are copies
    const updateNameWrong = () => {
        props.user.name = 'New Name';  // Only changes local copy
    };
    
    // ✅ This works - access state directly
    const updateNameRight = () => {
        const { user } = context.fluentState.getFluentStates();
        user.name = 'New Name';        // Modifies actual state
    };
    
    return {
        div: {
            children: [
                { p: { text: `Name: ${userName}` } },
                { button: { text: 'Update (Wrong)', onclick: updateNameWrong } },
                { button: { text: 'Update (Right)', onclick: updateNameRight } }
            ]
        }
    };
};

3. Sharing State Between Components

// Parent Component
const App = (props, context) => {
    const { shared } = context.fluentState.getFluentStates();
    
    // Initialize shared state
    shared.counter = shared.counter || 0;
    shared.user = shared.user || { name: 'Guest' };
    
    return {
        div: {
            children: [
                // Both components access the SAME state objects
                { ComponentA: {} },
                { ComponentB: {} }
            ]
        }
    };
};

// Component A
const ComponentA = (props, context) => {
    const { shared } = context.fluentState.getFluentStates();
    
    return {
        div: {
            children: [
                { p: { text: () => `A sees: ${shared.counter}` } },
                { button: {
                    text: 'A increment',
                    onclick: () => shared.counter++  // Modifies shared object
                }}
            ]
        }
    };
};

// Component B  
const ComponentB = (props, context) => {
    const { shared } = context.fluentState.getFluentStates();
    
    return {
        div: {
            children: [
                // Sees the SAME counter that Component A modifies
                { p: { text: () => `B sees: ${shared.counter}` } },
                { button: {
                    text: 'B increment', 
                    onclick: () => shared.counter++  // Modifies same shared object
                }}
            ]
        }
    };
};

Mental Model

Think of FluentState as shared objects in memory:

// This is conceptually what FluentState does:
const globalStateObjects = {
    user: { name: 'John', age: 25 },
    todos: { items: [], filter: 'all' },
    ui: { theme: 'dark', loading: false }
};

// Every component gets references to the same objects:
const ComponentA = () => {
    const user = globalStateObjects.user;     // Reference to same object
    const todos = globalStateObjects.todos;   // Reference to same object
    
    user.name = 'Jane';    // All components see this change
    todos.items.push(...); // All components see this change
};

const ComponentB = () => {
    const user = globalStateObjects.user;     // Same object reference
    console.log(user.name); // 'Jane' - sees ComponentA's change
};

Common Mistakes to Avoid

❌ Treating FluentState Like Props

// Wrong - thinking you need to "pass down" state
const Parent = (props, context) => {
    const { user } = context.fluentState.getFluentStates();
    
    return {
        // ❌ Don't do this - Child can access user directly
        Child: { userName: user.name, userAge: user.age }
    };
};

// ✅ Correct - access state directly where needed
const Child = (props, context) => {
    const { user } = context.fluentState.getFluentStates();
    
    return {
        div: { text: () => user.name }  // Direct access to state
    };
};

❌ Trying to Modify Props

const Component = (props, context) => {
    // ❌ This won't work - props are primitive copies
    const updateUser = () => {
        props.user.name = 'New Name';
    };
    
    // ✅ Use FluentState for modifications
    const updateUserCorrect = () => {
        const { user } = context.fluentState.getFluentStates();
        user.name = 'New Name';
    };
};

Pattern 1: Root Proxy ($)

const { $ } = context.fluentState.getFluentStates();

$.counter = 0;
$.user.name = 'John';
$.app.settings.theme = 'dark';

Pattern 2: Destructured Paths

const { user, counter, settings } = context.fluentState.getFluentStates();

counter.value = 0;
user.name = 'John';
settings.theme = 'dark';

Pattern 3: Mixed Approach

const { $, user, counter } = context.fluentState.getFluentStates();

// Use destructured paths for frequently accessed state
user.profile = { name: 'John', email: 'john@example.com' };
counter.clicks = 0;

// Use $ for occasional or global state
$.lastActivity = Date.now();
$.debugMode = false;

State Access Patterns

FluentState offers flexible ways to access and organize your state:

FluentState automatically creates nested object structures as you access them:

const { data } = context.fluentState.getFluentStates();

// All intermediate paths are created automatically
data.user.profile.preferences.notifications.email = true;

// Results in:
// {
//   user: {
//     profile: {
//       preferences: {
//         notifications: {
//           email: true
//         }
//       }
//     }
//   }
// }

Usage Patterns

Basic State Operations

const ProfileComponent = (props, context) => {
    const { user } = context.fluentState.getFluentStates();
    
    // Initialize state (safe to do repeatedly)
    user.name = user.name || 'Anonymous';
    user.email = user.email || '';
    
    return {
        div: { class: 'profile',
            children: [
                { input: {
                    type: 'text',
                    value: () => user.name,
                    oninput: (e) => user.name = e.target.value
                }},
                { input: {
                    type: 'email',
                    value: () => user.email,
                    oninput: (e) => user.email = e.target.value
                }},
                { p: { text: () => `Welcome, ${user.name}!` } }
            ]
        }
    };
};

Working with Arrays

const TodoList = (props, context) => {
    const { todos } = context.fluentState.getFluentStates();
    
    // Initialize array
    todos.items = todos.items || [];
    
    const addTodo = (text) => {
        todos.items.push({
            id: Date.now(),
            text: text,
            completed: false
        });
    };
    
    return {
        div: {
            children: [
                { button: {
                    text: 'Add Todo',
                    onclick: () => addTodo('New task')
                }},
                () => todos.items.map(todo => ({
                    div: {
                        key: todo.id,
                        text: todo.text,
                        onclick: () => todo.completed = !todo.completed
                    }
                }))
            ]
        }
    };
};

State Initialization Best Practices

✅ Define Base State in Juris Config

const juris = new Juris({
    states: {
        // Application state
        app: {
            initialized: true,
            version: '1.0.0',
            startTime: Date.now()
        },
        
        // User state with complete structure
        user: {
            authenticated: false,
            profile: {
                name: 'Guest',
                email: '',
                avatar: null
            },
            preferences: {
                theme: 'light',
                language: 'en',
                notifications: {
                    email: true,
                    push: false
                }
            }
        },
        
        // Feature-specific state
        todos: {
            items: [],
            filter: 'all',
            stats: {
                total: 0,
                completed: 0,
                remaining: 0
            }
        },
        
        // UI state
        ui: {
            loading: false,
            errors: [],
            modals: {
                settings: { visible: false },
                confirm: { visible: false, message: '' }
            }
        }
    },
    // ... rest of config
});

// Components are now clean and predictable
const TodoApp = (props, context) => {
    const { todos, ui } = context.fluentState.getFluentStates();
    
    // No initialization needed - structure already exists
    const addTodo = (text) => {
        todos.items.push({
            id: Date.now(),
            text: text.trim(),
            completed: false
        });
        // Update stats
        todos.stats.total++;
        todos.stats.remaining++;
    };
    
    return {
        div: { class: 'todo-app',
            children: [
                // All paths are guaranteed to exist
                { p: { text: () => `${todos.stats.remaining} remaining` } },
                { div: {
                    class: () => ui.loading ? 'loading' : '',
                    children: [
                        () => todos.items.map(todo => ({ /* todo item */ }))
                    ]
                }}
            ]
        }
    };
};

❌ Avoid Component-Level Initialization

// Don't do this - scattered initialization
const BadComponent = (props, context) => {
    const { todos, user } = context.fluentState.getFluentStates();
    
    // ❌ Initialization mixed with component logic
    todos.items = todos.items || [];
    todos.filter = todos.filter || 'all';
    user.profile = user.profile || {};
    user.profile.preferences = user.profile.preferences || {};
    
    // Component logic becomes unclear
    return { /* component */ };
};

// ✅ Good - clean component that just uses state
const GoodComponent = (props, context) => {
    const { todos, user } = context.fluentState.getFluentStates();
    
    // State structure is guaranteed - just use it
    return {
        div: {
            text: () => `${user.profile.name}: ${todos.items.length} todos`
        }
    };
};

Advanced State Organization

For larger applications, organize state by feature domains:

const juris = new Juris({
    states: {
        // Authentication domain
        auth: {
            user: null,
            token: null,
            refreshToken: null,
            loginAttempts: 0,
            session: {
                expiresAt: null,
                lastActivity: null
            }
        },
        
        // Data domains
        products: {
            items: [],
            categories: [],
            filters: {
                category: 'all',
                priceRange: { min: 0, max: 1000 },
                sortBy: 'name'
            },
            pagination: {
                page: 1,
                limit: 20,
                total: 0
            }
        },
        
        // Shopping cart domain
        cart: {
            items: [],
            totals: {
                subtotal: 0,
                tax: 0,
                shipping: 0,
                total: 0
            },
            checkout: {
                step: 1,
                shippingAddress: null,
                paymentMethod: null
            }
        },
        
        // UI/UX domain
        ui: {
            theme: 'light',
            layout: 'grid',
            modals: {
                productDetail: { visible: false, productId: null },
                cart: { visible: false },
                checkout: { visible: false }
            },
            notifications: {
                items: [],
                unreadCount: 0
            }
        },
        
        // Application meta
        app: {
            version: '2.1.0',
            environment: 'production',
            features: {
                darkMode: true,
                notifications: true,
                analytics: false
            },
            api: {
                baseUrl: 'https://api.myapp.com',
                timeout: 30000,
                retries: 3
            }
        }
    },
    // ... rest of config
});

Reactivity

How Juris Reactivity Actually Works

Unlike traditional frameworks, Juris doesn't re-render components. Instead, it uses automatic dependency tracking and fine-grained reactivity:

  • Reactive functions (like () => user.name) automatically update when their dependencies change
  • No component re-rendering - only specific reactive expressions update
  • Automatic tracking - Juris tracks which state paths each reactive function depends on
  • Precise updates - only the exact DOM elements that need to change are updated

Reactive Functions

Any function in your component template becomes reactive and updates automatically:

const Counter = (props, context) => {
    const { counter } = context.fluentState.getFluentStates();
    
    counter.value = counter.value || 0;
    
    return {
        div: {
            children: [
                // This function automatically updates when counter.value changes
                { h2: { text: () => `Count: ${counter.value}` } },
                { button: {
                    text: 'Increment',
                    onclick: () => counter.value++
                }},
                // This function is independent and only updates when counter.doubled changes
                { p: { text: () => `Doubled: ${counter.doubled || counter.value * 2}` } },
                // Conditional reactive function
                { p: { 
                    text: () => counter.value > 10 ? 'High count!' : 'Low count'
                }}
            ]
        }
    };
};

Fine-Grained Updates

Each reactive function tracks its own dependencies and updates independently:

const UserProfile = (props, context) => {
    const { user, stats } = context.fluentState.getFluentStates();
    
    user.name = user.name || 'Guest';
    stats.visits = stats.visits || 0;
    
    return {
        div: {
            children: [
                // Only updates when user.name changes
                { h1: { text: () => `Welcome, ${user.name}!` } },
                
                // Only updates when stats.visits changes
                { p: { text: () => `Visits: ${stats.visits}` } },
                
                // Updates when either user.name OR stats.visits changes
                { p: { 
                    text: () => `${user.name} has visited ${stats.visits} times`
                }},
                
                // Static elements never update
                { button: { text: 'Static Button' } }
            ]
        }
    };
};

Computed Reactive Values

Create computed values that automatically recalculate:

const ShoppingCart = (props, context) => {
    const { cart } = context.fluentState.getFluentStates();
    
    cart.items = cart.items || [];
    
    return {
        div: {
            children: [
                // This computation runs only when cart.items changes
                { p: {
                    text: () => {
                        const total = cart.items.reduce((sum, item) => 
                            sum + (item.price * item.quantity), 0
                        );
                        return `Total: ${total.toFixed(2)}`;
                    }
                }},
                
                // Independent computation
                { p: {
                    text: () => {
                        const itemCount = cart.items.length;
                        return `${itemCount} item${itemCount !== 1 ? 's' : ''} in cart`;
                    }
                }},
                
                // Reactive list - each item updates independently
                () => cart.items.map(item => ({
                    div: {
                        key: item.id,
                        children: [
                            // Only updates when this specific item.name changes
                            { span: { text: () => item.name } },
                            // Only updates when this specific item.quantity changes  
                            { span: { text: () => `Qty: ${item.quantity}` } }
                        ]
                    }
                }))
            ]
        }
    };
};

Cross-Component Reactivity

State changes automatically update reactive functions across different components:

const Header = (props, context) => {
    const { user } = context.fluentState.getFluentStates();
    
    return {
        header: {
            children: [
                // This reactive function updates when user.name changes
                { span: { text: () => `Logged in as: ${user.name || 'Guest'}` } },
                { button: {
                    text: 'Login',
                    onclick: () => user.name = 'John Doe'
                }}
            ]
        }
    };
};

const Sidebar = (props, context) => {
    const { user } = context.fluentState.getFluentStates();
    
    return {
        aside: {
            // This reactive function automatically updates when user.name 
            // changes in the Header component - no component re-rendering needed
            text: () => user.name ? `Welcome ${user.name}` : 'Please log in'
        }
    };
};

Advanced Features

Non-Reactive Access

Use .x to read/write state without creating reactive dependencies:

const AnalyticsComponent = (props, context) => {
    const { analytics, ui } = context.fluentState.getFluentStates();
    
    const trackEvent = (eventName) => {
        // These operations don't create reactive dependencies
        // No functions will be re-executed when these change
        analytics.x.events = analytics.x.events || [];
        analytics.x.events.push({
            name: eventName,
            timestamp: Date.now()
        });
        analytics.x.totalEvents = (analytics.x.totalEvents || 0) + 1;
    };
    
    return {
        div: {
            children: [
                // This IS reactive - function will re-execute when ui.eventCount changes
                { p: { text: () => `UI Event Count: ${ui.eventCount || 0}` } },
                
                { button: {
                    text: 'Track Event',
                    onclick: () => {
                        trackEvent('button_click');
                        // This creates reactive dependency - ui functions will update
                        ui.eventCount = (ui.eventCount || 0) + 1;
                    }
                }},
                
                // This reactive function accesses analytics normally
                // It WILL update when analytics.displayTotal changes
                { p: { text: () => `Display Total: ${analytics.displayTotal || 0}` } },
                
                { button: {
                    text: 'Update Display',
                    onclick: () => {
                        // Copy non-reactive data to reactive field
                        analytics.displayTotal = analytics.x.totalEvents || 0;
                    }
                }}
            ]
        }
    };
};

State Subscriptions

Subscribe to state changes for side effects that don't involve the UI:

const NotificationSystem = (props, context) => {
    const { user, notifications } = context.fluentState.getFluentStates();
    
    return {
        onMount: () => {
            // Subscribe to user changes for side effects
            const unsubscribe = user.subscribe((userData, oldData, changedPath) => {
                // This runs when user data changes, but doesn't affect reactive functions
                if (userData.name !== oldData?.name) {
                    // Log to analytics (non-reactive)
                    analytics.x.userChanges = analytics.x.userChanges || [];
                    analytics.x.userChanges.push({
                        from: oldData?.name,
                        to: userData.name,
                        timestamp: Date.now()
                    });
                    
                    // Update notifications (reactive - UI functions will update)
                    notifications.items = notifications.items || [];
                    notifications.items.push({
                        message: `Welcome ${userData.name}!`,
                        timestamp: Date.now()
                    });
                }
            });
            
            return unsubscribe;
        },
        
        render: () => ({
            div: {
                children: [
                    // This reactive function updates when notifications.items changes
                    { p: { text: () => `Notifications: ${notifications.items?.length || 0}` } },
                    
                    // This reactive function shows latest notification
                    () => {
                        const latest = notifications.items?.[notifications.items.length - 1];
                        return latest ? { p: { text: latest.message } } : null;
                    }
                ]
            }
        })
    };
};

Utility Methods

FluentState provides helpful utility methods:

const UtilityExample = (props, context) => {
    const { data } = context.fluentState.getFluentStates();
    
    return {
        div: {
            children: [
                { button: {
                    text: 'Check if exists',
                    onclick: () => {
                        if (data.user.exists()) {
                            console.log('User data exists');
                        }
                    }
                }},
                { button: {
                    text: 'Get raw value',
                    onclick: () => {
                        const rawData = data.raw();
                        console.log('Raw state:', rawData);
                    }
                }},
                { button: {
                    text: 'Clear data',
                    onclick: () => data.clear()
                }},
                { button: {
                    text: 'Update multiple',
                    onclick: () => {
                        data.user.update({
                            name: 'John',
                            email: 'john@example.com',
                            lastLogin: Date.now()
                        });
                    }
                }}
            ]
        }
    };
};

Best Practices

1. Always Define Base State in Juris Config

// ✅ Excellent - Complete state structure defined upfront
const juris = new Juris({
    states: {
        user: {
            authenticated: false,
            profile: { name: 'Guest', email: '' },
            preferences: { theme: 'light', notifications: true }
        },
        app: {
            initialized: true,
            loading: false,
            errors: []
        },
        todos: {
            items: [],
            filter: 'all',
            newTodo: ''
        }
    },
    // ... rest of config
});

// Components are clean and predictable
const UserComponent = (props, context) => {
    const { user } = context.fluentState.getFluentStates();
    
    // State structure is guaranteed - no initialization needed
    return {
        div: {
            // These paths are guaranteed to exist
            text: () => `${user.profile.name} (${user.preferences.theme} theme)`
        }
    };
};

// ❌ Avoid - Component-level initialization creates unpredictability
const BadUserComponent = (props, context) => {
    const { user } = context.fluentState.getFluentStates();
    
    // Don't do this - initialization scattered everywhere
    user.profile = user.profile || {};
    user.profile.name = user.profile.name || 'Guest';
    user.preferences = user.preferences || {};
    
    // Component logic is unclear and error-prone
    return { /* component */ };
};

2. Organize State by Domain

// Good: Clear domain separation for precise reactive dependencies
const { auth, products, cart, ui } = context.fluentState.getFluentStates();

// Reactive functions only depend on their specific domain:
// text: () => auth.user.name          // Only updates when auth changes
// text: () => products.items.length   // Only updates when products change  
// text: () => cart.totals.total       // Only updates when cart changes
// class: () => ui.theme               // Only updates when ui changes

// Less optimal: Generic organization
const { $, data, state } = context.fluentState.getFluentStates();

// Reactive functions may have broader dependencies:
// text: () => $.user.name    // Could be affected by any $ change

3. Use Reactive Functions Appropriately

const Component = (props, context) => {
    const { data, ui } = context.fluentState.getFluentStates();
    
    return {
        div: {
            children: [
                // Good: Minimal reactive dependencies
                { p: { text: () => `${data.items.length} items` } },
                
                // Good: Conditional reactive function
                () => ui.loading ? 
                    { div: { text: 'Loading...' } } : 
                    { div: { text: 'Ready' } },
                
                // Good: Static content (no reactive dependencies)
                { h1: { text: 'My App' } },
                
                // Be careful: This function depends on both data.items AND ui.theme
                { p: { 
                    text: () => `${data.items.length} items (${ui.theme} theme)`
                }}
            ]
        }
    };
};

4. Optimize Reactive Dependencies

// Good: Minimal reactive dependencies
const UserInfo = (props, context) => {
    const { user } = context.fluentState.getFluentStates();
    
    return {
        div: {
            children: [
                // This only updates when user.profile.name changes
                { h2: { text: () => user.profile.name } },
                
                // This only updates when user.profile.email changes
                { p: { text: () => user.profile.email } },
                
                // This updates when either user.profile.name OR user.profile.email changes
                { p: { text: () => `${user.profile.name} (${user.profile.email})` } }
            ]
        }
    };
};

// Less optimal: Broad reactive dependencies
const UserInfoBroad = (props, context) => {
    const { user } = context.fluentState.getFluentStates();
    
    return {
        div: {
            children: [
                // This updates whenever ANYTHING in user changes
                { div: { text: () => JSON.stringify(user.raw()) } }
            ]
        }
    };
};

5. Use Non-Reactive Mode for Side Effects

// Good: Use .x for data that shouldn't trigger reactive updates
const Analytics = (props, context) => {
    const { analytics, ui } = context.fluentState.getFluentStates();
    
    const trackEvent = (event) => {
        // This doesn't create reactive dependencies
        analytics.x.events = analytics.x.events || [];
        analytics.x.events.push({ event, timestamp: Date.now() });
        analytics.x.pageViews++;
    };
    
    return {
        div: {
            children: [
                // This creates reactive dependency on ui.displayCount
                { p: { text: () => `Display Count: ${ui.displayCount || 0}` } },
                
                { button: {
                    text: 'Track Event',
                    onclick: () => {
                        trackEvent('button_click');
                        // This triggers reactive functions to update
                        ui.displayCount = (ui.displayCount || 0) + 1;
                    }
                }}
            ]
        }
    };
};

6. Structure Components for Clear Reactive Boundaries

// Good: Clear separation of concerns with base state defined in config
const juris = new Juris({
    states: {
        todos: { items: [], stats: { total: 0, completed: 0 } },
        ui: { filter: 'all', theme: 'light' }
    }
    // ... rest of config
});

const TodoApp = (props, context) => {
    const { todos, ui } = context.fluentState.getFluentStates();
    
    return {
        div: {
            children: [
                // Header - only updates when todos.stats change
                { TodoHeader: {} },  // Accesses todos.stats directly
                
                // Main list - only updates when todos.items change  
                { TodoList: {} },    // Accesses todos.items directly
                
                // Footer - only updates when ui state changes
                { TodoFooter: {} }   // Accesses ui directly
            ]
        }
    };
};

const TodoHeader = (props, context) => {
    const { todos } = context.fluentState.getFluentStates();
    
    return {
        header: {
            // Only this function updates when todos.stats.completed changes
            text: () => `${todos.stats.completed} completed tasks`
        }
    };
};

API Reference

Core Access

context.fluentState.getFluentStates()

Returns a proxy object for accessing state with destructuring support.

Usage:

const { $, user, config } = context.fluentState.getFluentStates();

State Operations

Setting Values

user.name = 'John';                    // Simple assignment
user.profile.age = 25;                 // Nested assignment
$.globalCounter = 100;                 // Root proxy assignment

Getting Values

const name = user.name;                // Direct access
const age = user.profile.age;          // Nested access

Reactive Access

text: () => user.name                  // Reactive in component functions
text: () => `Hello ${user.name}!`      // Reactive with template

Non-Reactive Access

user.x.name = 'John';                  // Set without triggering re-renders
const name = user.x.name;              // Get without subscribing

Array Operations

items.push(newItem);                   // Add item
items.pop();                           // Remove last item
items.splice(index, 1);                // Remove at index
items.length = 0;                      // Clear array

Utility Methods

user.exists()                          // Check if path has value
user.raw()                             // Get raw JavaScript value
user.clear()                           // Set to null
user.update({ name: 'John' })          // Merge properties
user.subscribe(callback)               // Subscribe to changes

Complete Examples

Todo Application

const TodoApp = (props, context) => {
    const { todos } = context.fluentState.getFluentStates();
    
    // Initialize state
    todos.list = todos.list || [];
    todos.newTodo = todos.newTodo || '';
    todos.filter = todos.filter || 'all';
    
    const addTodo = () => {
        if (todos.newTodo.trim()) {
            todos.list.push({
                id: Date.now(),
                text: todos.newTodo.trim(),
                completed: false
            });
            todos.newTodo = '';
        }
    };
    
    const filteredTodos = () => {
        switch (todos.filter) {
            case 'active': return todos.list.filter(t => !t.completed);
            case 'completed': return todos.list.filter(t => t.completed);
            default: return todos.list;
        }
    };
    
    return {
        div: { class: 'todo-app',
            children: [
                { h1: { text: 'Todo List' } },
                
                // Add todo input
                { div: { class: 'add-todo',
                    children: [
                        { input: {
                            type: 'text',
                            value: () => todos.newTodo,
                            placeholder: 'What needs to be done?',
                            oninput: (e) => todos.newTodo = e.target.value,
                            onkeypress: (e) => e.key === 'Enter' && addTodo()
                        }},
                        { button: { text: 'Add', onclick: addTodo } }
                    ]
                }},
                
                // Filter buttons
                { div: { class: 'filters',
                    children: ['all', 'active', 'completed'].map(filter => ({
                        button: {
                            text: filter,
                            class: () => todos.filter === filter ? 'active' : '',
                            onclick: () => todos.filter = filter
                        }
                    }))
                }},
                
                // Todo list
                { div: { class: 'todo-list',
                    children: () => filteredTodos().map(todo => ({
                        div: { 
                            key: todo.id,
                            class: () => todo.completed ? 'completed' : '',
                            children: [
                                { input: {
                                    type: 'checkbox',
                                    checked: () => todo.completed,
                                    onchange: (e) => todo.completed = e.target.checked
                                }},
                                { span: { text: todo.text } },
                                { button: {
                                    text: '×',
                                    onclick: () => {
                                        const index = todos.list.findIndex(t => t.id === todo.id);
                                        todos.list.splice(index, 1);
                                    }
                                }}
                            ]
                        }
                    }))
                }},
                
                // Status bar
                { p: { 
                    text: () => {
                        const remaining = todos.list.filter(t => !t.completed).length;
                        return `${remaining} item${remaining !== 1 ? 's' : ''} left`;
                    }
                }}
            ]
        }
    };
};

User Dashboard

const UserDashboard = (props, context) => {
    const { user, stats, ui } = context.fluentState.getFluentStates();
    
    // Initialize state
    user.profile = user.profile || { name: 'Guest', email: '' };
    stats.visits = stats.visits || 0;
    stats.lastLogin = stats.lastLogin || null;
    ui.darkMode = ui.darkMode ?? false;
    
    const login = () => {
        user.profile.name = 'John Doe';
        user.profile.email = 'john@example.com';
        stats.visits++;
        stats.lastLogin = Date.now();
    };
    
    const logout = () => {
        user.profile.name = 'Guest';
        user.profile.email = '';
    };
    
    return {
        div: { 
            class: () => `dashboard ${ui.darkMode ? 'dark' : 'light'}`,
            children: [
                { header: {
                    children: [
                        { h1: { text: () => `Welcome, ${user.profile.name}!` } },
                        { button: {
                            text: () => ui.darkMode ? '☀️ Light' : '🌙 Dark',
                            onclick: () => ui.darkMode = !ui.darkMode
                        }}
                    ]
                }},
                
                { main: {
                    children: [
                        { section: { class: 'user-info',
                            children: [
                                { h2: { text: 'Profile' } },
                                { p: { text: () => `Email: ${user.profile.email || 'Not set'}` } },
                                { p: { text: () => `Visits: ${stats.visits}` } },
                                { p: { 
                                    text: () => stats.lastLogin ? 
                                        `Last login: ${new Date(stats.lastLogin).toLocaleString()}` :
                                        'Never logged in'
                                }}
                            ]
                        }},
                        
                        { section: { class: 'actions',
                            children: [
                                () => user.profile.name === 'Guest' ?
                                    { button: { text: 'Login', onclick: login } } :
                                    { button: { text: 'Logout', onclick: logout } }
                            ]
                        }}
                    ]
                }}
            ]
        }
    };
};

Conclusion

FluentState transforms state management in Juris applications by providing an intuitive, powerful API that feels like working with regular JavaScript objects. With automatic reactivity, flexible access patterns, and minimal setup, you can build complex, responsive applications with ease.

Key Benefits

  • Intuitive API: State access feels like regular JavaScript
  • Automatic Reactivity: UI updates automatically when state changes
  • Flexible Patterns: Choose the access pattern that fits your component
  • Zero Configuration: Start using state immediately after setup
  • Powerful Features: Non-reactive access, subscriptions, utilities, and more

Start building reactive applications with FluentState today!

Clone this wiki locally