-
Notifications
You must be signed in to change notification settings - Fork 8
Juris FluentState
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.
- 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
<!-- 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 install juris@0.9.0
import { Juris } from 'juris/juris'; import { HeadlessManager } from 'juris/juris-headless'; import { createFluentStateHeadless } from 'juris/headless/juris-fluentstate';
<!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>
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 } } ] } }; };
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 } };
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` } }; };
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); } } }; };
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}` } } ] } }; };
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 } })) } } ] } }; };
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' } }; };
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 } } ] } }; };
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) } } ] } })) } } ] } }; };
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 } } ] } }; };
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 } } ] } }; };
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 } })) } } ] } }; };
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(); } } } ] } }; };
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 } } ] } }; };
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' } }; };
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 } }; };
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 } }; };
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' } }) }; };
-
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
-
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
-
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
Standard JavaScript array methods work naturally:
stateArray.push(...items)stateArray.pop()stateArray.shift()stateArray.unshift(...items)stateArray.splice(start, deleteCount, ...items)
-
debug.getStats()- Get statistics about subscriptions and cache -
debug.clearCache()- Clear internal cache -
debug.clearSubscriptions()- Clear all subscriptions
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
FluentState provides comprehensive logging. Check browser console for detailed state operation logs.
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
- [Juris Framework Documentation](https://jurisjs.com)
- [Interactive FluentState Examples](https://codepen.io/jurisauthor)
- [GitHub Repository](https://github.com/jurisjs/juris)
- [Online Testing Platform](https://jurisjs.com/tests/juris_pure_test_interface.html)
MIT License - Part of the Juris JavaScript Unified Reactive Interface Solution