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.

  1. Property Change - A store property is updated via setter
  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.

  • 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.