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.

  1. Property Change - A store property is updated via signal function call
  2. Dependency Marking - Only computations dependent on that specific property are marked dirty
  3. Selective Propagation - Changes propagate only through affected dependency chains
  4. Lazy Evaluation - Computed values recalculate only when accessed AND dependencies changed
  5. 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.