npm.io
10.1.2 • Published 5 months ago

substate

Licence
MIT
Version
10.1.2
Deps
2
Size
330 kB
Vulns
0
Weekly
0
Stars
17

Substate

npm version npm downloads npm bundle size coverage license TypeScript

A lightweight, type-safe state management library that combines the Pub/Sub pattern with immutable state management.

Substate provides a simple yet powerful way to manage application state with built-in event handling, middleware support, and seamless synchronization capabilities. Perfect for applications that need reactive state management without the complexity of larger frameworks.

Table of Contents

Features

  • Lightweight - Tiny bundle size at 10kb
  • Type-safe - Full TypeScript support with comprehensive type definitions
  • Reactive - Built-in Pub/Sub pattern for reactive state updates
  • Time Travel - Complete state history with ability to navigate between states
  • Tagged States - Named checkpoints for easy state restoration
  • Immutable - Automatic deep cloning prevents accidental state mutations
  • Sync - Unidirectional data binding with middleware transformations
  • Middleware - Extensible with before/after update hooks
  • Nested Props - Easy access to nested properties with optional dot notation or standard object spread
  • Framework Agnostic - Works with any JavaScript framework or vanilla JS

Installation

npm install substate

Quick Start

Installation & Basic Usage
npm install substate
import { createStore } from 'substate';

// Create a simple counter store
const counterStore = createStore({
  name: 'CounterStore',
  state: { count: 0, lastUpdated: Date.now() }
});

// Update state
counterStore.updateState({ count: 1 });
console.log(counterStore.getCurrentState()); // { count: 1, lastUpdated: 1234567890 }

// Listen to changes
counterStore.on('UPDATE_STATE', (newState) => {
  console.log('Counter updated:', newState.count);
});

Tagged States - Named State Checkpoint System

Tagged states is a Named State Checkpoint System that allows you to create semantic, named checkpoints in your application's state history. Instead of navigating by numeric indices, you can jump to meaningful moments in your app's lifecycle.

What is a Named State Checkpoint System?

A Named State Checkpoint System provides:

  • Semantic Navigation: Jump to states by meaningful names instead of numbers
  • State Restoration: Restore to any named checkpoint and continue from there
  • Debugging Support: Tag known-good states for easy rollback
  • User Experience: Enable features like "save points" and "undo to specific moment"
Basic Usage
import { createStore } from 'substate';

const gameStore = createStore({
  name: 'GameStore',
  state: { level: 1, score: 0, lives: 3 }
});

// Create tagged checkpoints with meaningful names
gameStore.updateState({ 
  level: 5, 
  score: 1250, 
  $tag: "level-5-start" 
});

gameStore.updateState({ 
  level: 10, 
  score: 5000, 
  lives: 2, 
  $tag: "boss-fight" 
});

// Jump back to any tagged state by name
gameStore.jumpToTag("level-5-start");
console.log(gameStore.getCurrentState()); // { level: 5, score: 1250, lives: 3 }

// Access tagged states without changing current state
const bossState = gameStore.getTaggedState("boss-fight");
console.log(bossState); // { level: 10, score: 5000, lives: 2 }

// Manage your tags
console.log(gameStore.getAvailableTags()); // ["level-5-start", "boss-fight"]
gameStore.removeTag("level-5-start");
Advanced Checkpoint Patterns
Form Wizard with Step Restoration
const formStore = createStore({
  name: 'FormWizard',
  state: {
    currentStep: 1,
    personalInfo: { firstName: '', lastName: '', email: '' },
    addressInfo: { street: '', city: '', zip: '' },
    paymentInfo: { cardNumber: '', expiry: '' }
  }
});

// Save progress at each completed step
function completePersonalInfo(data) {
  formStore.updateState({
    personalInfo: data,
    currentStep: 2,
    $tag: "step-1-complete"
  });
}

function completeAddressInfo(data) {
  formStore.updateState({
    addressInfo: data,
    currentStep: 3,
    $tag: "step-2-complete"
  });
}

// User can jump back to any completed step
function goToStep(stepNumber) {
  const stepTag = `step-${stepNumber}-complete`;
  if (formStore.getAvailableTags().includes(stepTag)) {
    formStore.jumpToTag(stepTag);
  }
}

// Usage
goToStep(1); // Jump back to personal info step
goToStep(2); // Jump back to address info step
Debugging and Error Recovery
const appStore = createStore({
  name: 'AppStore',
  state: {
    userData: null,
    settings: {},
    lastError: null
  }
});

// Tag known good states for debugging
function markKnownGoodState() {
  appStore.updateState({
    $tag: "last-known-good"
  });
}

// When errors occur, jump back to known good state
function handleError(error) {
  console.error('Error occurred:', error);
  
  if (appStore.getAvailableTags().includes("last-known-good")) {
    console.log('Rolling back to last known good state...');
    appStore.jumpToTag("last-known-good");
  }
}

// Tag states before risky operations
function performRiskyOperation() {
  appStore.updateState({
    $tag: "before-risky-operation"
  });
  
  // ... perform operation that might fail
  
  if (operationFailed) {
    appStore.jumpToTag("before-risky-operation");
  }
}
Game Save System
const gameStore = createStore({
  name: 'GameStore',
  state: {
    player: { health: 100, level: 1, inventory: [] },
    world: { currentArea: 'town', discoveredAreas: [] },
    quests: { active: [], completed: [] }
  }
});

// Auto-save system
function autoSave() {
  const timestamp = new Date().toISOString();
  gameStore.updateState({
    $tag: `auto-save-${timestamp}`
  });
}

// Manual save system
function manualSave(saveName) {
  gameStore.updateState({
    $tag: `save-${saveName}`
  });
}

// Load save system
function loadSave(saveName) {
  const saveTag = `save-${saveName}`;
  if (gameStore.getAvailableTags().includes(saveTag)) {
    gameStore.jumpToTag(saveTag);
    return true;
  }
  return false;
}

// Get all available saves
function getAvailableSaves() {
  return gameStore.getAvailableTags()
    .filter(tag => tag.startsWith('save-'))
    .map(tag => tag.replace('save-', ''));
}

// Usage
manualSave("checkpoint-1");
manualSave("before-boss-fight");
loadSave("checkpoint-1");
Feature Flag and A/B Testing
const experimentStore = createStore({
  name: 'ExperimentStore',
  state: {
    features: {},
    userGroup: null,
    experimentResults: {}
  }
});

// Tag different experiment variants
function setupExperimentVariant(variant) {
  experimentStore.updateState({
    userGroup: variant,
    $tag: `experiment-${variant}`
  });
}

// Jump between experiment variants
function switchToVariant(variant) {
  const variantTag = `experiment-${variant}`;
  if (experimentStore.getAvailableTags().includes(variantTag)) {
    experimentStore.jumpToTag(variantTag);
  }
}

// Usage
setupExperimentVariant("control");
setupExperimentVariant("variant-a");
setupExperimentVariant("variant-b");

switchToVariant("variant-a"); // Switch to variant A
Common Tagging Patterns
// Form checkpoints
formStore.updateState({ ...formData, $tag: "before-validation" });

// API operation snapshots  
store.updateState({ users: userData, $tag: "after-user-import" });

// Feature flags / A-B testing
store.updateState({ features: newFeatures, $tag: "experiment-variant-a" });

// Debugging checkpoints
store.updateState({ debugInfo: data, $tag: "issue-reproduction" });

// Game saves
gameStore.updateState({ saveData, $tag: `save-${Date.now()}` });

// Workflow states
workflowStore.updateState({ status: "approved", $tag: "workflow-approved" });

// User session states
sessionStore.updateState({ user: userData, $tag: "user-logged-in" });

Usage Examples

1. Todo List Management
import { createStore } from 'substate';

interface Todo {
  id: string;
  text: string;
  completed: boolean;
}

const todoStore = createStore({
  name: 'TodoStore',
  state: {
    todos: [] as Todo[],
    filter: 'all' as 'all' | 'active' | 'completed'
  },
  defaultDeep: true
});

// Add a new todo
function addTodo(text: string) {
  const currentTodos = todoStore.getProp('todos') as Todo[];
  todoStore.updateState({
    todos: [...currentTodos, {
      id: crypto.randomUUID(),
      text,
      completed: false
    }]
  });
}

// Toggle todo completion
function toggleTodo(id: string) {
  const todos = todoStore.getProp('todos') as Todo[];
  todoStore.updateState({
    todos: todos.map(todo => 
      todo.id === id ? { ...todo, completed: !todo.completed } : todo
    )
  });
}

// Subscribe to changes
todoStore.on('UPDATE_STATE', (state) => {
  console.log(`${state.todos.length} todos, filter: ${state.filter}`);
});
2. User Authentication Store
import { createStore } from 'substate';

const authStore = createStore({
  name: 'AuthStore',
  state: {
    user: null,
    isAuthenticated: false,
    loading: false,
    error: null
  },
  beforeUpdate: [
    (store, action) => {
      // Log all state changes
      console.log('Auth state changing:', action);
    }
  ],
  afterUpdate: [
    (store, action) => {
      // Persist authentication state
      if (action.user || action.isAuthenticated !== undefined) {
        localStorage.setItem('auth', JSON.stringify(store.getCurrentState()));
      }
    }
  ]
});

// Login action
async function login(email: string, password: string) {
  authStore.updateState({ loading: true, error: null });
  
  try {
    const user = await authenticateUser(email, password);
    authStore.updateState({
      user,
      isAuthenticated: true,
      loading: false
    });
  } catch (error) {
    authStore.updateState({
      error: error.message,
      loading: false,
      isAuthenticated: false
    });
  }
}
3. Shopping Cart with Middleware
import { createStore } from 'substate';

const cartStore = createStore({
  name: 'CartStore',
  state: {
    items: [],
    total: 0,
    tax: 0,
    discount: 0
  },
  defaultDeep: true,
  afterUpdate: [
    // Automatically calculate totals after any update
    (store) => {
      const state = store.getCurrentState();
      const subtotal = state.items.reduce((sum, item) => sum + (item.price * item.quantity), 0);
      const tax = subtotal * 0.08; // 8% tax
      const total = subtotal + tax - state.discount;
      
      // Update calculated fields without triggering infinite loop
      store.stateStorage[store.currentState] = {
        ...state,
        total,
        tax
      };
    }
  ]
});

function addToCart(product) {
  const items = cartStore.getProp('items');
  const existingItem = items.find(item => item.id === product.id);
  
  if (existingItem) {
    cartStore.updateState({
      items: items.map(item =>
        item.id === product.id
          ? { ...item, quantity: item.quantity + 1 }
          : item
      )
    });
  } else {
    cartStore.updateState({
      items: [...items, { ...product, quantity: 1 }]
    });
  }
}
4. Working with Nested Properties
const userStore = createStore({
  name: 'UserStore',
  state: {
    profile: {
      personal: {
        name: 'John Doe',
        email: 'john@example.com'
      },
      preferences: {
        theme: 'dark',
        notifications: true
      }
    },
    settings: {
      privacy: {
        publicProfile: false
      }
    }
  },
  defaultDeep: true
});

// Update nested properties using dot notation (convenient for simple updates)
userStore.updateState({ 'profile.personal.name': 'Jane Doe' });
userStore.updateState({ 'profile.preferences.theme': 'light' });
userStore.updateState({ 'settings.privacy.publicProfile': true });

// Or update nested properties using object spread (no string notation required)
userStore.updateState({ 
  profile: { 
    ...userStore.getProp('profile'),
    personal: { 
      ...userStore.getProp('profile.personal'),
      name: 'Jane Doe' 
    }
  }
});

// Both approaches work - choose what feels more natural for your use case
userStore.updateState({ 'profile.preferences.theme': 'light' }); // Dot notation
userStore.updateState({ 
  profile: { 
    ...userStore.getProp('profile'),
    preferences: { 
      ...userStore.getProp('profile.preferences'),
      theme: 'light' 
    }
  }
}); // Object spread

// Get nested properties
console.log(userStore.getProp('profile.personal.name')); // 'Jane Doe'
console.log(userStore.getProp('profile.preferences')); // { theme: 'light', notifications: true }

Sync - Unidirectional Data Binding

Substate supports two sync() modes:

  • Proxy mode (v11+, recommended): sync(path?, config?) returns a reactive proxy for a state slice. Reads always reflect the latest store state, and writes auto-commit via updateState().
  • Legacy binding mode (v10 compatible): sync(configObject) keeps the unidirectional binding behavior. It remains supported, but logs a one-time console.warn per store instance to encourage migration.
Proxy Sync (v11+): Reactive Proxy
import { createStore } from 'substate';

const store = createStore({
  name: 'UserStore',
  state: { user: { name: 'John', settings: { theme: 'light' } } }
});

const user = store.sync('user'); // reactive proxy

console.log(user.name);     // 'John'
user.name = 'Thomas';       // updateState({ 'user.name': 'Thomas' })

// Nested writes work
user.settings.theme = 'dark';

// Batch multiple writes
const batch = user.batch();
batch.name = 'Thomas R.';
batch.settings.theme = 'light';
batch.commit(); // one updateState call

// Tag/type/deep and scoped middleware for next write(s)
user.with({ $tag: 'profile-save', $type: 'USER_EDIT', $deep: true }).name = 'Tom';

// Or callback form (auto-batch + auto-commit once)
user.with({ $tag: 'profile-save' }, (draft) => {
  draft.name = 'Tom';
});
Primitive Sync (v11+): .value

For single primitive fields, use .value on the proxy returned by sync():

const age = store.sync<number>('age');
console.log(age.value); // 25
age.value = 30;
Proxy Sync Config
type TProxySyncConfig = {
  beforeUpdate?: UpdateMiddleware[];
  afterUpdate?: UpdateMiddleware[];
};
Root Sync (v11+): sync() with no args

Calling sync() without a path returns a proxy for the entire state.

const state = store.sync();       // root proxy
console.log(state.value);         // full current state snapshot
console.log(state.user.name);     // nested read
state.user.name = 'Thomas';       // nested write
with() semantics (v11+)

with() applies tags/metadata + scoped middleware to the next write (one assignment) or to the single commit produced by the callback form.

Basic with() usage
// Tag a single write
store.sync('user').with({ $tag: 'profile-save' }).name = 'Tom';

// Multiple attributes
store.sync('user').with({
  $tag: 'profile-save',
  $type: 'USER_EDIT',
  $deep: true
}).name = 'Tom';
with() on primitives (using .value)
const age = store.sync<number>('age');

// Tag a primitive update
age.with({ $tag: 'age-update' }).value = 30;

// With validation middleware
age.with({
  $tag: 'age-update',
  before: [
    (store, action) => {
      const newAge = action['age'] as number;
      if (newAge < 0 || newAge > 150) {
        throw new Error('Invalid age');
      }
    }
  ]
}).value = 25;
with() with middleware
const user = store.sync('user');

// Validation before update
user.with({
  before: [
    (store, action) => {
      const name = action['user.name'] as string;
      if (!name || name.length < 2) {
        throw new Error('Name must be at least 2 characters');
      }
    }
  ],
  after: [
    (store, action) => {
      console.log('User updated:', action);
    }
  ]
}).name = 'Thomas';
with() callback form (auto-batch)
// Multiple changes in one commit with attributes
store.sync('user').with({ $tag: 'profile-save' }, (draft) => {
  draft.name = 'Tom';
  draft.settings.theme = 'dark';
  draft.settings.notifications = true;
});
// All changes committed atomically with $tag: 'profile-save'
with() on root sync
const state = store.sync();

// Tag a root-level update
state.with({ $tag: 'app-reset' }).user = { name: 'Guest' };

// Or update multiple root fields
state.with({ $tag: 'app-init' }, (draft) => {
  draft.user = { name: 'Admin' };
  draft.settings = { theme: 'dark' };
});
batch() + with() (v11+)

When combining batch() and with(), the attributes apply to the commit (the grouped update), not individual writes.

const user = store.sync('user');

// Start batch, then apply attributes
const batch = user.batch();
batch.with({ $tag: 'profile-batch-update' });
batch.name = 'Thomas';
batch.settings.theme = 'dark';
batch.commit(); // One updateState with $tag: 'profile-batch-update'

// Or: attributes first, then batch
user.with({ $tag: 'profile-batch-update' });
const batch2 = user.batch();
batch2.name = 'Thomas';
batch2.settings.theme = 'dark';
batch2.commit(); // Attributes still apply to the commit

Important: If you call with(...) and then do one immediate assignment (without batch), it applies to that single assignment and is cleared. If you use batch(), the attributes apply to the commit.


Legacy Sync Example (v10): Unidirectional Data Binding

This is the classic sync API that binds store state to a target object (unidirectional). It remains supported.

Basic Sync Example
import { createStore } from 'substate';

const userStore = createStore({
  name: 'UserStore',
  state: { userName: 'John', age: 25 }
});

// Target object (could be a UI model, form, etc.)
const uiModel = { displayName: '', userAge: 0 };

// Sync userName from store to displayName in uiModel
const unsync = userStore.sync({
  readerObj: uiModel,
  stateField: 'userName',
  readField: 'displayName'
});

console.log(uiModel.displayName); // 'John' - immediately synced

// When store updates, uiModel automatically updates
userStore.updateState({ userName: 'Alice' });
console.log(uiModel.displayName); // 'Alice'

// Changes to uiModel don't affect the store (unidirectional)
uiModel.displayName = 'Bob';
console.log(userStore.getProp('userName')); // Still 'Alice'

// Cleanup when no longer needed
unsync();
Sync with Middleware Transformations
const productStore = createStore({
  name: 'ProductStore',
  state: { 
    price: 29.99,
    currency: 'USD',
    name: 'awesome widget'
  }
});

const displayModel = { formattedPrice: '', productTitle: '' };

// Sync with transformation middleware
const unsyncPrice = productStore.sync({
  readerObj: displayModel,
  stateField: 'price',
  readField: 'formattedPrice',
  beforeUpdate: [
    // Transform price to currency format
    (price, context) => `$${price.toFixed(2)}`,
    // Add currency symbol based on store state
    (formattedPrice, context) => {
      const currency = productStore.getProp('currency');
      return currency === 'EUR' ? formattedPrice.replace('
Real-world Sync Example: Form Binding
// Form state store
const formStore = createStore({
  name: 'FormStore',
  state: {
    user: {
      firstName: '',
      lastName: '',
      email: '',
      birthDate: null
    },
    validation: {
      isValid: false,
      errors: []
    }
  }
});

// Form UI object (could be from any UI framework)
const formUI = {
  fullName: '',
  emailInput: '',
  ageDisplay: '',
  submitEnabled: false
};

// Sync full name (combining first + last)
const unsyncName = formStore.sync({
  readerObj: formUI,
  stateField: 'user',
  readField: 'fullName',
  beforeUpdate: [
    (user) => `${user.firstName} ${user.lastName}`.trim()
  ]
});

// Sync email directly
const unsyncEmail = formStore.sync({
  readerObj: formUI,
  stateField: 'user.email',
  readField: 'emailInput'
});

// Sync age calculation from birth date
const unsyncAge = formStore.sync({
  readerObj: formUI,
  stateField: 'user.birthDate',
  readField: 'ageDisplay',
  beforeUpdate: [
    (birthDate) => {
      if (!birthDate) return 'Not provided';
      const age = new Date().getFullYear() - new Date(birthDate).getFullYear();
      return `${age} years old`;
    }
  ]
});

// Sync form validity to submit button
const unsyncValid = formStore.sync({
  readerObj: formUI,
  stateField: 'validation.isValid',
  readField: 'submitEnabled'
});

// Update form data
formStore.updateState({
  'user.firstName': 'John',
  'user.lastName': 'Doe',
  'user.email': 'john@example.com',
  'user.birthDate': '1990-05-15',
  'validation.isValid': true
});

console.log(formUI);
// {
//   fullName: 'John Doe',
//   emailInput: 'john@example.com', 
//   ageDisplay: '34 years old',
//   submitEnabled: true
// }
Multiple Sync Instances

You can sync the same state field to multiple targets with different transformations:

const dataStore = createStore({
  name: 'DataStore',
  state: { timestamp: Date.now() }
});

const dashboard = { lastUpdate: '' };
const report = { generatedAt: '' };
const api = { timestamp: 0 };

// Sync to dashboard with human-readable format
const unsync1 = dataStore.sync({
  readerObj: dashboard,
  stateField: 'timestamp',
  readField: 'lastUpdate',
  beforeUpdate: [(ts) => new Date(ts).toLocaleString()]
});

// Sync to report with ISO string
const unsync2 = dataStore.sync({
  readerObj: report,
  stateField: 'timestamp', 
  readField: 'generatedAt',
  beforeUpdate: [(ts) => new Date(ts).toISOString()]
});

// Sync to API with raw timestamp
const unsync3 = dataStore.sync({
  readerObj: api,
  stateField: 'timestamp' // uses same field name when readField omitted
});

// One update triggers all syncs
dataStore.updateState({ timestamp: Date.now() });
TypeScript Support
import { createStore, type ISubstate, type ICreateStoreConfig } from 'substate';

const config: ICreateStoreConfig = {
  name: 'TypedStore',
  state: { count: 0 },
  defaultDeep: true
};

const store: ISubstate = createStore(config);

API Reference

createStore(config)

Factory function to create a new Substate store with a clean, intuitive API.

function createStore(config: ICreateStoreConfig): ISubstate

Parameters:

Property Type Required Default Description
name string - Unique identifier for the store
state object {} Initial state object
defaultDeep boolean false Enable deep cloning by default for all updates
beforeUpdate UpdateMiddleware[] [] Functions called before each state update
afterUpdate UpdateMiddleware[] [] Functions called after each state update
maxHistorySize number 50 Maximum number of states to keep in history

Returns: A new ISubstate instance

Example:

const store = createStore({
  name: 'MyStore',
  state: { count: 0 },
  defaultDeep: true,
  maxHistorySize: 25, // Keep only last 25 states for memory efficiency
  beforeUpdate: [(store, action) => console.log('Updating...', action)],
  afterUpdate: [(store, action) => console.log('Updated!', store.getCurrentState())]
});

Store Methods
updateState(action: IState): void

Updates the current state with new values. Supports both shallow and deep merging.

// Simple update
store.updateState({ count: 5 });

// Nested property update with dot notation (optional convenience feature)
store.updateState({ 'user.profile.name': 'John' });

// Or update nested properties using standard object spread (no strings required)
store.updateState({ 
  user: { 
    ...store.getProp('user'),
    profile: { 
      ...store.getProp('user.profile'),
      name: 'John' 
    }
  }
});

// Force deep cloning for this update
store.updateState({ 
  data: complexObject,
  $deep: true 
});

// Update with custom type identifier
store.updateState({ 
  items: newItems,
  $type: 'BULK_UPDATE'
});

// Adding a tag
store.updateState({
  items: importantItem,
  $tag: 'important-item-added'
});

Parameters:

  • action - Object containing the properties to update
  • action.$deep (optional) - Force deep cloning for this update
  • action.$type (optional) - Custom identifier for this update
  • action.$tag (optional) - Tag name to create a named checkpoint of this state

batchUpdateState(actions: Array<Partial<TState> & IState>): void

Updates multiple properties at once for better performance. This method is optimized for bulk operations and provides significant performance improvements over multiple individual updateState() calls.

// Instead of multiple individual updates (slower)
store.updateState({ counter: 1 });
store.updateState({ user: { name: "John" } });
store.updateState({ theme: "dark" });

// Use batch update for better performance
store.batchUpdateState([
  { counter: 1 },
  { user: { name: "John" } },
  { theme: "dark" }
]);

// Batch updates with complex operations
store.batchUpdateState([
  { 'user.profile.name': 'Jane' },
  { 'user.profile.email': 'jane@example.com' },
  { 'settings.theme': 'light' },
  { 'settings.notifications': true }
]);

// Batch updates with metadata
store.batchUpdateState([
  { data: newData, $type: 'DATA_IMPORT' },
  { lastUpdated: Date.now() },
  { version: '2.0.0' }
]);

Performance Benefits:

  • Single state clone instead of multiple clones
  • One event emission instead of multiple events
  • Reduced middleware calls (if using middleware)
  • Better memory efficiency

When to Use:

  • Multiple related updates that should happen together
  • Performance-critical code with frequent state changes
  • Bulk operations like form submissions or data imports
  • Reducing re-renders in React/Preact components

Parameters:

  • actions - Array of update action objects (same format as updateState)

Smart Optimization: The method automatically detects if it can use the fast path (no middleware, no deep cloning, no tagging) and processes all updates in a single optimized operation. If any action requires the full feature set, it falls back to processing each action individually.

Example Use Cases:

// Form submission with multiple fields
function submitForm(formData) {
  store.batchUpdateState([
    { 'form.isSubmitting': true },
    { 'form.data': formData },
    { 'form.errors': [] },
    { 'form.lastSubmitted': Date.now() }
  ]);
}

// Bulk data import
function importData(items) {
  store.batchUpdateState([
    { 'data.items': items },
    { 'data.totalCount': items.length },
    { 'data.lastImport': Date.now() },
    { 'ui.showImportSuccess': true }
  ]);
}

// User profile update
function updateProfile(profileData) {
  store.batchUpdateState([
    { 'user.profile': profileData },
    { 'user.lastUpdated': Date.now() },
    { 'ui.profileUpdated': true }
  ]);
}

getCurrentState(): IState

Returns the current active state object.

const currentState = store.getCurrentState();
console.log(currentState); // { count: 5, user: { name: 'John' } }

getProp(prop: string): unknown

Retrieves a specific property from the current state using dot notation for nested access.

// Get top-level property
const count = store.getProp('count'); // 5

// Get nested property
const userName = store.getProp('user.profile.name'); // 'John'

// Get array element
const firstItem = store.getProp('items.0.title');

// Returns undefined for non-existent properties
const missing = store.getProp('nonexistent.path'); // undefined

getState(index: number): IState

Returns a specific state from the store's history by index.

// Get initial state (always at index 0)
const initialState = store.getState(0);

// Get previous state
const previousState = store.getState(store.currentState - 1);

// Get specific historical state
const specificState = store.getState(3);

resetState(): void

Resets the store to its initial state (index 0) and emits an UPDATE_STATE event.

store.resetState();
console.log(store.currentState); // 0
console.log(store.getCurrentState()); // Returns initial state

sync(config: ISyncConfig): () => void

Creates unidirectional data binding between a state property and a target object.

const unsync = store.sync({
  readerObj: targetObject,
  stateField: 'user.name',
  readField: 'displayName',
  beforeUpdate: [(value) => value.toUpperCase()],
  afterUpdate: [(value) => console.log('Synced:', value)]
});

// Call to cleanup the sync
unsync();

Parameters:

Property Type Required Description
readerObj Record<string, unknown> Target object to sync to
stateField string State property to watch (supports dot notation)
readField string Target property name (defaults to stateField)
beforeUpdate BeforeMiddleware[] Transform functions applied before sync
afterUpdate AfterMiddleware[] Side-effect functions called after sync

Returns: Function to call for cleanup (removes event listeners)


clearHistory(): void

Clears all state history except the current state to free up memory.

// After many state updates...
console.log(store.stateStorage.length); // 50+ states

store.clearHistory();
console.log(store.stateStorage.length); // 1 state
console.log(store.currentState); // 0

// Current state is preserved
console.log(store.getCurrentState()); // Latest state data

Use cases:

  • Memory optimization in long-running applications
  • Cleaning up after bulk operations
  • Preparing for application state snapshots

limitHistory(maxSize: number): void

Sets a new limit for state history size and trims existing history if necessary.

// Current setup
store.limitHistory(10); // Keep only last 10 states

// If current history exceeds the limit, it gets trimmed
console.log(store.stateStorage.length); // Max 10 states

// Dynamic adjustment for debugging
if (debugMode) {
  store.limitHistory(100); // More history for debugging
} else {
  store.limitHistory(5);   // Minimal history for production
}

Parameters:

  • maxSize - Maximum number of states to keep (minimum: 1)

Throws: Error if maxSize is less than 1


getMemoryUsage(): { stateCount: number; taggedCount: number; estimatedSizeKB: number }

Returns estimated memory usage information for performance monitoring.

const usage = store.getMemoryUsage();
console.log(`States: ${usage.stateCount}`);
console.log(`Estimated Size: ${usage.estimatedSizeKB}KB`);

// Memory monitoring
if (usage.estimatedSizeKB > 1000) {
  console.warn('Store using over 1MB of memory');
  store.clearHistory(); // Clean up if needed
}

// Performance tracking
setInterval(() => {
  const { stateCount, estimatedSizeKB } = store.getMemoryUsage();
  console.log(`Memory: ${estimatedSizeKB}KB (${stateCount} states)`);
}, 10000);

Returns:

  • stateCount - Number of states currently stored
  • taggedCount - Number of tagged states currently stored
  • estimatedSizeKB - Rough estimation of memory usage in kilobytes

Note: Size estimation is approximate and based on JSON serialization size.


getTaggedState(tag: string): IState | undefined

Retrieves a tagged state by its tag name without affecting the current state.

// Create tagged states
store.updateState({ user: userData, $tag: "user-login" });
store.updateState({ cart: cartData, $tag: "checkout-ready" });

// Retrieve specific tagged states
const loginState = store.getTaggedState("user-login");
const checkoutState = store.getTaggedState("checkout-ready");

// Returns undefined for non-existent tags
const missing = store.getTaggedState("non-existent"); // undefined

Parameters:

  • tag - The tag name to look up

Returns: Deep cloned tagged state or undefined if tag doesn't exist


getAvailableTags(): string[]

Returns an array of all available tag names.

store.updateState({ step: 1, $tag: "step-1" });
store.updateState({ step: 2, $tag: "step-2" });

console.log(store.getAvailableTags()); // ["step-1", "step-2"]

// Use for conditional navigation
if (store.getAvailableTags().includes("last-known-good")) {
  store.jumpToTag("last-known-good");
}

Returns: Array of tag names currently stored


jumpToTag(tag: string): void

Jumps to a tagged state, making it the current state and adding it to history.

// Create checkpoints
store.updateState({ page: "home", $tag: "home-page" });
store.updateState({ page: "profile", user: userData, $tag: "profile-page" });
store.updateState({ page: "settings" });

// Jump back to a checkpoint
store.jumpToTag("profile-page");
console.log(store.getCurrentState().page); // "profile"

// Continue from the restored state
store.updateState({ page: "edit-profile" });

Parameters:

  • tag - The tag name to jump to

Throws: Error if the tag doesn't exist

Events: Emits TAG_JUMPED and STATE_UPDATED


removeTag(tag: string): boolean

Removes a tag from the tagged states collection.

store.updateState({ temp: "data", $tag: "temporary" });

const wasRemoved = store.removeTag("temporary");
console.log(wasRemoved); // true

// Tag is now gone
console.log(store.getTaggedState("temporary")); // undefined

Parameters:

  • tag - The tag name to remove

Returns: true if tag was found and removed, false if it didn't exist

Events: Emits TAG_REMOVED for existing tags


clearTags(): void

Removes all tagged states from the collection.

// After bulk operations with many tags
store.clearTags();
console.log(store.getAvailableTags()); // []

// State history remains intact
console.log(store.stateStorage.length); // Still has all states

Events: Emits TAGS_CLEARED with count of cleared tags


Event Methods (Inherited from PubSub)
on(event: string, callback: Function): void

Subscribe to store events. Substate emits several built-in events for different operations.

Built-in Events:

Event When Emitted Data Payload
STATE_UPDATED After any state update newState: IState
STATE_RESET When resetState() is called None
TAG_JUMPED When jumpToTag() is called { tag: string, state: IState }
TAG_REMOVED When removeTag() removes an existing tag { tag: string }
TAGS_CLEARED When clearTags() is called { clearedCount: number }
HISTORY_CLEARED When clearHistory() is called { previousLength: number }
HISTORY_LIMIT_CHANGED When limitHistory() is called { newLimit: number, oldLimit: number, trimmed: number }
// Listen to state updates
store.on('STATE_UPDATED', (newState: IState) => {
  console.log('State changed:', newState);
});

// Listen to tagging events
store.on('TAG_JUMPED', ({ tag, state }) => {
  console.log(`Jumped to tag: ${tag}`, state);
});

// Listen to memory management events
store.on('HISTORY_CLEARED', ({ previousLength }) => {
  console.log(`Cleared ${previousLength} states from history`);
});

// Listen to custom events
store.on('USER_LOGIN', (userData) => {
  console.log('User logged in:', userData);
});

emit(event: string, data?: unknown): void

Emit custom events to all subscribers.

// Emit custom event
store.emit('USER_LOGIN', { userId: 123, name: 'John' });

// Emit without data
store.emit('CACHE_CLEARED');

off(event: string, callback: Function): void

Unsubscribe from store events.

const handler = (state) => console.log(state);

store.on('UPDATE_STATE', handler);
store.off('UPDATE_STATE', handler); // Removes this specific handler

Store Properties
Property Type Description
name string Store identifier
currentState number Index of current state in history
stateStorage IState[] Array of all state versions
defaultDeep boolean Default deep cloning setting
maxHistorySize number Maximum number of states to keep in history
beforeUpdate UpdateMiddleware[] Pre-update middleware functions
afterUpdate UpdateMiddleware[] Post-update middleware functions
Middleware Order

updateState(action) ├── store.beforeUpdate[] (store-wide) ├── State Processing │ ├── Clone state │ ├── Apply temp updates │ ├── Push to history │ └── Update tagged states ├── sync.beforeUpdate[] (per sync instance) ├── sync.afterUpdate[] (per sync instance) ├── store.afterUpdate[] (store-wide) └── emit STATE_UPDATED or $type event

Memory Management

Substate automatically manages memory through configurable history limits and provides tools for monitoring and optimization.

Automatic History Management

By default, Substate keeps the last 50 states in memory. This provides excellent debugging capabilities while preventing unbounded memory growth:

const store = createStore({
  name: 'AutoManagedStore',
  state: { data: [] },
  maxHistorySize: 50 // Default - good for most applications
});

// After 100 updates, only the last 50 states are kept
for (let i = 0; i < 100; i++) {
  store.updateState({ data: [i] });
}

console.log(store.stateStorage.length); // 50 (not 100!)
Memory Optimization Strategies
For Small Applications (Default)
// Use default settings - 50 states is perfect for small apps
const store = createStore({
  name: 'SmallApp',
  state: { user: null, settings: {} }
  // maxHistorySize: 50 (default)
});
For High-Frequency Updates
// Reduce history for apps with frequent state changes
const store = createStore({
  name: 'RealtimeApp',
  state: { liveData: [] },
  maxHistorySize: 10 // Keep minimal history
});

// Or dynamically adjust
if (isRealtimeMode) {
  store.limitHistory(5);
}
For Large State Objects
// Monitor and manage memory proactively
const store = createStore({
  name: 'LargeDataApp',
  state: { dataset: [], cache: {} },
  maxHistorySize: 20
});

// Regular memory monitoring
setInterval(() => {
  const { stateCount, estimatedSizeKB } = store.getMemoryUsage();
  
  if (estimatedSizeKB > 5000) { // Over 5MB
    console.log('Memory usage high, clearing history...');
    store.clearHistory();
  }
}, 30000);
For Debugging vs Production
const store = createStore({
  name: 'FlexibleApp',
  state: { app: 'data' },
  maxHistorySize: process.env.NODE_ENV === 'development' ? 100 : 25
});

// Runtime adjustment
if (debugMode) {
  store.limitHistory(200); // More history for debugging
} else {
  store.limitHistory(10);  // Minimal for production
}
Memory Monitoring

Use the built-in monitoring tools to track memory usage:

// Basic monitoring
function logMemoryUsage(store: ISubstate, context: string) {
  const { stateCount, estimatedSizeKB } = store.getMemoryUsage();
  console.log(`${context}: ${stateCount} states, ~${estimatedSizeKB}KB`);
}

// After bulk operations
logMemoryUsage(store, 'After data import');

// Regular health checks
setInterval(() => logMemoryUsage(store, 'Health check'), 60000);
Best Practices
  1. Choose appropriate limits: 50 states for normal apps, 10-20 for high-frequency updates
  2. Monitor memory usage: Use getMemoryUsage() to track growth patterns
  3. Clean up after bulk operations: Call clearHistory() after large imports/updates
  4. Balance debugging vs performance: More history = better debugging, less history = better performance
  5. Adjust dynamically: Use limitHistory() to adapt to different application modes
Performance Impact

The default settings are optimized for most use cases:

  • Memory: ~50KB - 5MB typical usage depending on state size
  • Performance: Negligible impact with default 50-state limit
  • Time Travel: Full debugging capabilities maintained
  • Automatic cleanup: No manual intervention required

Note: The 50-state default is designed for smaller applications. For enterprise applications with large state objects or high-frequency updates, consider customizing maxHistorySize based on your specific memory constraints.

Performance Benchmarks

Substate delivers excellent performance across different use cases. Here are real benchmark results from our test suite (averaged over 5 runs for statistical accuracy):

Test Environment: 13th Gen Intel(R) Core(TM) i7-13650HX (14 cores), 16 GB RAM, Windows 10 Home

Shallow State Performance
State Size Store Creation Single Update Avg Update Property Access Memory (50 states)
Small (10 props) 41μs 61μs 1.41μs 0.15μs 127KB
Medium (100 props) 29μs 63μs 25.93μs 0.15μs 1.3MB
Large (1000 props) 15μs 598μs 254μs 0.32μs 12.8MB
Deep State Performance
Complexity Store Creation Deep Update Deep Access Deep Clone Memory Usage
Shallow Deep (1.2K nodes) 52μs 428μs 0.90μs 200μs 10.4MB
Medium Deep (5.7K nodes) 39μs 694μs 0.75μs 705μs 45.8MB
Very Deep (6K nodes) 17μs 754μs 0.90μs 788μs 43.3MB
Key Performance Insights
  • Ultra-fast property access: Sub-microsecond access times regardless of state size
  • Efficient updates: Shallow updates scale linearly, deep cloning adds ~10-100x overhead (expected)
  • Smart memory management: Automatic history limits prevent unbounded growth
  • Consistent performance: Property access speed stays constant as state grows
  • Scalable architecture: Handles 1000+ properties with <300μs update times
Real-World Performance
// ✅ Excellent for high-frequency updates
const fastStore = createStore({
  name: 'RealtimeStore',
  state: { liveData: [] },
  defaultDeep: false // 1.41μs per update
});

// ✅ Great for complex nested state  
const complexStore = createStore({
  name: 'ComplexStore', 
  state: deepNestedObject,
  defaultDeep: true // 428μs per deep update
});

// ✅ Property access is always fast
const value = store.getProp('deeply.nested.property'); // ~1μs

Benchmark Environment:

  • Hardware: 13th Gen Intel(R) Core(TM) i7-13650HX (14 cores), 16 GB RAM
  • OS: Windows 10 Home (Version 2009)
  • Runtime: Node.js v18+
  • Method: Averaged over 5 runs for statistical accuracy

Your results may vary based on hardware and usage patterns.

Why Choose Substate?

Comparison with Other State Management Solutions
Feature Substate Redux Zustand Valtio MobX
Bundle Size ~11KB ~4KB ~2KB ~7KB ~63KB
TypeScript Excellent Excellent Excellent Excellent Excellent
Learning Curve Low High Low Medium High
Boilerplate Minimal Heavy Minimal Minimal Some
Time Travel Built-in DevTools No No No
Memory Management Auto + Manual Manual only Manual only Manual only Manual only
Immutability Auto Manual Manual Auto Mutable
Sync/Binding Built-in No No No Yes
Framework Agnostic Yes Yes Yes Yes Yes
Middleware Support Simple Complex Yes Yes Yes
Nested Updates Dot notation + Object spread Reducers Manual Direct Direct
Tagged States Built-in No No No No

NOTE: Clone our repo and run the benchmarks to see how we stack up!

About This Comparison:

  • Bundle sizes are approximate and may vary by version
  • Learning curve and boilerplate assessments are subjective and based on typical developer experience
  • Feature availability is based on core functionality (some libraries may have community plugins for additional features)
  • Middleware Support includes traditional middleware, subscriptions, interceptors, and other extensibility patterns
  • Performance data is based on our benchmark suite - run npm run test:comparison for current results
When to Use Substate

Perfect for:

  • Any size application that needs reactive state with automatic memory management
  • Rapid prototyping where you want full features without configuration overhead
  • Projects requiring unidirectional data binding (unique sync functionality)
  • Applications with complex nested state (dot notation updates)
  • Teams that want minimal setup with enterprise-grade features
  • Long-running applications where memory management is critical
  • Time-travel debugging and comprehensive state history requirements
  • High-frequency updates with configurable memory optimization

Especially great for:

  • Real-time applications (automatic memory limits prevent bloat)
  • Form-heavy applications (sync functionality + memory management)
  • Development and debugging (built-in time travel + memory monitoring)
  • Production apps that need to scale without memory leaks

Consider alternatives for:

  • Extremely large enterprise apps with complex distributed state (consider Redux + RTK for strict patterns)
  • Teams requiring specific architectural constraints (Redux enforces stricter patterns)
  • Projects already heavily invested in other state solutions with extensive tooling
Migration Benefits

From Redux:

  • Significantly less boilerplate - No action creators, reducers, or complex setup
  • Built-in time travel without DevTools dependency
  • Automatic memory management - No manual cleanup required
  • Simpler middleware system with before/after hooks
  • Built-in monitoring tools for performance optimization

From Context API:

  • Better performance with granular updates and memory limits
  • Built-in state history with configurable retention
  • Advanced synchronization capabilities (unique to Substate)
  • Smaller bundle size with more features
  • No memory leaks from unbounded state growth

From Zustand:

  • Unique sync functionality for unidirectional data binding
  • Complete state history with automatic memory management
  • Built-in TypeScript support with comprehensive types
  • Flexible nested property handling with dot notation
  • Built-in memory monitoring and optimization tools

From Vanilla State Management:

  • Structured approach without architectural overhead
  • Automatic immutability and history tracking
  • Memory management prevents common memory leak issues
  • Developer tools built-in (no external dependencies)

What Makes Substate Unique

Substate is one of the few state management libraries that combines all these features out of the box:

  1. Built-in Sync System - Unidirectional data binding with middleware transformations
  2. Intelligent Memory Management - Automatic history limits with manual controls
  3. Zero-Config Time Travel - Full debugging without external tools
  4. Tagged State Checkpoints - Named snapshots for easy navigation
  5. Performance Monitoring - Built-in memory usage tracking
  6. Flexible Nested Updates - Intuitive nested state management with dot notation or object spread
  7. Production Ready - Optimized defaults that scale from prototype to enterprise

Key Insight: Most libraries make you choose between features and simplicity. Substate gives you enterprise-grade capabilities with a learning curve measured in minutes, not weeks.

TypeScript Definitions

Core Interfaces
interface ISubstate extends IPubSub {
  name: string;
  afterUpdate: UpdateMiddleware[];
  beforeUpdate: UpdateMiddleware[];
  currentState: number;
  stateStorage: IState[];
  defaultDeep: boolean;
  
  getState(index: number): IState;
  getCurrentState(): IState;
  getProp(prop: string): unknown;
  resetState(): void;
  updateState(action: IState): void;
  sync(config: ISyncConfig): () => void;
}

interface ICreateStoreConfig {
  name: string;
  state?: object;
  defaultDeep?: boolean;
  beforeUpdate?: UpdateMiddleware[];
  afterUpdate?: UpdateMiddleware[];
}

interface IState {
  [key: string]: unknown;
  $type?: string;
  $deep?: boolean;
}

interface ISyncConfig {
  readerObj: Record<string, unknown>;
  stateField: string;
  readField?: string;
  beforeUpdate?: BeforeMiddleware[];
  afterUpdate?: AfterMiddleware[];
}
Middleware Types
// Update middleware for state changes
type TUpdateMiddleware = (store: ISubstate, action: Partial<TUserState>) => void;

// Sync middleware for unidirectional data binding
type TSyncMiddleware = (value: unknown, context: ISyncContext, store: ISubstate) => unknown;

// Sync configuration with middleware support
type TSyncConfig = {
  readerObj: Record<string, unknown> | object;
  stateField: string;
  readField?: string;
  beforeUpdate?: TSyncMiddleware[];
  afterUpdate?: TSyncMiddleware[];
  syncEvents?: string[] | string;
};

// Context provided to sync middleware
interface ISyncContext {
  source: string;
  field: string;
  readField: string;
}

// State keywords for special functionality
type TStateKeywords = {
  $type?: string;
  $deep?: boolean;
  $tag?: string;
  [key: string]: unknown;
};

// User-defined state with keyword support
type TUserState = object & TStateKeywords;

Migration Guide

Version 10.x Migration

Substate v10 introduces several improvements and breaking changes. Here's how to upgrade:

Breaking Changes
  1. Import Changes
// ❌ Old (v9)
import Substate from 'substate';

// ✅ New (v10)
import { createStore, Substate } from 'substate';
  1. Store Creation
// ❌ Old (v9)
const store = new Substate({ name: 'MyStore', state: { count: 0 } });

// ✅ New (v10) - Recommended
const store = createStore({ name: 'MyStore', state: { count: 0 } });

// ✅ New (v10) - Still works but not recommended
const store = new Substate({ name: 'MyStore', state: { count: 0 } });
  1. Peer Dependencies
# Install peer dependencies
npm install clone-deep object-bystring
New Features in v10
  • Sync Method: Unidirectional data binding with middleware
  • Enhanced TypeScript: Better type inference and safety
  • Improved Performance: Optimized event handling and state updates
  • Better Tree Shaking: Only import what you use
Migration Steps
  1. Update imports and installation
npm install substate@10 clone-deep object-bystring
  1. Replace direct instantiation with createStore
// Before
const stores = [
  new Substate({ name: 'Store1', state: { data: [] } }),
  new Substate({ name: 'Store2', state: { user: null } })
];

// After  
const stores = [
  createStore({ name: 'Store1', state: { data: [] } }),
  createStore({ name: 'Store2', state: { user: null } })
];
  1. Leverage new sync functionality
// New capability - sync store to UI models
const unsync = store.sync({
  readerObj: uiModel,
  stateField: 'user.profile',
  readField: 'userInfo'
});
From Other Libraries
From Redux
// Redux setup
const store = createStore(rootReducer);
store.dispatch({ type: 'INCREMENT', payload: 1 });

// Substate equivalent
const store = createStore({ name: 'Counter', state: { count: 0 } });
store.updateState({ count: store.getProp('count') + 1 });
From Zustand
// Zustand
const useStore = create((set) => ({
  count: 0,
  increment: () => set((state) => ({ count: state.count + 1 }))
}));

// Substate
const store = createStore({ name: 'Counter', state: { count: 0 } });
const increment = () => store.updateState({ 
  count: store.getProp('count') + 1 
});

Development

Project Structure
substate/
├── src/
│   ├── index.ts                    # Main exports and type definitions
│   ├── index.test.ts               # Main export tests
│   └── core/
│       ├── consts.ts               # Event constants and shared values
│       ├── createStore/
│       │   ├── createStore.ts      # Factory function for store creation
│       │   └── createStore.interface.ts
│       ├── Substate/
│       │   ├── Substate.ts         # Main Substate class implementation
│       │   ├── Substate.interface.ts # Substate class interfaces
│       │   ├── interfaces.ts       # Type definitions for state and middleware
│       │   ├── helpers/            # Utility functions for optimization
│       │   │   ├── canUseFastPath.ts
│       │   │   ├── checkForFastPathPossibility.ts
│       │   │   ├── isDeep.ts
│       │   │   ├── requiresByString.ts
│       │   │   ├── tempUpdate.ts
│       │   │   └── tests/          # Helper function tests
│       │   └── tests/              # Substate class tests
│       │       ├── Substate.test.ts
│       │       ├── sync.test.ts    # Sync functionality tests
│       │       ├── tagging.test.ts # Tag functionality tests
│       │       ├── memory-management.test.ts
│       │       └── mocks.ts        # Test utilities
│       └── PubSub/
│           ├── PubSub.ts           # Event system base class
│           ├── PubSub.interface.ts
│           └── PubSub.test.ts
│   └── integrations/               # Framework-specific integrations
│       ├── preact/                 # Preact hooks and components
│       └── react/                  # React hooks and components
├── dist/                           # Compiled output (ESM, UMD, declarations)
├── coverage/                       # Test coverage reports
├── integration-tests/              # End-to-end integration tests
│   ├── lit-vite/                   # Lit integration test
│   ├── preact-vite/                # Preact integration test
│   └── react-vite/                 # React integration test
├── benchmark-comparisons/          # Performance comparison suite
├── performance-tests/              # Internal performance testing
└── scripts/                        # Build and utility scripts
Contributing
  1. Fork the repository
  2. Create a feature branch: git checkout -b feature/amazing-feature
  3. Make your changes with tests
  4. Run tests: npm test
  5. Run linting: npm run lint:fix
  6. Commit your changes: git commit -m 'Add amazing feature'
  7. Push to the branch: git push origin feature/amazing-feature
  8. Open a Pull Request
Scripts
Core Development
npm run build         # Build all distributions (ESM, UMD, declarations)
npm run clean         # Clean dist directory
npm run fix           # Auto-fix formatting and linting issues
npm run format        # Format code with Biome
npm run lint          # Check code linting with Biome
npm run check         # Run Biome checks on source code
Testing Suite
npm test              # Run all tests (core + integration)
npm run test:core     # Run core unit tests only
npm run test:watch    # Run tests in watch mode
npm run test:coverage # Run tests with coverage report
npm run test:all      # Comprehensive test suite (check + test + builds + integrations + perf)
Build Testing
npm run test:builds   # Test both ESM and UMD builds
npm run _test:esm     # Test ESM build specifically
npm run _test:umd     # Test UMD build specifically
Performance Testing
npm run test:perf           # Run all performance tests (shallow + deep)
npm run _test:perf:shallow   # Shallow state performance test
npm run _test:perf:deep      # Deep state performance test
npm run test:perf:avg        # Run performance tests with 5-run averages
npm run _test:perf:shallow:avg # Shallow performance with averaging
npm run _test:perf:deep:avg    # Deep performance with averaging
Integration Testing
npm run test:integrations           # Run all integration tests
npm run _test:integrations:check     # Check dependency compatibility
npm run _test:integration:react      # Test React integration
npm run _test:integration:preact     # Test Preact integration
Isolation Testing
npm run test:isolation # Test module isolation and integrity
Development Servers
npm run dev:react     # Start React integration dev server
npm run dev:preact    # Start Preact integration dev server
Setup and Maintenance
npm run integration:setup           # Setup all integration test environments
npm run _integration:setup:react    # Setup React integration only
npm run _integration:setup:preact   # Setup Preact integration only
npm run reset                       # Clear all dependencies and reinstall
npm run refresh                     # Clean install and setup integrations
Performance Benchmarking
npm run benchmark   # Run performance comparisons vs other libraries
Publishing
npm run pre         # Pre-publish checks (test + build) - publishes to 'next' tag
npm run safe-publish # Full publish pipeline (test + build + publish)

Contributing

Contributions are welcome! Please read our contributing guidelines and submit pull requests to help improve Substate.

License

MIT Tom Saporito "Tamb"


Made with for developers who want powerful state management without the complexity.

, '€') : formattedPrice; } ], afterUpdate: [ // Log the transformation (finalValue, context) => { console.log(`Price synced: ${finalValue} for field ${context.readField}`); } ] }); const unsyncName = productStore.sync({ readerObj: displayModel, stateField: 'name', readField: 'productTitle', beforeUpdate: [ // Transform to title case (name) => name.split(' ').map(word => word.charAt(0).toUpperCase() + word.slice(1) ).join(' ') ] }); console.log(displayModel.formattedPrice); // '$29.99' console.log(displayModel.productTitle); // 'Awesome Widget' // Update triggers all synced transformations productStore.updateState({ price: 39.99, name: 'super awesome widget' }); console.log(displayModel.formattedPrice); // '$39.99' console.log(displayModel.productTitle); // 'Super Awesome Widget'
Real-world Sync Example: Form Binding
__CODE_BLOCK_25__
Multiple Sync Instances

You can sync the same state field to multiple targets with different transformations:

__CODE_BLOCK_26__
TypeScript Support
__CODE_BLOCK_27__

API Reference

createStore(config)

Factory function to create a new Substate store with a clean, intuitive API.

__CODE_BLOCK_28__

Parameters:

Property Type Required Default Description
__INLINE_CODE_24__ __INLINE_CODE_25__ - Unique identifier for the store
__INLINE_CODE_26__ __INLINE_CODE_27__ __INLINE_CODE_28__ Initial state object
__INLINE_CODE_29__ __INLINE_CODE_30__ __INLINE_CODE_31__ Enable deep cloning by default for all updates
__INLINE_CODE_32__ __INLINE_CODE_33__ __INLINE_CODE_34__ Functions called before each state update
__INLINE_CODE_35__ __INLINE_CODE_36__ __INLINE_CODE_37__ Functions called after each state update
__INLINE_CODE_38__ __INLINE_CODE_39__ __INLINE_CODE_40__ Maximum number of states to keep in history

Returns: A new __INLINE_CODE_41__ instance

Example:

__CODE_BLOCK_29__
Store Methods
__INLINE_CODE_42__

Updates the current state with new values. Supports both shallow and deep merging.

__CODE_BLOCK_30__

Parameters:

  • __INLINE_CODE_43__ - Object containing the properties to update
  • __INLINE_CODE_44__ (optional) - Force deep cloning for this update
  • __INLINE_CODE_45__ (optional) - Custom identifier for this update
  • __INLINE_CODE_46__ (optional) - Tag name to create a named checkpoint of this state

__INLINE_CODE_47__

Updates multiple properties at once for better performance. This method is optimized for bulk operations and provides significant performance improvements over multiple individual __INLINE_CODE_48__ calls.

__CODE_BLOCK_31__

Performance Benefits:

  • Single state clone instead of multiple clones
  • One event emission instead of multiple events
  • Reduced middleware calls (if using middleware)
  • Better memory efficiency

When to Use:

  • Multiple related updates that should happen together
  • Performance-critical code with frequent state changes
  • Bulk operations like form submissions or data imports
  • Reducing re-renders in React/Preact components

Parameters:

  • __INLINE_CODE_49__ - Array of update action objects (same format as __INLINE_CODE_50__)

Smart Optimization: The method automatically detects if it can use the fast path (no middleware, no deep cloning, no tagging) and processes all updates in a single optimized operation. If any action requires the full feature set, it falls back to processing each action individually.

Example Use Cases:

__CODE_BLOCK_32__
__INLINE_CODE_51__

Returns the current active state object.

__CODE_BLOCK_33__
__INLINE_CODE_52__

Retrieves a specific property from the current state using dot notation for nested access.

__CODE_BLOCK_34__
__INLINE_CODE_53__

Returns a specific state from the store's history by index.

__CODE_BLOCK_35__
__INLINE_CODE_54__

Resets the store to its initial state (index 0) and emits an __INLINE_CODE_55__ event.

__CODE_BLOCK_36__
__INLINE_CODE_56__

Creates unidirectional data binding between a state property and a target object.

__CODE_BLOCK_37__

Parameters:

Property Type Required Description
__INLINE_CODE_57__ __INLINE_CODE_58__ Target object to sync to
__INLINE_CODE_59__ __INLINE_CODE_60__ State property to watch (supports dot notation)
__INLINE_CODE_61__ __INLINE_CODE_62__ Target property name (defaults to __INLINE_CODE_63__)
__INLINE_CODE_64__ __INLINE_CODE_65__ Transform functions applied before sync
__INLINE_CODE_66__ __INLINE_CODE_67__ Side-effect functions called after sync

Returns: Function to call for cleanup (removes event listeners)


__INLINE_CODE_68__

Clears all state history except the current state to free up memory.

__CODE_BLOCK_38__

Use cases:

  • Memory optimization in long-running applications
  • Cleaning up after bulk operations
  • Preparing for application state snapshots

__INLINE_CODE_69__

Sets a new limit for state history size and trims existing history if necessary.

__CODE_BLOCK_39__

Parameters:

  • __INLINE_CODE_70__ - Maximum number of states to keep (minimum: 1)

Throws: Error if __INLINE_CODE_71__ is less than 1


__INLINE_CODE_72__

Returns estimated memory usage information for performance monitoring.

__CODE_BLOCK_40__

Returns:

  • __INLINE_CODE_73__ - Number of states currently stored
  • __INLINE_CODE_74__ - Number of tagged states currently stored
  • __INLINE_CODE_75__ - Rough estimation of memory usage in kilobytes

Note: Size estimation is approximate and based on JSON serialization size.


__INLINE_CODE_76__

Retrieves a tagged state by its tag name without affecting the current state.

__CODE_BLOCK_41__

Parameters:

  • __INLINE_CODE_77__ - The tag name to look up

Returns: Deep cloned tagged state or __INLINE_CODE_78__ if tag doesn't exist


__INLINE_CODE_79__

Returns an array of all available tag names.

__CODE_BLOCK_42__

Returns: Array of tag names currently stored


__INLINE_CODE_80__

Jumps to a tagged state, making it the current state and adding it to history.

__CODE_BLOCK_43__

Parameters:

  • __INLINE_CODE_81__ - The tag name to jump to

Throws: Error if the tag doesn't exist

Events: Emits __INLINE_CODE_82__ and __INLINE_CODE_83__


__INLINE_CODE_84__

Removes a tag from the tagged states collection.

__CODE_BLOCK_44__

Parameters:

  • __INLINE_CODE_85__ - The tag name to remove

Returns: __INLINE_CODE_86__ if tag was found and removed, __INLINE_CODE_87__ if it didn't exist

Events: Emits __INLINE_CODE_88__ for existing tags


__INLINE_CODE_89__

Removes all tagged states from the collection.

__CODE_BLOCK_45__

Events: Emits __INLINE_CODE_90__ with count of cleared tags


Event Methods (Inherited from PubSub)
__INLINE_CODE_91__

Subscribe to store events. Substate emits several built-in events for different operations.

Built-in Events:

Event When Emitted Data Payload
__INLINE_CODE_92__ After any state update __INLINE_CODE_93__
__INLINE_CODE_94__ When __INLINE_CODE_95__ is called None
__INLINE_CODE_96__ When __INLINE_CODE_97__ is called __INLINE_CODE_98__
__INLINE_CODE_99__ When __INLINE_CODE_100__ removes an existing tag __INLINE_CODE_101__
__INLINE_CODE_102__ When __INLINE_CODE_103__ is called __INLINE_CODE_104__
__INLINE_CODE_105__ When __INLINE_CODE_106__ is called __INLINE_CODE_107__
__INLINE_CODE_108__ When __INLINE_CODE_109__ is called __INLINE_CODE_110__
__CODE_BLOCK_46__
__INLINE_CODE_111__

Emit custom events to all subscribers.

__CODE_BLOCK_47__
__INLINE_CODE_112__

Unsubscribe from store events.

__CODE_BLOCK_48__
Store Properties
Property Type Description
__INLINE_CODE_113__ __INLINE_CODE_114__ Store identifier
__INLINE_CODE_115__ __INLINE_CODE_116__ Index of current state in history
__INLINE_CODE_117__ __INLINE_CODE_118__ Array of all state versions
__INLINE_CODE_119__ __INLINE_CODE_120__ Default deep cloning setting
__INLINE_CODE_121__ __INLINE_CODE_122__ Maximum number of states to keep in history
__INLINE_CODE_123__ __INLINE_CODE_124__ Pre-update middleware functions
__INLINE_CODE_125__ __INLINE_CODE_126__ Post-update middleware functions
Middleware Order

updateState(action) ├── store.beforeUpdate[] (store-wide) ├── State Processing │ ├── Clone state │ ├── Apply temp updates │ ├── Push to history │ └── Update tagged states ├── sync.beforeUpdate[] (per sync instance) ├── sync.afterUpdate[] (per sync instance) ├── store.afterUpdate[] (store-wide) └── emit STATE_UPDATED or __INLINE_CODE_127__ event

Memory Management

Substate automatically manages memory through configurable history limits and provides tools for monitoring and optimization.

Automatic History Management

By default, Substate keeps the last 50 states in memory. This provides excellent debugging capabilities while preventing unbounded memory growth:

__CODE_BLOCK_49__
Memory Optimization Strategies
For Small Applications (Default)
__CODE_BLOCK_50__
For High-Frequency Updates
__CODE_BLOCK_51__
For Large State Objects
__CODE_BLOCK_52__
For Debugging vs Production
__CODE_BLOCK_53__
Memory Monitoring

Use the built-in monitoring tools to track memory usage:

__CODE_BLOCK_54__
Best Practices
  1. Choose appropriate limits: 50 states for normal apps, 10-20 for high-frequency updates
  2. Monitor memory usage: Use __INLINE_CODE_128__ to track growth patterns
  3. Clean up after bulk operations: Call __INLINE_CODE_129__ after large imports/updates
  4. Balance debugging vs performance: More history = better debugging, less history = better performance
  5. Adjust dynamically: Use __INLINE_CODE_130__ to adapt to different application modes
Performance Impact

The default settings are optimized for most use cases:

  • Memory: ~50KB - 5MB typical usage depending on state size
  • Performance: Negligible impact with default 50-state limit
  • Time Travel: Full debugging capabilities maintained
  • Automatic cleanup: No manual intervention required

Note: The 50-state default is designed for smaller applications. For enterprise applications with large state objects or high-frequency updates, consider customizing __INLINE_CODE_131__ based on your specific memory constraints.

Performance Benchmarks

Substate delivers excellent performance across different use cases. Here are real benchmark results from our test suite (averaged over 5 runs for statistical accuracy):

Test Environment: 13th Gen Intel(R) Core(TM) i7-13650HX (14 cores), 16 GB RAM, Windows 10 Home

Shallow State Performance
State Size Store Creation Single Update Avg Update Property Access Memory (50 states)
Small (10 props) 41μs 61μs 1.41μs 0.15μs 127KB
Medium (100 props) 29μs 63μs 25.93μs 0.15μs 1.3MB
Large (1000 props) 15μs 598μs 254μs 0.32μs 12.8MB
Deep State Performance
Complexity Store Creation Deep Update Deep Access Deep Clone Memory Usage
Shallow Deep (1.2K nodes) 52μs 428μs 0.90μs 200μs 10.4MB
Medium Deep (5.7K nodes) 39μs 694μs 0.75μs 705μs 45.8MB
Very Deep (6K nodes) 17μs 754μs 0.90μs 788μs 43.3MB
Key Performance Insights
  • Ultra-fast property access: Sub-microsecond access times regardless of state size
  • Efficient updates: Shallow updates scale linearly, deep cloning adds ~10-100x overhead (expected)
  • Smart memory management: Automatic history limits prevent unbounded growth
  • Consistent performance: Property access speed stays constant as state grows
  • Scalable architecture: Handles 1000+ properties with <300μs update times
Real-World Performance
__CODE_BLOCK_55__

Benchmark Environment:

  • Hardware: 13th Gen Intel(R) Core(TM) i7-13650HX (14 cores), 16 GB RAM
  • OS: Windows 10 Home (Version 2009)
  • Runtime: Node.js v18+
  • Method: Averaged over 5 runs for statistical accuracy

Your results may vary based on hardware and usage patterns.

Why Choose Substate?

Comparison with Other State Management Solutions
Feature Substate Redux Zustand Valtio MobX
Bundle Size ~11KB ~4KB ~2KB ~7KB ~63KB
TypeScript Excellent Excellent Excellent Excellent Excellent
Learning Curve Low High Low Medium High
Boilerplate Minimal Heavy Minimal Minimal Some
Time Travel Built-in DevTools No No No
Memory Management Auto + Manual Manual only Manual only Manual only Manual only
Immutability Auto Manual Manual Auto Mutable
Sync/Binding Built-in No No No Yes
Framework Agnostic Yes Yes Yes Yes Yes
Middleware Support Simple Complex Yes Yes Yes
Nested Updates Dot notation + Object spread Reducers Manual Direct Direct
Tagged States Built-in No No No No

NOTE: Clone our repo and run the benchmarks to see how we stack up!

About This Comparison:

  • Bundle sizes are approximate and may vary by version
  • Learning curve and boilerplate assessments are subjective and based on typical developer experience
  • Feature availability is based on core functionality (some libraries may have community plugins for additional features)
  • Middleware Support includes traditional middleware, subscriptions, interceptors, and other extensibility patterns
  • Performance data is based on our benchmark suite - run __INLINE_CODE_132__ for current results
When to Use Substate

Perfect for:

  • Any size application that needs reactive state with automatic memory management
  • Rapid prototyping where you want full features without configuration overhead
  • Projects requiring unidirectional data binding (unique sync functionality)
  • Applications with complex nested state (dot notation updates)
  • Teams that want minimal setup with enterprise-grade features
  • Long-running applications where memory management is critical
  • Time-travel debugging and comprehensive state history requirements
  • High-frequency updates with configurable memory optimization

Especially great for:

  • Real-time applications (automatic memory limits prevent bloat)
  • Form-heavy applications (sync functionality + memory management)
  • Development and debugging (built-in time travel + memory monitoring)
  • Production apps that need to scale without memory leaks

Consider alternatives for:

  • Extremely large enterprise apps with complex distributed state (consider Redux + RTK for strict patterns)
  • Teams requiring specific architectural constraints (Redux enforces stricter patterns)
  • Projects already heavily invested in other state solutions with extensive tooling
Migration Benefits

From Redux:

  • Significantly less boilerplate - No action creators, reducers, or complex setup
  • Built-in time travel without DevTools dependency
  • Automatic memory management - No manual cleanup required
  • Simpler middleware system with before/after hooks
  • Built-in monitoring tools for performance optimization

From Context API:

  • Better performance with granular updates and memory limits
  • Built-in state history with configurable retention
  • Advanced synchronization capabilities (unique to Substate)
  • Smaller bundle size with more features
  • No memory leaks from unbounded state growth

From Zustand:

  • Unique sync functionality for unidirectional data binding
  • Complete state history with automatic memory management
  • Built-in TypeScript support with comprehensive types
  • Flexible nested property handling with dot notation
  • Built-in memory monitoring and optimization tools

From Vanilla State Management:

  • Structured approach without architectural overhead
  • Automatic immutability and history tracking
  • Memory management prevents common memory leak issues
  • Developer tools built-in (no external dependencies)

What Makes Substate Unique

Substate is one of the few state management libraries that combines all these features out of the box:

  1. Built-in Sync System - Unidirectional data binding with middleware transformations
  2. Intelligent Memory Management - Automatic history limits with manual controls
  3. Zero-Config Time Travel - Full debugging without external tools
  4. Tagged State Checkpoints - Named snapshots for easy navigation
  5. Performance Monitoring - Built-in memory usage tracking
  6. Flexible Nested Updates - Intuitive nested state management with dot notation or object spread
  7. Production Ready - Optimized defaults that scale from prototype to enterprise

Key Insight: Most libraries make you choose between features and simplicity. Substate gives you enterprise-grade capabilities with a learning curve measured in minutes, not weeks.

TypeScript Definitions

Core Interfaces
__CODE_BLOCK_56__
Middleware Types
__CODE_BLOCK_57__

Migration Guide

Version 10.x Migration

Substate v10 introduces several improvements and breaking changes. Here's how to upgrade:

Breaking Changes
  1. Import Changes
__CODE_BLOCK_58__
  1. Store Creation
__CODE_BLOCK_59__
  1. Peer Dependencies
__CODE_BLOCK_60__
New Features in v10
  • Sync Method: Unidirectional data binding with middleware
  • Enhanced TypeScript: Better type inference and safety
  • Improved Performance: Optimized event handling and state updates
  • Better Tree Shaking: Only import what you use
Migration Steps
  1. Update imports and installation
__CODE_BLOCK_61__
  1. Replace direct instantiation with createStore
__CODE_BLOCK_62__
  1. Leverage new sync functionality
__CODE_BLOCK_63__
From Other Libraries
From Redux
__CODE_BLOCK_64__
From Zustand
__CODE_BLOCK_65__

Development

Project Structure
__CODE_BLOCK_66__
Contributing
  1. Fork the repository
  2. Create a feature branch: __INLINE_CODE_133__
  3. Make your changes with tests
  4. Run tests: __INLINE_CODE_134__
  5. Run linting: __INLINE_CODE_135__
  6. Commit your changes: __INLINE_CODE_136__
  7. Push to the branch: __INLINE_CODE_137__
  8. Open a Pull Request
Scripts
Core Development
__CODE_BLOCK_67__
Testing Suite
__CODE_BLOCK_68__
Build Testing
__CODE_BLOCK_69__
Performance Testing
__CODE_BLOCK_70__
Integration Testing
__CODE_BLOCK_71__
Isolation Testing
__CODE_BLOCK_72__
Development Servers
__CODE_BLOCK_73__
Setup and Maintenance
__CODE_BLOCK_74__
Performance Benchmarking
__CODE_BLOCK_75__
Publishing
__CODE_BLOCK_76__

Contributing

Contributions are welcome! Please read our contributing guidelines and submit pull requests to help improve Substate.

License

MIT Tom Saporito "Tamb"


Made with for developers who want powerful state management without the complexity.