Composable State Management
The HellaJS store and core packages provide sophisticated and composable state management patterns that scale from simple component state with reactive primitives to complex, shared application states.
State With Signals
Signals enable both local component state and global application state using reactive primitives.
Local Component State
Create encapsulated state within components using signals directly.
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 across components by creating signals outside component scope.
// 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
);
// Components access global state directly
const Header = () => (
<header class={`header theme-${theme()}`}>
<h1>My App</h1>
{isLoggedIn() ? (
<div>Welcome, {user().name}!</div>
) : (
<button onClick={showLogin}>Login</button>
)}
<span class="badge">{unreadCount}</span>
</header>
);
const ThemeToggle = () => (
<button onClick={() => theme(theme() === 'light' ? 'dark' : 'light')}>
Switch to {theme() === 'light' ? 'dark' : 'light'} mode
</button>
);
State With Stores
Built on the reactive core, stores deliver fine-grained updates where only the specific properties that change trigger re-computation.
Creating Stores
Each property becomes independently reactive.
// 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
Property Access
Store properties behave like signals. Call them to read values, call them with arguments to set values.
const counter = store({
count: 0,
label: 'Counter'
});
// Reading values
const currentCount = counter.count();
const currentLabel = counter.label();
// Setting values (triggers only dependent computations)
counter.count(5);
counter.label('Advanced Counter');
// Individual property updates
counter.count(counter.count() + 1); // Only count-dependent code runs
Complete Replacement
The set
method allows you to replace the entire state of the store with a new object.
const counter = store({
count: 0,
label: 'Counter'
});
// Replace the entire store object
counter.set({
count: 10,
label: 'Updated Counter'
});
Partial Updates
The update
method allows partial updates to store properties.
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
}
});
Readonly Properties
Control which properties can be modified using readonly options.
// 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'); // ❌ Runtime error - readonly
// All properties readonly
const constants = store({
PI: 3.14159,
MAX_USERS: 100
}, { readonly: true });
Nested Stores
Stores can contain other stores, creating hierarchical state structures with fine-grained reactivity at every level.
const appState = store({
user: store({
name: 'John Doe',
email: 'john@example.com',
preferences: store({
theme: 'dark',
language: 'en',
notifications: true
})
}),
ui: store({
sidebarOpen: false,
activeTab: 'dashboard',
loading: false
})
});
// Access nested properties
console.log(appState.user.name()); // 'John Doe'
console.log(appState.user.preferences.theme()); // 'dark'
// Update nested properties - only affects dependent computations
appState.user.preferences.theme('light');
appState.ui.sidebarOpen(true);
Computed Values
Stores expose a computed
property that is a computed signal itself, allowing you to derive state from the store’s properties reactively.
const inventory = store({
items: [
{ id: 1, name: 'Widget', price: 10, quantity: 5 },
{ id: 2, name: 'Gadget', price: 25, quantity: 2 }
],
});
// Access the reactive computed signal
const totalValue = inventory.computed(() =>
inventory.items().reduce((sum, item) =>
sum + (item.price * item.quantity), 0
)
);
const lowStockItems = inventory.computed(() =>
inventory.items().filter(item => item.quantity < 3)
);
console.log(totalValue()); // 100
console.log(lowStockItems()); // [{ id: 2, ... }]
Batched Updates
Use batch for multiple store updates.
const updateUser = (updates) => {
batch(() => {
user.name(updates.name);
user.email(updates.email);
user.lastUpdated(new Date());
});
// All dependent computations run once after batch
};
Store Cleanup
Stores provide a cleanup method for memory management.
const subscription = effect(() => {
console.log('User changed:', user.name());
});
const cleanup = () => {
subscription(); // Clean up effect
user.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 fundamentally a reactive proxy that intercepts property access and 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 and arrays become nested stores with recursive property transformation
- Functions remain as-is, 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 setter
- 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.
- Property Reuse - Reactive primitives are reused across property access patterns
- Lazy Initialization - Nested stores are created only when properties are first accessed
- 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.