Movatterモバイル変換


[0]ホーム

URL:


Skip to content

Navigation Menu

Sign in
Appearance settings

Search code, repositories, users, issues, pull requests...

Provide feedback

We read every piece of feedback, and take your input very seriously.

Saved searches

Use saved searches to filter your results more quickly

Sign up
Appearance settings

pub/sub state management with optional deep cloning

License

NotificationsYou must be signed in to change notification settings

tamb/substate

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

npm versionnpm downloadsnpm bundle sizecoveragelicenseTypeScript

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 ~11KB

Substate is designed to be minimal yet powerful. The core library weighs just ~11KB minified, making it perfect for applications where bundle size matters.

import{createStore}from'substate';// Only ~11KB total

🔒Type-safe - Full TypeScript support with comprehensive type definitions

Complete TypeScript support with advanced type inference, ensuring type safety throughout your application.

interfaceUser{id:number;name:string;email:string;}constuserStore=createStore<User>({name:'UserStore',state:{user:nullasUser|null,loading:false}});// TypeScript knows the exact shape of your stateuserStore.updateState({user:{id:1,name:'John',email:'john@example.com'}});

🔄Reactive - Built-in Pub/Sub pattern for reactive state updates

Event-driven architecture with built-in subscription system for reactive updates.

conststore=createStore({name:'ReactiveStore',state:{count:0}});// Subscribe to state changesstore.on('UPDATE_STATE',(newState)=>{console.log('State updated:',newState);// React to changes automatically});

🕰️Time Travel - Complete state history with ability to navigate between states

Full state history with configurable memory management. Navigate through state changes like a debugger.

conststore=createStore({name:'TimeTravelStore',state:{count:0}});// Make some changesstore.updateState({count:1});store.updateState({count:2});store.updateState({count:3});// Go back in timeconsole.log(store.getState(0));// { count: 0 } - initial stateconsole.log(store.getState(1));// { count: 1 } - first updateconsole.log(store.getState(2));// { count: 2 } - second update

🏷️Tagged States - Named checkpoints for easy state restoration

Create named checkpoints in your state history for easy navigation and debugging.

constgameStore=createStore({name:'GameStore',state:{level:1,score:0}});// Create a checkpointgameStore.updateState({level:5,score:1250,$tag:'level-5-start'});// Later, jump back to that checkpointgameStore.jumpToTag('level-5-start');console.log(gameStore.getCurrentState());// { level: 5, score: 1250 }

🎯Immutable - Automatic deep cloning prevents accidental state mutations

Automatic deep cloning ensures immutability by default, preventing accidental mutations.

conststore=createStore({name:'ImmutableStore',state:{user:{name:'John',settings:{theme:'dark'}}}});// State is automatically deep cloned - original object is safeconstoriginalState=store.getCurrentState();originalState.user.name='Jane';// This won't affect the storeconsole.log(store.getProp('user.name'));// Still 'John'

🔗Sync - Unidirectional data binding with middleware transformations

Connect your store to UI components or external systems with powerful sync capabilities.

conststore=createStore({name:'SyncStore',state:{price:29.99}});// Sync to UI with formattingconstuiModel={formattedPrice:''};constunsync=store.sync({readerObj:uiModel,stateField:'price',readField:'formattedPrice',beforeUpdate:[(price)=>`$${price.toFixed(2)}`]});console.log(uiModel.formattedPrice);// '$29.99'

🎪Middleware - Extensible with before/after update hooks

Powerful middleware system for logging, validation, persistence, and custom logic.

conststore=createStore({name:'MiddlewareStore',state:{count:0},beforeUpdate:[(store,action)=>{console.log('About to update:',action);// Validation logic here}],afterUpdate:[(store,action)=>{console.log('Updated to:',store.getCurrentState());// Persistence logic here}]});

🌳Nested Props - Easy access to nested properties with optional dot notation or standard object spread

Flexible nested property access with both dot notation convenience and object spread patterns.

conststore=createStore({name:'NestedStore',state:{user:{profile:{name:'John',email:'john@example.com'}}}});// Dot notation (convenient)store.updateState({'user.profile.name':'Jane'});// Object spread (explicit)store.updateState({user:{    ...store.getProp('user'),profile:{      ...store.getProp('user.profile'),name:'Jane'}}});// Both approaches work equally well

📦Framework Agnostic - Works with any JavaScript framework or vanilla JS

No framework dependencies - use with React, Vue, Angular, Svelte, or vanilla JavaScript.

// Vanilla JavaScriptconststore=createStore({name:'VanillaStore',state:{count:0}});// Reactimport{useEffect,useState}from'react';functionCounter(){const[count,setCount]=useState(0);useEffect(()=>{store.on('UPDATE_STATE',(state)=>setCount(state.count));},[]);}// Vue, Angular, Svelte - works with all frameworks!

📦 Installation

npm

npm install substate

yarn

yarn add substate

pnpm

pnpm add substate

Requirements

  • Node.js: >= 16.0.0
  • TypeScript: >= 4.5.0 (for TypeScript support)
  • Peer Dependencies:clone-deep,object-bystring

Bundle Size

  • Minified: ~11KB
  • Gzipped: ~4KB

CDN

<!-- UMD Build --><scriptsrc="https://cdn.jsdelivr.net/npm/substate@latest/dist/index.umd.js"></script><!-- ESM Build --><scripttype="module">import{createStore}from'https://cdn.jsdelivr.net/npm/substate@latest/dist/index.esm.js';</script>

🚀 Quick Start

Installation & Basic Usage

npm install substate
import{createStore}from'substate';// Create a simple counter storeconstcounterStore=createStore({name:'CounterStore',state:{count:0,lastUpdated:Date.now()}});// Update statecounterStore.updateState({count:1});console.log(counterStore.getCurrentState());// { count: 1, lastUpdated: 1234567890 }// Listen to changescounterStore.on('UPDATE_STATE',(newState)=>{console.log('Counter updated:',newState.count);});

Next Steps

  1. 📚 Usage Examples - Learn through practical examples
  2. 🔗 Sync - Unidirectional Data Binding - Connect your store to UI components
  3. 🏷️ Tagged States - Save and restore state checkpoints
  4. 📖 API Reference - Complete method documentation

Common Patterns

Counter with Actions

constcounterStore=createStore({name:'Counter',state:{count:0}});// Action functions (optional but recommended)functionincrement(){counterStore.updateState({count:counterStore.getProp('count')+1});}functiondecrement(){counterStore.updateState({count:counterStore.getProp('count')-1});}functionreset(){counterStore.resetState();// Back to initial state}// Subscribe to changescounterStore.on('UPDATE_STATE',(state)=>{console.log('Count changed to:',state.count);});// Use the actionsincrement();// count: 1increment();// count: 2decrement();// count: 1reset();// count: 0

Form State Management

constformStore=createStore({name:'ContactForm',state:{name:'',email:'',message:'',isSubmitting:false,errors:{}}});// Update form fieldsfunctionupdateField(field:string,value:string){formStore.updateState({[field]:value});}// Submit formasyncfunctionsubmitForm(){formStore.updateState({isSubmitting:true,errors:{}});try{awaitsubmitToAPI(formStore.getCurrentState());console.log('Form submitted successfully!');formStore.resetState();// Clear form}catch(error){formStore.updateState({isSubmitting:false,errors:{submit:error.message}});}}// Listen for form changesformStore.on('UPDATE_STATE',(state)=>{if(state.isSubmitting){console.log('Submitting form...');}});

🏷️ Tagged States

Named State Checkpoint System

Tagged states is aNamed 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';constgameStore=createStore({name:'GameStore',state:{level:1,score:0,lives:3}});// Create tagged checkpoints with meaningful namesgameStore.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 namegameStore.jumpToTag("level-5-start");console.log(gameStore.getCurrentState());// { level: 5, score: 1250, lives: 3 }// Access tagged states without changing current stateconstbossState=gameStore.getTaggedState("boss-fight");console.log(bossState);// { level: 10, score: 5000, lives: 2 }// Manage your tagsconsole.log(gameStore.getAvailableTags());// ["level-5-start", "boss-fight"]gameStore.removeTag("level-5-start");

Advanced Checkpoint Patterns

Form Wizard with Step Restoration

constformStore=createStore({name:'FormWizard',state:{currentStep:1,personalInfo:{firstName:'',lastName:'',email:''},addressInfo:{street:'',city:'',zip:''},paymentInfo:{cardNumber:'',expiry:''}}});// Save progress at each completed stepfunctioncompletePersonalInfo(data){formStore.updateState({personalInfo:data,currentStep:2,$tag:"step-1-complete"});}functioncompleteAddressInfo(data){formStore.updateState({addressInfo:data,currentStep:3,$tag:"step-2-complete"});}// User can jump back to any completed stepfunctiongoToStep(stepNumber){conststepTag=`step-${stepNumber}-complete`;if(formStore.getAvailableTags().includes(stepTag)){formStore.jumpToTag(stepTag);}}// UsagegoToStep(1);// Jump back to personal info stepgoToStep(2);// Jump back to address info step

Debugging and Error Recovery

constappStore=createStore({name:'AppStore',state:{userData:null,settings:{},lastError:null}});// Tag known good states for debuggingfunctionmarkKnownGoodState(){appStore.updateState({$tag:"last-known-good"});}// When errors occur, jump back to known good statefunctionhandleError(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 operationsfunctionperformRiskyOperation(){appStore.updateState({$tag:"before-risky-operation"});// ... perform operation that might failif(operationFailed){appStore.jumpToTag("before-risky-operation");}}

Game Save System

constgameStore=createStore({name:'GameStore',state:{player:{health:100,level:1,inventory:[]},world:{currentArea:'town',discoveredAreas:[]},quests:{active:[],completed:[]}}});// Auto-save systemfunctionautoSave(){consttimestamp=newDate().toISOString();gameStore.updateState({$tag:`auto-save-${timestamp}`});}// Manual save systemfunctionmanualSave(saveName){gameStore.updateState({$tag:`save-${saveName}`});}// Load save systemfunctionloadSave(saveName){constsaveTag=`save-${saveName}`;if(gameStore.getAvailableTags().includes(saveTag)){gameStore.jumpToTag(saveTag);returntrue;}returnfalse;}// Get all available savesfunctiongetAvailableSaves(){returngameStore.getAvailableTags().filter(tag=>tag.startsWith('save-')).map(tag=>tag.replace('save-',''));}// UsagemanualSave("checkpoint-1");manualSave("before-boss-fight");loadSave("checkpoint-1");

Feature Flag and A/B Testing

constexperimentStore=createStore({name:'ExperimentStore',state:{features:{},userGroup:null,experimentResults:{}}});// Tag different experiment variantsfunctionsetupExperimentVariant(variant){experimentStore.updateState({userGroup:variant,$tag:`experiment-${variant}`});}// Jump between experiment variantsfunctionswitchToVariant(variant){constvariantTag=`experiment-${variant}`;if(experimentStore.getAvailableTags().includes(variantTag)){experimentStore.jumpToTag(variantTag);}}// UsagesetupExperimentVariant("control");setupExperimentVariant("variant-a");setupExperimentVariant("variant-b");switchToVariant("variant-a");// Switch to variant A

🎯 Common Tagging Patterns

// Form checkpointsformStore.updateState({ ...formData,$tag:"before-validation"});// API operation snapshotsstore.updateState({users:userData,$tag:"after-user-import"});// Feature flags / A-B testingstore.updateState({features:newFeatures,$tag:"experiment-variant-a"});// Debugging checkpointsstore.updateState({debugInfo:data,$tag:"issue-reproduction"});// Game savesgameStore.updateState({ saveData,$tag:`save-${Date.now()}`});// Workflow statesworkflowStore.updateState({status:"approved",$tag:"workflow-approved"});// User session statessessionStore.updateState({user:userData,$tag:"user-logged-in"});

📚 Usage Examples

1. Todo List Management

import{createStore}from'substate';interfaceTodo{id:string;text:string;completed:boolean;}consttodoStore=createStore({name:'TodoStore',state:{todos:[]asTodo[],filter:'all'as'all'|'active'|'completed'},defaultDeep:true});// Add a new todofunctionaddTodo(text:string){constcurrentTodos=todoStore.getProp('todos')asTodo[];todoStore.updateState({todos:[...currentTodos,{id:crypto.randomUUID(),      text,completed:false}]});}// Toggle todo completionfunctiontoggleTodo(id:string){consttodos=todoStore.getProp('todos')asTodo[];todoStore.updateState({todos:todos.map(todo=>todo.id===id ?{ ...todo,completed:!todo.completed} :todo)});}// Subscribe to changestodoStore.on('UPDATE_STATE',(state)=>{console.log(`${state.todos.length} todos, filter:${state.filter}`);});

2. User Authentication Store

import{createStore}from'substate';constauthStore=createStore({name:'AuthStore',state:{user:null,isAuthenticated:false,loading:false,error:null},beforeUpdate:[(store,action)=>{// Log all state changesconsole.log('Auth state changing:',action);}],afterUpdate:[(store,action)=>{// Persist authentication stateif(action.user||action.isAuthenticated!==undefined){localStorage.setItem('auth',JSON.stringify(store.getCurrentState()));}}]});// Login actionasyncfunctionlogin(email:string,password:string){authStore.updateState({loading:true,error:null});try{constuser=awaitauthenticateUser(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';constcartStore=createStore({name:'CartStore',state:{items:[],total:0,tax:0,discount:0},defaultDeep:true,afterUpdate:[// Automatically calculate totals after any update(store)=>{conststate=store.getCurrentState();constsubtotal=state.items.reduce((sum,item)=>sum+(item.price*item.quantity),0);consttax=subtotal*0.08;// 8% taxconsttotal=subtotal+tax-state.discount;// Update calculated fields without triggering infinite loopstore.stateStorage[store.currentState]={        ...state,        total,        tax};}]});functionaddToCart(product){constitems=cartStore.getProp('items');constexistingItem=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

constuserStore=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 caseuserStore.updateState({'profile.preferences.theme':'light'});// Dot notationuserStore.updateState({profile:{     ...userStore.getProp('profile'),preferences:{       ...userStore.getProp('profile.preferences'),theme:'light'}}});// Object spread// Get nested propertiesconsole.log(userStore.getProp('profile.personal.name'));// 'Jane Doe'console.log(userStore.getProp('profile.preferences'));// { theme: 'light', notifications: true }

🎯 Framework Integrations

React/Preact

import{createStore,typeTState}from"substate";import{useSubstate,useSubstateActions}from"substate/react";// or "substate/preact"typeMyAppState=TState&{firstName:string;}conststore=createStore<MyAppState>({state:{firstName:"Ralph"}});functionMyApp(){const{ state}=useSubstate(store(state)=>state);//whole stateconst{ firstName}=useSubstate(store,(state)=>state.firstName);//state with seletorconst{     updateState,     resetState,     jumpToTag,     getAvailableTags,    getMemoryUsage}=useSubstateActions(store);return(<inputonChange={e=>updateState({firstName:e.target.value})}value={firstName}/>);}

Lit

import{LitElement,html}from'lit'import{customElement,property}from'lit/decorators.js'import{store}from'../../store';// store instance@customElement('counter')exportclassCounterLitElement{privatesyncedCount;constructor(){super();this.syncedCount=store.sync({readerObj:thisasMultiplier,stateField:'count',});}disconnectedCallback(){super.disconnectedCallback();this.syncedCount.unsync();}  @property({type:Number})count=0;render(){returnhtml`<div>${this.count}<buttontype="button"@click=${this._increment}+1</button></div>    `}private_increment(){store.updateState({count:this.count+1});}}declare global{interfaceHTMLElementTagNameMap{'multiplier-el':Multiplier}}```## 🔗 Sync - Unidirectional Data BindingOne of Substate's most powerful features is the `sync` method, which provides unidirectional data binding between your store and any target object (like UI models, form objects, or API payloads).### Basic Sync Example```typescriptimport{createStore}from'substate';constuserStore=createStore({name:'UserStore',state:{userName:'John',age:25}});// Target object (could be a UI model, form, etc.)constuiModel={displayName:'',userAge:0};// Sync userName from store to displayName in uiModelconstunsync=userStore.sync({readerObj:uiModel,stateField:'userName',readField:'displayName'});console.log(uiModel.displayName);// 'John' - immediately synced// When store updates, uiModel automatically updatesuserStore.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 neededunsync();

Sync with Middleware Transformations

constproductStore=createStore({name:'ProductStore',state:{price:29.99,currency:'USD',name:'awesome widget'}});constdisplayModel={formattedPrice:'',productTitle:''};// Sync with transformation middlewareconstunsyncPrice=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)=>{constcurrency=productStore.getProp('currency');returncurrency==='EUR' ?formattedPrice.replace('$','€') :formattedPrice;}],afterUpdate:[// Log the transformation(finalValue,context)=>{console.log(`Price synced:${finalValue} for field${context.readField}`);}]});constunsyncName=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 transformationsproductStore.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

// Form state storeconstformStore=createStore({name:'FormStore',state:{user:{firstName:'',lastName:'',email:'',birthDate:null},validation:{isValid:false,errors:[]}}});// Form UI object (could be from any UI framework)constformUI={fullName:'',emailInput:'',ageDisplay:'',submitEnabled:false};// Sync full name (combining first + last)constunsyncName=formStore.sync({readerObj:formUI,stateField:'user',readField:'fullName',beforeUpdate:[(user)=>`${user.firstName}${user.lastName}`.trim()]});// Sync email directlyconstunsyncEmail=formStore.sync({readerObj:formUI,stateField:'user.email',readField:'emailInput'});// Sync age calculation from birth dateconstunsyncAge=formStore.sync({readerObj:formUI,stateField:'user.birthDate',readField:'ageDisplay',beforeUpdate:[(birthDate)=>{if(!birthDate)return'Not provided';constage=newDate().getFullYear()-newDate(birthDate).getFullYear();return`${age} years old`;}]});// Sync form validity to submit buttonconstunsyncValid=formStore.sync({readerObj:formUI,stateField:'validation.isValid',readField:'submitEnabled'});// Update form dataformStore.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:

constdataStore=createStore({name:'DataStore',state:{timestamp:Date.now()}});constdashboard={lastUpdate:''};constreport={generatedAt:''};constapi={timestamp:0};// Sync to dashboard with human-readable formatconstunsync1=dataStore.sync({readerObj:dashboard,stateField:'timestamp',readField:'lastUpdate',beforeUpdate:[(ts)=>newDate(ts).toLocaleString()]});// Sync to report with ISO stringconstunsync2=dataStore.sync({readerObj:report,stateField:'timestamp',readField:'generatedAt',beforeUpdate:[(ts)=>newDate(ts).toISOString()]});// Sync to API with raw timestampconstunsync3=dataStore.sync({readerObj:api,stateField:'timestamp'// uses same field name when readField omitted});// One update triggers all syncsdataStore.updateState({timestamp:Date.now()});

TypeScript Support

import{createStore,typeISubstate,typeICreateStoreConfig}from'substate';constconfig:ICreateStoreConfig={name:'TypedStore',state:{count:0},defaultDeep:true};conststore:ISubstate=createStore(config);

📖 API Reference

createStore(config)

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

functioncreateStore(config:ICreateStoreConfig):ISubstate

Parameters:

PropertyTypeRequiredDefaultDescription
namestring-Unique identifier for the store
stateobject{}Initial state object
defaultDeepbooleanfalseEnable deep cloning by default for all updates
beforeUpdateUpdateMiddleware[][]Functions called before each state update
afterUpdateUpdateMiddleware[][]Functions called after each state update
maxHistorySizenumber50Maximum number of states to keep in history

Returns: A newISubstate instance

Example:

conststore=createStore({name:'MyStore',state:{count:0},defaultDeep:true,maxHistorySize:25,// Keep only last 25 states for memory efficiencybeforeUpdate:[(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 updatestore.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 updatestore.updateState({data:complexObject,$deep:true});// Update with custom type identifierstore.updateState({items:newItems,$type:'BULK_UPDATE'});// Adding a tagstore.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<TSubstateState> & IState>): void

Updates multiple properties at once for better performance. This method is optimized for bulk operations and provides significant performance improvements over multiple individualupdateState() 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 performancestore.batchUpdateState([{counter:1},{user:{name:"John"}},{theme:"dark"}]);// Batch updates with complex operationsstore.batchUpdateState([{'user.profile.name':'Jane'},{'user.profile.email':'jane@example.com'},{'settings.theme':'light'},{'settings.notifications':true}]);// Batch updates with metadatastore.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 asupdateState)

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 fieldsfunctionsubmitForm(formData){store.batchUpdateState([{'form.isSubmitting':true},{'form.data':formData},{'form.errors':[]},{'form.lastSubmitted':Date.now()}]);}// Bulk data importfunctionimportData(items){store.batchUpdateState([{'data.items':items},{'data.totalCount':items.length},{'data.lastImport':Date.now()},{'ui.showImportSuccess':true}]);}// User profile updatefunctionupdateProfile(profileData){store.batchUpdateState([{'user.profile':profileData},{'user.lastUpdated':Date.now()},{'ui.profileUpdated':true}]);}

getCurrentState(): IState

Returns the current active state object.

constcurrentState=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 propertyconstcount=store.getProp('count');// 5// Get nested propertyconstuserName=store.getProp('user.profile.name');// 'John'// Get array elementconstfirstItem=store.getProp('items.0.title');// Returns undefined for non-existent propertiesconstmissing=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)constinitialState=store.getState(0);// Get previous stateconstpreviousState=store.getState(store.currentState-1);// Get specific historical stateconstspecificState=store.getState(3);

resetState(): void

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

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

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

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

constunsync=store.sync({readerObj:targetObject,stateField:'user.name',readField:'displayName',beforeUpdate:[(value)=>value.toUpperCase()],afterUpdate:[(value)=>console.log('Synced:',value)]});// Call to cleanup the syncunsync();

Parameters:

PropertyTypeRequiredDescription
readerObjRecord<string, unknown>Target object to sync to
stateFieldstringState property to watch (supports dot notation)
readFieldstringTarget property name (defaults tostateField)
beforeUpdateBeforeMiddleware[]Transform functions applied before sync
afterUpdateAfterMiddleware[]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+ statesstore.clearHistory();console.log(store.stateStorage.length);// 1 stateconsole.log(store.currentState);// 0// Current state is preservedconsole.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 setupstore.limitHistory(10);// Keep only last 10 states// If current history exceeds the limit, it gets trimmedconsole.log(store.stateStorage.length);// Max 10 states// Dynamic adjustment for debuggingif(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 ifmaxSize is less than 1


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

Returns estimated memory usage information for performance monitoring.

constusage=store.getMemoryUsage();console.log(`States:${usage.stateCount}`);console.log(`Estimated Size:${usage.estimatedSizeKB}KB`);// Memory monitoringif(usage.estimatedSizeKB>1000){console.warn('Store using over 1MB of memory');store.clearHistory();// Clean up if needed}// Performance trackingsetInterval(()=>{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 statesstore.updateState({user:userData,$tag:"user-login"});store.updateState({cart:cartData,$tag:"checkout-ready"});// Retrieve specific tagged statesconstloginState=store.getTaggedState("user-login");constcheckoutState=store.getTaggedState("checkout-ready");// Returns undefined for non-existent tagsconstmissing=store.getTaggedState("non-existent");// undefined

Parameters:

  • tag - The tag name to look up

Returns: Deep cloned tagged state orundefined 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 navigationif(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 checkpointsstore.updateState({page:"home",$tag:"home-page"});store.updateState({page:"profile",user:userData,$tag:"profile-page"});store.updateState({page:"settings"});// Jump back to a checkpointstore.jumpToTag("profile-page");console.log(store.getCurrentState().page);// "profile"// Continue from the restored statestore.updateState({page:"edit-profile"});

Parameters:

  • tag - The tag name to jump to

Throws: Error if the tag doesn't exist

Events: EmitsTAG_JUMPED andSTATE_UPDATED


removeTag(tag: string): boolean

Removes a tag from the tagged states collection.

store.updateState({temp:"data",$tag:"temporary"});constwasRemoved=store.removeTag("temporary");console.log(wasRemoved);// true// Tag is now goneconsole.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: EmitsTAG_REMOVED for existing tags


clearTags(): void

Removes all tagged states from the collection.

// After bulk operations with many tagsstore.clearTags();console.log(store.getAvailableTags());// []// State history remains intactconsole.log(store.stateStorage.length);// Still has all states

Events: EmitsTAGS_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:

EventWhen EmittedData Payload
STATE_UPDATEDAfter any state updatenewState: IState
STATE_RESETWhenresetState() is calledNone
TAG_JUMPEDWhenjumpToTag() is called{ tag: string, state: IState }
TAG_REMOVEDWhenremoveTag() removes an existing tag{ tag: string }
TAGS_CLEAREDWhenclearTags() is called{ clearedCount: number }
HISTORY_CLEAREDWhenclearHistory() is called{ previousLength: number }
HISTORY_LIMIT_CHANGEDWhenlimitHistory() is called{ newLimit: number, oldLimit: number, trimmed: number }
// Listen to state updatesstore.on('STATE_UPDATED',(newState:IState)=>{console.log('State changed:',newState);});// Listen to tagging eventsstore.on('TAG_JUMPED',({ tag, state})=>{console.log(`Jumped to tag:${tag}`,state);});// Listen to memory management eventsstore.on('HISTORY_CLEARED',({ previousLength})=>{console.log(`Cleared${previousLength} states from history`);});// Listen to custom eventsstore.on('USER_LOGIN',(userData)=>{console.log('User logged in:',userData);});

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

Emit custom events to all subscribers.

// Emit custom eventstore.emit('USER_LOGIN',{userId:123,name:'John'});// Emit without datastore.emit('CACHE_CLEARED');

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

Unsubscribe from store events.

consthandler=(state)=>console.log(state);store.on('UPDATE_STATE',handler);store.off('UPDATE_STATE',handler);// Removes this specific handler

Store Properties

PropertyTypeDescription
namestringStore identifier
currentStatenumberIndex of current state in history
stateStorageIState[]Array of all state versions
defaultDeepbooleanDefault deep cloning setting
maxHistorySizenumberMaximum number of states to keep in history
beforeUpdateUpdateMiddleware[]Pre-update middleware functions
afterUpdateUpdateMiddleware[]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 last50 states in memory. This provides excellent debugging capabilities while preventing unbounded memory growth:

conststore=createStore({name:'AutoManagedStore',state:{data:[]},maxHistorySize:50// Default - good for most applications});// After 100 updates, only the last 50 states are keptfor(leti=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 appsconststore=createStore({name:'SmallApp',state:{user:null,settings:{}}// maxHistorySize: 50 (default)});

For High-Frequency Updates

// Reduce history for apps with frequent state changesconststore=createStore({name:'RealtimeApp',state:{liveData:[]},maxHistorySize:10// Keep minimal history});// Or dynamically adjustif(isRealtimeMode){store.limitHistory(5);}

For Large State Objects

// Monitor and manage memory proactivelyconststore=createStore({name:'LargeDataApp',state:{dataset:[],cache:{}},maxHistorySize:20});// Regular memory monitoringsetInterval(()=>{const{ stateCount, estimatedSizeKB}=store.getMemoryUsage();if(estimatedSizeKB>5000){// Over 5MBconsole.log('Memory usage high, clearing history...');store.clearHistory();}},30000);

For Debugging vs Production

conststore=createStore({name:'FlexibleApp',state:{app:'data'},maxHistorySize:process.env.NODE_ENV==='development' ?100 :25});// Runtime adjustmentif(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 monitoringfunctionlogMemoryUsage(store:ISubstate,context:string){const{ stateCount, estimatedSizeKB}=store.getMemoryUsage();console.log(`${context}:${stateCount} states, ~${estimatedSizeKB}KB`);}// After bulk operationslogMemoryUsage(store,'After data import');// Regular health checkssetInterval(()=>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: UsegetMemoryUsage() to track growth patterns
  3. 🧹 Clean up after bulk operations: CallclearHistory() after large imports/updates
  4. ⚖️ Balance debugging vs performance: More history = better debugging, less history = better performance
  5. 🔄 Adjust dynamically: UselimitHistory() 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 customizingmaxHistorySize 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 SizeStore CreationSingle UpdateAvg UpdateProperty AccessMemory (50 states)
Small (10 props)41μs61μs1.41μs0.15μs127KB
Medium (100 props)29μs63μs25.93μs0.15μs1.3MB
Large (1000 props)15μs598μs254μs0.32μs12.8MB

🏗️ Deep State Performance

ComplexityStore CreationDeep UpdateDeep AccessDeep CloneMemory Usage
Shallow Deep (1.2K nodes)52μs428μs0.90μs200μs10.4MB
Medium Deep (5.7K nodes)39μs694μs0.75μs705μs45.8MB
Very Deep (6K nodes)17μs754μs0.90μs788μs43.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 updatesconstfastStore=createStore({name:'RealtimeStore',state:{liveData:[]},defaultDeep:false// 1.41μs per update});// ✅ Great for complex nested stateconstcomplexStore=createStore({name:'ComplexStore',state:deepNestedObject,defaultDeep:true// 428μs per deep update});// ✅ Property access is always fastconstvalue=store.getProp('deeply.nested.property');// ~1μs

🆚 Performance Comparison

OperationSubstateNative ObjectReduxZustand
Property Access0.15μs~0.1μs~2-5μs~1-3μs
Shallow Update1.41μs~1μs~50-100μs~20-50μs
Memory ManagementAutomaticManualManualManual
History/Time TravelBuilt-inNoneDevToolsNone

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

🔬 Performance Comparison Benchmarks

Substate includes comprehensive performance benchmarks comparing it with other popular state management libraries. These benchmarks providescientifically accurate performance data based on real measurements, not estimates.

📊 What We Compare

  • Substate - Our lightweight state management library
  • Redux - Industry standard state management
  • Zustand - Modern lightweight alternative
  • Native JavaScript Objects - Baseline performance

🎯 Measured Metrics

  • Store Creation - Time to initialize a new store/state
  • Single Update - Time for individual state updates
  • Batch Updates - Time for multiple updates in sequence
  • Property Access - Time to read state properties
  • Memory Usage - Estimated memory consumption

🚀 Running Comparison Benchmarks

# Run all comparison benchmarksnpm run test:comparison# Generate comparison reportnpm run test:comparison:report# Run individual benchmarkscd benchmark-comparisonsnpm run benchmark:substatenpm run benchmark:reduxnpm run benchmark:zustandnpm run benchmark:native

📈 Sample Results

Here's a sample comparison from our benchmark suite:

LibraryProperty AccessUpdate PerformanceStore CreationMemory (Small State)
Native JS47.90ns75.19ns525.13ns1KB
Redux47.76ns78.20ns2.23μs61KB
Zustand48.07ns78.62ns3.29μs61KB
Substate61.42ns285.69ns5.45μs7KB

🔬 Benchmark Methodology

✅ Fair Comparison:

  • Identical test data across all libraries
  • Same operations (store creation, updates, property access)
  • Statistical rigor (5 runs per test with mean/median/min/max/std)
  • Multiple state sizes (small: 10 props, medium: 100 props, large: 1000 props)

✅ Scientific Accuracy:

  • Real measurements, not estimates
  • Reproducible - anyone can run the same tests
  • Comprehensive - tests multiple scenarios and metrics
  • Transparent - full statistical analysis provided

📁 Results Storage

Benchmark results are automatically saved as JSON files inbenchmark-comparisons/results/ with:

  • Timestamped filenames for version tracking
  • Complete statistical data (mean, median, min, max, standard deviation)
  • Environment information (platform, Node.js version, CI status)
  • Detailed breakdowns for each test scenario

📊 Report Generation

The report generator creates multiple output formats:

JSON Summary (performance-summary-latest.json):

  • Consolidated averages from all libraries
  • Structured data for programmatic analysis
  • Environment metadata for reproducibility

Markdown Tables (performance-tables-latest.md):

  • Ready-to-use markdown tables for documentation
  • Formatted performance comparisons with proper units
  • Best performance highlighted in bold for easy identification
  • Performance insights and recommendations
  • Can be directly included in README files or documentation

Console Output:

  • Real-time display of comparison results
  • Detailed statistical breakdowns for each library
  • Performance insights and fastest metrics identification

🎯 Key Insights

  • Native JavaScript: Fastest raw performance, no overhead
  • Substate: Optimized for reactive state with minimal overhead (~5x slower than native)
  • Zustand: Good balance of features and performance
  • Redux: More overhead due to action/reducer pattern

📊 Use Case Recommendations

  • High-frequency updates: Consider Native JS or Substate
  • Complex state logic: Redux provides predictable patterns
  • Simple state management: Zustand offers good balance
  • Reactive features needed: Substate provides built-in Pub/Sub

💡 Note: Performance varies by use case. Choose based on your specific requirements, not just raw speed. The comparison benchmarks help you make informed decisions based on real data.

📊 Latest Results: The most recent benchmark results are available inbenchmark-comparisons/results/performance-tables-latest.md and can be included directly in documentation.

🔄 Why Choose Substate?

Comparison with Other State Management Solutions

FeatureSubstateReduxZustandValtioMobX
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 andboilerplate 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 - runnpm 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 isone 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

interfaceISubstateextendsIPubSub{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;}interfaceICreateStoreConfig{name:string;state?:object;defaultDeep?:boolean;beforeUpdate?:UpdateMiddleware[];afterUpdate?:UpdateMiddleware[];}interfaceIState{[key:string]:unknown;$type?:string;$deep?:boolean;}interfaceISyncConfig{readerObj:Record<string,unknown>;stateField:string;readField?:string;beforeUpdate?:BeforeMiddleware[];afterUpdate?:AfterMiddleware[];}

Middleware Types

// Primary state type - represents any state object with optional keywordstypeTSubstateState=object&TStateKeywords;// Update middleware for state changestypeTUpdateMiddleware=(store:ISubstate,action:Partial<TSubstateState>)=>void;// Sync middleware for unidirectional data bindingtypeTSyncMiddleware=(value:unknown,context:ISyncContext,store:ISubstate)=>unknown;// Sync configuration with middleware supporttypeTSyncConfig={readerObj:Record<string,unknown>|object;stateField:string;readField?:string;beforeUpdate?:TSyncMiddleware[];afterUpdate?:TSyncMiddleware[];syncEvents?:string[]|string;};// Context provided to sync middlewareinterfaceISyncContext{source:string;field:string;readField:string;}// State keywords for special functionalitytypeTStateKeywords={$type?:string;$deep?:boolean;$tag?:string;[key:string]:unknown;};

📈 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)importSubstatefrom'substate';// ✅ New (v10)import{createStore,Substate}from'substate';
  1. Store Creation
// ❌ Old (v9)conststore=newSubstate({name:'MyStore',state:{count:0}});// ✅ New (v10) - Recommendedconststore=createStore({name:'MyStore',state:{count:0}});// ✅ New (v10) - Still works but not recommendedconststore=newSubstate({name:'MyStore',state:{count:0}});
  1. Peer Dependencies
# Install peer dependenciesnpm 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
// Beforeconststores=[newSubstate({name:'Store1',state:{data:[]}}),newSubstate({name:'Store2',state:{user:null}})];// Afterconststores=[createStore({name:'Store1',state:{data:[]}}),createStore({name:'Store2',state:{user:null}})];
  1. Leverage new sync functionality
// New capability - sync store to UI modelsconstunsync=store.sync({readerObj:uiModel,stateField:'user.profile',readField:'userInfo'});

From Other Libraries

From Redux

// Redux setupconststore=createStore(rootReducer);store.dispatch({type:'INCREMENT',payload:1});// Substate equivalentconststore=createStore({name:'Counter',state:{count:0}});store.updateState({count:store.getProp('count')+1});

From Zustand

// ZustandconstuseStore=create((set)=>({count:0,increment:()=>set((state)=>({count:state.count+1}))}));// Substateconststore=createStore({name:'Counter',state:{count:0}});constincrement=()=>store.updateState({count:store.getProp('count')+1});

🔧 Troubleshooting

Common Issues and Solutions

🔄 State Updates Not Triggering Re-renders

Problem: State updates aren't triggering component re-renders or event listeners.

Solutions:

// ✅ Correct: Use updateState methodstore.updateState({count:1});// ❌ Wrong: Direct state mutationstore.stateStorage[store.currentState].count=1;// Won't trigger events// ✅ Correct: Subscribe properlystore.on('UPDATE_STATE',(newState)=>{console.log('State changed:',newState);// Update your UI here});

🧊 Deep Cloning Issues

Problem: Performance issues with deep cloning on complex objects.

Solutions:

// For simple updates, disable deep cloningstore.updateState({simpleValue:'new value',$deep:false// Skip deep cloning for this update});// Or configure store to skip deep cloning by defaultconststore=createStore({name:'FastStore',state:{data:largeObject},defaultDeep:false// Skip deep cloning by default});// Force deep cloning when neededstore.updateState({complexData:largeObject,$deep:true// Force deep cloning});

🔗 Sync Not Working

Problem: Sync bindings aren't updating target objects.

Solutions:

// ✅ Correct sync setupconstunsync=store.sync({readerObj:targetObject,stateField:'user.name',readField:'displayName'});// Make sure to call unsync() when component unmountsuseEffect(()=>{return()=>unsync();// Cleanup sync binding},[]);// Check that property paths existconsole.log(store.getProp('user.name'));// Should not be undefined

🏷️ Tagged States Not Found

Problem:jumpToTag() throws "tag not found" error.

Solutions:

// ✅ Correct tag creationstore.updateState({data:importantData,$tag:'checkpoint-1'// Create tag when updating});// Wait for state update before jumpingstore.updateState({step:2,$tag:'step-2'});setTimeout(()=>{store.jumpToTag('step-2');// Tag exists now},0);// Check available tags firstif(store.getAvailableTags().includes('my-tag')){store.jumpToTag('my-tag');}

📊 Memory Usage Issues

Problem: Store consuming too much memory.

Solutions:

// Limit history sizeconststore=createStore({name:'MemoryEfficientStore',state:{data:[]},maxHistorySize:10// Keep only last 10 states});// Manually clear historystore.clearHistory();// Monitor memory usagesetInterval(()=>{constusage=store.getMemoryUsage();if(usage.estimatedSizeKB>1000){store.clearHistory();// Clean up if over 1MB}},30000);

🔄 Event Listener Memory Leaks

Problem: Event listeners not being cleaned up.

Solutions:

// ✅ Correct cleanupconsthandler=(state)=>console.log(state);store.on('UPDATE_STATE',handler);// Later, remove the listenerstore.off('UPDATE_STATE',handler);// Or use unsync for sync bindingsconstunsync=store.sync({/* config */});// Laterunsync();// Clean up sync binding

🐛 TypeScript Type Errors

Problem: TypeScript complaining about state types.

Solutions:

// Define proper interfacesinterfaceUser{id:number;name:string;email:string;}interfaceAppState{user:User|null;loading:boolean;error:string|null;}// Type your storeconststore=createStore({name:'TypedStore',state:{user:null,loading:false,error:null}asAppState});// Or use generics if availableconststore=createStore<AppState>({name:'TypedStore',state:{user:null,loading:false,error:null}});

⚡ Performance Issues with Large State

Problem: Slow updates with large state objects.

Solutions:

// Use batch updates for multiple changesstore.batchUpdateState([{'user.name':'John'},{'user.email':'john@example.com'},{'settings.theme':'dark'}]);// Minimize state sizeconststore=createStore({name:'OptimizedStore',state:{// Only store what you needessentialData:smallObject,// Avoid storing large objects if possible// computedData: computeOnDemand()}});// Use shallow updates when possiblestore.updateState({simpleField:'value',$deep:false});

🔍 Debugging State Changes

Problem: Hard to track what changed in state.

Solutions:

// Add logging middlewareconststore=createStore({name:'DebugStore',state:{count:0},beforeUpdate:[(store,action)=>{console.log('Before update:',store.getCurrentState());console.log('Action:',action);}],afterUpdate:[(store,action)=>{console.log('After update:',store.getCurrentState());}]});// Log all state changesstore.on('UPDATE_STATE',(newState,oldState)=>{console.log('State changed from:',oldState,'to:',newState);});// Use tagged states for debuggingstore.updateState({debugInfo:data,$tag:'before-bug'});

Need More Help?

🛠️ 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 directorynpm run fix# Auto-fix formatting and linting issuesnpm run format# Format code with Biomenpm run lint# Check code linting with Biomenpm run check# Run Biome checks on source code

Testing Suite

npmtest# Run all tests (core + integration)npm run test:core# Run core unit tests onlynpm run test:watch# Run tests in watch modenpm run test:coverage# Run tests with coverage reportnpm run test:all# Comprehensive test suite (check + test + builds + integrations + perf)

Build Testing

npm run test:builds# Test both ESM and UMD buildsnpm run _test:esm# Test ESM build specificallynpm 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 testnpm run _test:perf:deep# Deep state performance testnpm run test:perf:avg# Run performance tests with 5-run averagesnpm run _test:perf:shallow:avg# Shallow performance with averagingnpm run _test:perf:deep:avg# Deep performance with averaging

Integration Testing

npm run test:integrations# Run all integration testsnpm run _test:integrations:check# Check dependency compatibilitynpm run _test:integration:react# Test React integrationnpm 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 servernpm run dev:preact# Start Preact integration dev server

Setup and Maintenance

npm run integration:setup# Setup all integration test environmentsnpm run _integration:setup:react# Setup React integration onlynpm run _integration:setup:preact# Setup Preact integration onlynpm run reset# Clear all dependencies and reinstallnpm 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' tagnpm 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.

About

pub/sub state management with optional deep cloning

Topics

Resources

License

Stars

Watchers

Forks

Packages

No packages published

Contributors4

  •  
  •  
  •  
  •  

[8]ページ先頭

©2009-2025 Movatter.jp