-
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.0import { 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