store

A deeply reactive store for managing application state with full type safety and flexible readonly controls.

API

function store<T>(initial: T, options?: StoreOptions<T>): Store<T>

interface Store<T> {
  // Properties from T are converted to signals or nested stores
  [K in keyof T]: // ...

  // Methods
  computed: ReadonlySignal<T>;
  set(value: T): void;
  update(partial: PartialDeep<T>): void;
  cleanup(): void;
}

interface StoreOptions<T> {
  readonly?: boolean | readonly (keyof T)[];
}

TypeScript

The store function is fully type-safe, preserving your initial object’s structure and providing type inference for all properties and methods.

import { store } from '@hellajs/store';

interface User {
  name: string;
  age: number;
  settings: { theme: 'dark' | 'light' };
}

const userStore = store<User>({
  name: 'John Doe',
  age: 30,
  settings: { theme: 'dark' },
});

// userStore.name is Signal<string>
// userStore.settings.theme is Signal<'dark' | 'light'>

Basic Usage

Create a store by passing a plain JavaScript object. Access properties as signal functions to read their values, and call them with a new value to update.

import { store } from '@hellajs/store';
import { effect } from '@hellajs/core';

const user = store({
  name: 'John Doe',
  address: {
    city: 'New York',
  },
});

effect(() => {
  console.log(`${user.name()} lives in ${user.address.city()}`);
});
// Logs: "John Doe lives in New York"

// Update a top-level property
user.name('Jane Doe');
// Logs: "Jane Doe lives in New York"

// Update a nested property
user.address.city('San Francisco');
// Logs: "Jane Doe lives in San Francisco"

Key Concepts

Deep Reactivity

All nested objects and arrays are automatically converted to reactive stores, creating a fully reactive state tree.

const userStore = store({
  profile: { name: 'John', age: 30 },
  settings: { theme: 'dark' }
});

// All nested properties are reactive
effect(() => {
  console.log(userStore.profile.name()); // Reactive to name changes
  console.log(userStore.settings.theme()); // Reactive to theme changes
});

Signal Properties

Each store property becomes a signal that can be read and written using function call syntax.

const appStore = store({ count: 0, user: { name: 'Alice' } });

// Read values (getter syntax)
console.log(appStore.count()); // 0
console.log(appStore.user.name()); // 'Alice'

// Write values (setter syntax)
appStore.count(5);
appStore.user.name('Bob');

Flexible Updates

Choose between complete state replacement or partial deep updates using the update() method.

const store = store({ user: { name: 'John', age: 30 }, ui: { theme: 'light' } });

// Partial update - only changes specified properties
store.update({ user: { name: 'Jane' } }); // age remains 30

// Complete replacement - replaces entire state
store.set({ user: { name: 'Alice', age: 25 }, ui: { theme: 'dark' } });

Computed Values

Stores expose a computed property that is a computed signal itself, allowing you to derive state from the store’s properties reactively.

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 }
  ],
});

// 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, ... }]

Readonly Control

Granular control over which properties can be modified, with full TypeScript support.

const configStore = store(
  { apiUrl: 'https://api.com', theme: 'light', debug: false },
  { readonly: ['apiUrl'] } // apiUrl cannot be modified
);

configStore.theme('dark'); // ✅ Works
configStore.apiUrl('new-url'); // ❌ TypeScript error + runtime error

Important Considerations

Performance

Large stores create many signals - consider splitting into smaller, focused stores for better performance.

// ❌ Monolithic store
const appStore = store({ user: {...}, ui: {...}, data: {...} });
// ✅ Split into specialized stores
const userStore = store({ name: '', email: '' });
const uiStore = store({ theme: 'light', sidebar: false });

Memory Management

Call cleanup() when stores are no longer needed to prevent memory leaks.

// ✅ Clean up when done
const userStore = store({ name: 'John' });
// Later...
userStore.cleanup();

Nested Updates

Always use the update() method for partial changes to avoid replacing entire nested objects.

// ✅ Efficient partial update
store.update({ settings: { theme: 'dark' } });
// ❌ Replaces entire nested object
store.settings({ theme: 'dark', other: undefined });