Composable State Management
Powerful state management patterns that scale from simple component state to complex, global states.
State With Signals
Combining signals enable both local component state and global application state using reactive primitives.
Local Component State
Create encapsulated state within components using signals.
import { signal } from '@hellajs/core';
const Counter = () => {
// Local component state
const count = signal(0);
const isVisible = signal(true);
return (
<div>
<h1>{count}</h1>
<button onClick={() => count(count() + 1)}>+</button>
<button onClick={() => isVisible(!isVisible())}>
Toggle
</button>
{isVisible() && <p>Count is visible</p>}
</div>
);
};
Global Shared State
Share state by creating signals in the global scope.
import { signal, computed } from '@hellajs/core';
// Global application state
const user = signal(null);
const theme = signal('light');
const notifications = signal([]);
// Global computed values
const isLoggedIn = computed(() => !!user());
const unreadCount = computed(() =>
notifications().filter(n => !n.read).length
);
Composable State Patterns
Choose any state management pattern that fits your application’s requirements.
import { signal } from '@hellajs/core';
import { store } from '@hellajs/store';
const counterState = () => {
const count = signal(0);
return {
count() {
return count;
},
increment() {
count(count() + 1);
},
reset() {
count(0);
}
};
}
class CounterState {
constructor() {
this.count = signal(0);
}
increment() {
this.count(this.count() + 1);
}
reset() {
this.count(0);
}
}
State With Stores
A store delivers fine-grained updates where only the specific properties that change trigger re-computation.
Creating Stores
Each property becomes an independently reactive signal.
import { store } from '@hellajs/store';
// Create a reactive store
const counter = store({
count: 0,
step: 1,
guards: {
min: 0,
max: 10
}
});
// Each property is individually reactive
console.log(counter.count()); // 0
console.log(counter.step()); // 1
console.log(counter.guards.max()); // 10
Readonly Properties
Control which properties can be modified using readonly options.
import { store } from '@hellajs/store';
// Specific readonly properties
const config = store({
apiUrl: 'https://api.example.com',
version: '1.0.0',
debug: true
}, {
readonly: ['apiUrl', 'version']
});
config.debug(false); // ✅ Allowed
config.apiUrl('new-url'); // ❌ Signal accepts calls but doesn't update
// All properties readonly
const constants = store({
PI: 3.14159,
MAX_USERS: 100
}, { readonly: true });
Store Snapshots
Stores expose snapshot, a plain object representation of your store data.
import { store } from '@hellajs/store';
import { computed } from '@hellajs/core';
const inventory = store({
items: [
{ id: 1, name: 'Widget', price: 10, quantity: 5 },
{ id: 2, name: 'Gadget', price: 25, quantity: 2 }
],
});
// Get a reactive snapshot of the entire store
const inventorySnapshot = inventory.snapshot();
console.log(inventorySnapshot.items); // Current array value from reactive computed
const lowStockItems = computed(() =>
inventory.items().filter(item => item.quantity < 3)
);
console.log(lowStockItems()); // [{ id: 2, ... }]
Updating Stores
The update
method allows partial updates to store properties and set
updates all values.
import { store } from '@hellajs/store';
const user = store({
name: 'John',
email: 'john@example.com',
age: 30,
address: {
street: '123 Main St',
city: 'Anytown',
zip: '12345'
}
});
// Partial update - only updates specified properties
user.update({
age: 31,
address: {
city: 'New City' // Merges with existing address
}
});
console.log(user.age()); // 31
// Full update - replaces all properties
user.set({
name: 'Jane',
email: 'jane@example.com',
age: 25,
address: {
street: '456 Elm St',
city: 'Othertown',
zip: '67890'
}
});
console.log(user.name()); // 'Jane'
Nested Stores
Stores can contain other stores, creating hierarchical state with fine-grained reactivity.
import { store } from '@hellajs/store';
const userStore = store({
name: 'John Doe',
email: 'john@example.com',
preferences: {
theme: 'dark',
language: 'en',
notifications: true
}
});
const uiStore = store({
sidebarOpen: false,
activeTab: 'dashboard',
loading: false
});
const appStore = store({
user: userStore,
ui: uiStore
});
// Access nested properties
console.log(appStore.user.name()); // 'John Doe'
console.log(appStore.user.preferences.theme()); // 'dark'
// Update nested properties - only affects dependent computations
appStore.user.preferences.theme('light');
appStore.ui.sidebarOpen(true);
Batched Updates
Use batch for multiple store updates.
import { store } from '@hellajs/store';
import { batch } from '@hellajs/core';
const userStore = store({
name: 'John Doe',
email: 'john@example.com',
preferences: {
theme: 'dark',
language: 'en',
notifications: true
}
});
const updateUser = (updates) => {
batch(() => {
userStore.name(updates.name);
userStore.email(updates.email);
// Update existing properties only
});
// All dependent computations run once after batch
};
// Note: Store methods like set() and update() handle batching internally
Store Cleanup
Stores provide a cleanup method for memory management.
import { effect } from '@hellajs/core';
const subscription = effect(() => {
console.log('User changed:', userStore.name());
});
const cleanup = () => {
subscription(); // Clean up effect
userStore.cleanup(); // Clean up store and nested reactive values
};
Internal Mechanics
A regular object becomes a reactive store by wrapping its primitive properties in signals.
Store Architecture
Each store is conceptually a reactive proxy that converts object properties into reactive primitives.
Store Object
├── Property A → Signal<T>
├── Property B → Signal<T>
└── Nested Store
├── Property C → Signal<T>
└── Property D → Computed<T>
Property Transformation
When a store is created, the system transforms each property based on its type and characteristics.
- Primitive values (strings, numbers, booleans) become individual signals
- Objects become nested stores with recursive property transformation
- Arrays become signals containing the array value
- Functions remain unchanged, preserving their original behavior
- Computed expressions maintain their reactive computation capabilities
This transformation process occurs once during store creation, establishing a reactive structure that handles all subsequent updates.
Dependency Graph Integration
Store signals maintain their own set of dependencies and subscribers, propagating only through the specific paths that need updates.
Signal Property → Computed Values → Effects
↑ ↑ ↑
(individual) (property-specific) (targeted)
This distributed approach eliminates the cascade re-computation problems common in monolithic state systems.
Reactive Property Binding
Store properties work with the templating engine’s function reference binding system without extra configuration.
// Direct reactive binding
const counter = store({
count: 0,
label: 'Count'
});
// Template binding
<h1>{counter.label}: {counter.count}</h1> // Automatically reactive
Update Propagation
Store updates follow the same efficient propagation mechanism as the core reactive system.
- Property Change - A store property is updated via signal function call
- Dependency Marking - Only computations dependent on that specific property are marked dirty
- Selective Propagation - Changes propagate only through affected dependency chains
- Lazy Evaluation - Computed values recalculate only when accessed AND dependencies changed
- Effect Execution - Effects run immediately for properties they depend on
Memory Efficiency
The store system includes several memory optimization strategies.
- Automatic Cleanup - The cleanup system recursively disposes of all nested reactive values
- Reference Management - Circular references are prevented through careful property definition
Immutability Support
While stores provide mutable interfaces for developer convenience, they maintain compatibility with immutable update patterns. The update()
method performs shallow merging that respects the reactive system’s change detection while preserving the convenience of partial updates.