Skip to content

Navigation Menu

Sign in
Appearance settings

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

Provide feedback

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

Saved searches

Use saved searches to filter your results more quickly

Sign up
Appearance settings

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:

×ばつ', onclick: () => removeTodo(todo.id) } } ] } })) } } ] } }; };">
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

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