@owlmeans/state
v0.1.2
Published
A reactive state management library for OwlMeans Common applications. This package provides a comprehensive state management system with subscription-based reactivity, resource integration, and real-time data synchronization for building responsive fullst
Downloads
17
Readme
@owlmeans/state
A reactive state management library for OwlMeans Common applications. This package provides a comprehensive state management system with subscription-based reactivity, resource integration, and real-time data synchronization for building responsive fullstack applications.
Overview
The @owlmeans/state package is a reactive state management library in the OwlMeans Common ecosystem that provides:
- Reactive State Management: Subscribe to state changes with automatic updates
- Resource Integration: Built on top of OwlMeans Resource system for data persistence
- Real-time Synchronization: Live updates across multiple subscribers
- Model-Based Architecture: Type-safe state models with built-in update methods
- Query Subscriptions: Subscribe to filtered data sets with criteria
- Memory Management: Automatic cleanup and unsubscription handling
- Context Integration: Seamless integration with OwlMeans context system
Installation
npm install @owlmeans/stateCore Concepts
State Resource
The StateResource extends the standard OwlMeans Resource interface with reactive capabilities, allowing components to subscribe to data changes and receive automatic updates.
State Model
A StateModel wraps resource records with reactive capabilities, providing methods to update data and automatically notify subscribers of changes.
Subscriptions
Subscriptions connect components to state changes, automatically triggering updates when subscribed data changes.
Listeners
Functions that handle state change notifications, receiving updated state models when data changes.
API Reference
Types
StateResource<T extends ResourceRecord>
Extended resource interface with subscription capabilities.
interface StateResource<T extends ResourceRecord> extends Resource<T> {
subscribe: (params: StateSubscriptionOption<T>) => [() => void, StateModel<T>[]]
listen: (listener: StateListener<T>) => () => void
erase: () => Promise<void>
}StateModel<T extends ResourceRecord>
Reactive wrapper for resource records.
interface StateModel<T extends ResourceRecord> {
record: T
commit: (force?: boolean) => void
update: (data?: Partial<T>) => void
clear: () => void
}StateSubscriptionOption<T>
Configuration for state subscriptions.
interface StateSubscriptionOption<T extends ResourceRecord> {
id?: string | string[] // Specific record IDs to subscribe to
_systemId?: string // System-level subscription identifier
query?: ListCriteria // Query criteria for filtered subscriptions
default?: Partial<T> // Default data for new records
listener: StateListener<T> // Callback function for updates
}StateListener<T>
Function type for handling state changes.
interface StateListener<T extends ResourceRecord> {
(record: StateModel<T>[]): void | Promise<void>
}Factory Functions
createStateResource<R>(alias?: string): StateResource<R>
Creates a new state resource with reactive capabilities.
Parameters:
alias: Optional resource alias (defaults to 'state')
Returns: StateResource instance with subscription support
appendStateResource<C, T>(ctx: T, alias?: string): T & StateResourceAppend
Appends a state resource to an existing context.
Parameters:
ctx: OwlMeans context instancealias: Optional resource alias
Returns: Context with state resource capabilities
Core Methods
subscribe(params: StateSubscriptionOption<T>): [() => void, StateModel<T>[]]
Subscribes to state changes for specific records or queries.
Parameters:
params: Subscription configuration with listener and criteria
Returns: Tuple of [unsubscribe function, current state models]
listen(listener: StateListener<T>): () => void
Adds a global listener for all state changes.
Parameters:
listener: Function to handle state changes
Returns: Unsubscribe function
erase(): Promise<void>
Clears all state data and notifies subscribers.
State Model Methods
commit(force?: boolean): void
Persists model changes to the underlying resource.
update(data?: Partial<T>): void
Updates model data and notifies subscribers.
clear(): void
Clears model data while maintaining subscription.
Error Types
StateToolingError
Base error for state management tooling issues.
StateListenerError
Error related to listener management and execution.
Usage Examples
Basic State Subscription
import { createStateResource } from '@owlmeans/state'
interface User extends ResourceRecord {
id?: string
name: string
email: string
status: 'active' | 'inactive'
}
// Create state resource
const userState = createStateResource<User>('users')
// Subscribe to a specific user
const [unsubscribe, models] = userState.subscribe({
id: 'user123',
listener: (models) => {
const user = models[0]
console.log('User updated:', user.record)
}
})
// Get current state immediately
if (models.length > 0) {
console.log('Current user:', models[0].record)
}
// Update user data
models[0].update({ status: 'inactive' })
models[0].commit()
// Cleanup
unsubscribe()Multiple Record Subscription
// Subscribe to multiple users
const [unsubscribe, models] = userState.subscribe({
id: ['user1', 'user2', 'user3'],
listener: (models) => {
console.log(`Received updates for ${models.length} users`)
models.forEach(model => {
console.log(`User ${model.record.id}: ${model.record.name}`)
})
}
})Query-Based Subscription
// Subscribe to active users
const [unsubscribe, models] = userState.subscribe({
query: {
status: 'active',
role: ['admin', 'moderator']
},
listener: (models) => {
console.log(`Active admin/moderator users: ${models.length}`)
}
})Global State Listening
// Listen to all state changes
const unsubscribe = userState.listen((models) => {
console.log('Global state change detected')
// Handle any user state changes
})React Integration Example
import React, { useEffect, useState } from 'react'
const UserProfile: React.FC<{ userId: string }> = ({ userId }) => {
const [user, setUser] = useState<StateModel<User> | null>(null)
useEffect(() => {
const [unsubscribe, models] = userState.subscribe({
id: userId,
default: { name: '', email: '', status: 'active' },
listener: (models) => {
setUser(models[0] || null)
}
})
// Set initial state
if (models.length > 0) {
setUser(models[0])
}
return unsubscribe
}, [userId])
const handleUpdate = (data: Partial<User>) => {
if (user) {
user.update(data)
user.commit()
}
}
if (!user) {
return <div>Loading...</div>
}
return (
<div>
<h2>{user.record.name}</h2>
<p>Email: {user.record.email}</p>
<p>Status: {user.record.status}</p>
<button onClick={() => handleUpdate({ status: 'inactive' })}>
Deactivate User
</button>
</div>
)
}Context Integration
import { createBasicContext } from '@owlmeans/context'
import { appendStateResource } from '@owlmeans/state'
// Create context with state capabilities
const context = createBasicContext()
appendStateResource(context, 'user-state')
// Access state resource from context
const stateResource = context.getStateResource<User>('user-state')Bulk Operations
// Subscribe to system-level changes
const [unsubscribe, models] = userState.subscribe({
_systemId: 'bulk-operations',
listener: async (models) => {
// Handle bulk updates
console.log(`Processing ${models.length} records`)
// Batch commit changes
models.forEach(model => model.commit())
}
})
// Perform bulk updates
await userState.create({ name: 'User 1', email: '[email protected]' })
await userState.create({ name: 'User 2', email: '[email protected]' })Default Data Handling
// Subscribe with default data for new records
const [unsubscribe, models] = userState.subscribe({
id: 'new-user',
default: {
name: 'New User',
email: '',
status: 'active'
},
listener: (models) => {
const user = models[0]
if (!user.record.email) {
// User still needs email
console.log('User needs email address')
}
}
})Error Handling
import { StateListenerError } from '@owlmeans/state'
try {
const [unsubscribe, models] = userState.subscribe({
id: 'user123',
listener: async (models) => {
// Async listener that might throw
await processUserData(models[0].record)
}
})
} catch (error) {
if (error instanceof StateListenerError) {
console.error('Listener error:', error.message)
}
}Advanced Features
Custom Subscription Patterns
// Conditional subscriptions
const subscribeToUser = (userId: string, condition: (user: User) => boolean) => {
return userState.subscribe({
id: userId,
listener: (models) => {
const user = models[0]
if (user && condition(user.record)) {
// Handle conditional updates
console.log('Condition met for user:', user.record.id)
}
}
})
}
// Subscribe only to active users
const [unsubscribe] = subscribeToUser('user123', user => user.status === 'active')Memory Management
class UserManager {
private subscriptions: (() => void)[] = []
subscribeToUser(userId: string) {
const [unsubscribe] = userState.subscribe({
id: userId,
listener: this.handleUserUpdate.bind(this)
})
this.subscriptions.push(unsubscribe)
}
cleanup() {
// Clean up all subscriptions
this.subscriptions.forEach(unsubscribe => unsubscribe())
this.subscriptions = []
}
private handleUserUpdate(models: StateModel<User>[]) {
// Handle updates
}
}Performance Optimization
// Debounced updates
let updateTimeout: NodeJS.Timeout
const [unsubscribe] = userState.subscribe({
id: 'user123',
listener: (models) => {
clearTimeout(updateTimeout)
updateTimeout = setTimeout(() => {
// Process updates after debounce
console.log('Processing debounced update')
}, 100)
}
})Integration with OwlMeans Ecosystem
The @owlmeans/state package integrates with:
- @owlmeans/resource: Built on the resource system for data persistence
- @owlmeans/context: Context-based service registration and access
- @owlmeans/client: React hooks and component integration
- @owlmeans/error: Error handling and reporting
- @owlmeans/client-resource: Client-side resource management
Best Practices
Subscription Management
- Always store unsubscribe functions and call them during cleanup
- Use specific IDs when possible instead of broad queries
- Implement proper error handling in listeners
- Avoid creating subscriptions in render loops
Performance
- Use query-based subscriptions for filtered data sets
- Implement debouncing for high-frequency updates
- Clean up unused subscriptions promptly
- Use default data to avoid loading states
Memory Management
- Implement proper cleanup in component unmount
- Use weak references for large object graphs
- Monitor subscription count in development
- Clean up system subscriptions when no longer needed
Error Handling
- Wrap async listeners in try-catch blocks
- Implement fallback UI for failed state updates
- Log subscription errors for debugging
- Use defensive programming for model access
Fixes #32.
