
Security News
Attackers Are Hunting High-Impact Node.js Maintainers in a Coordinated Social Engineering Campaign
Multiple high-impact npm maintainers confirm they have been targeted in the same social engineering campaign that compromised Axios.
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.
npm install substate
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 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.
A Named State Checkpoint System provides:
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");
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
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");
}
}
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");
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
// 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" });
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}`);
});
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
});
}
}
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 }]
});
}
}
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 }
Substate supports two sync() modes:
sync(path?, config?) returns a reactive proxy for a state slice. Reads always reflect the latest store state, and writes auto-commit via updateState().sync(configObject) keeps the unidirectional binding behavior. It remains supported, but logs a one-time console.warn per store instance to encourage migration.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';
});
.valueFor 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;
type TProxySyncConfig = {
beforeUpdate?: UpdateMiddleware[];
afterUpdate?: UpdateMiddleware[];
};
sync() with no argsCalling 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.
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 middlewareconst 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 syncconst 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.
This is the classic sync API that binds store state to a target object (unidirectional). It remains supported.
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();
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('$', 'โฌ') : 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'
// 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
// }
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() });
import { createStore, type ISubstate, type ICreateStoreConfig } from 'substate';
const config: ICreateStoreConfig = {
name: 'TypedStore',
state: { count: 0 },
defaultDeep: true
};
const store: ISubstate = 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())]
});
updateState(action: IState): voidUpdates 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 updateaction.$deep (optional) - Force deep cloning for this updateaction.$type (optional) - Custom identifier for this updateaction.$tag (optional) - Tag name to create a named checkpoint of this statebatchUpdateState(actions: Array<Partial<TState> & IState>): voidUpdates 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:
When to Use:
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(): IStateReturns the current active state object.
const currentState = store.getCurrentState();
console.log(currentState); // { count: 5, user: { name: 'John' } }
getProp(prop: string): unknownRetrieves 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): IStateReturns 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(): voidResets 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): () => voidCreates 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(): voidClears 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:
limitHistory(maxSize: number): voidSets 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 storedtaggedCount - Number of tagged states currently storedestimatedSizeKB - Rough estimation of memory usage in kilobytesNote: Size estimation is approximate and based on JSON serialization size.
getTaggedState(tag: string): IState | undefinedRetrieves 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 upReturns: 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): voidJumps 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 toThrows: Error if the tag doesn't exist
Events: Emits TAG_JUMPED and STATE_UPDATED
removeTag(tag: string): booleanRemoves 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 removeReturns: true if tag was found and removed, false if it didn't exist
Events: Emits TAG_REMOVED for existing tags
clearTags(): voidRemoves 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
on(event: string, callback: Function): voidSubscribe 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): voidEmit 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): voidUnsubscribe from store events.
const handler = (state) => console.log(state);
store.on('UPDATE_STATE', handler);
store.off('UPDATE_STATE', handler); // Removes this specific handler
| 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 |
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
Substate automatically manages memory through configurable history limits and provides tools for monitoring and optimization.
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!)
// Use default settings - 50 states is perfect for small apps
const store = createStore({
name: 'SmallApp',
state: { user: null, settings: {} }
// maxHistorySize: 50 (default)
});
// 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);
}
// 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);
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
}
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);
getMemoryUsage() to track growth patternsclearHistory() after large imports/updateslimitHistory() to adapt to different application modesThe default settings are optimized for most use cases:
๐ก Note: The 50-state default is designed for smaller applications. For enterprise applications with large state objects or high-frequency updates, consider customizing
maxHistorySizebased on your specific memory constraints.
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
| 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 |
| 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 |
// โ
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
| 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:comparisonfor current results
โ Perfect for:
โ Especially great for:
โ ๏ธ Consider alternatives for:
From Redux:
From Context API:
From Zustand:
From Vanilla State Management:
Substate is one of the few state management libraries that combines all these features out of the box:
๐ก 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.
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[];
}
// 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;
Substate v10 introduces several improvements and breaking changes. Here's how to upgrade:
// โ Old (v9)
import Substate from 'substate';
// โ
New (v10)
import { createStore, Substate } from 'substate';
// โ 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 } });
# Install peer dependencies
npm install clone-deep object-bystring
npm install substate@10 clone-deep object-bystring
// 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 } })
];
// New capability - sync store to UI models
const unsync = store.sync({
readerObj: uiModel,
stateField: 'user.profile',
readField: 'userInfo'
});
// 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 });
// 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
});
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
git checkout -b feature/amazing-featurenpm testnpm run lint:fixgit commit -m 'Add amazing feature'git push origin feature/amazing-featurenpm 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
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)
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
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
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
npm run test:isolation # Test module isolation and integrity
npm run dev:react # Start React integration dev server
npm run dev:preact # Start Preact integration dev server
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
npm run benchmark # Run performance comparisons vs other libraries
npm run pre # Pre-publish checks (test + build) - publishes to 'next' tag
npm run safe-publish # Full publish pipeline (test + build + publish)
Contributions are welcome! Please read our contributing guidelines and submit pull requests to help improve Substate.
MIT ยฉ Tom Saporito "Tamb"
Made with โค๏ธ for developers who want powerful state management without the complexity.
FAQs
Pub/Sub pattern with State Management
The npm package substate receives a total of 102 weekly downloads. As such, substate popularity was classified as not popular.
We found that substate demonstrated a healthy version release cadence and project activity because the last version was released less than a year ago.ย It has 1 open source maintainer collaborating on the project.
Did you know?

Socket for GitHub automatically highlights issues in each pull request and monitors the health of all your open source dependencies. Discover the contents of your packages and block harmful activity before you install or update your dependencies.

Security News
Multiple high-impact npm maintainers confirm they have been targeted in the same social engineering campaign that compromised Axios.

Security News
Axios compromise traced to social engineering, showing how attacks on maintainers can bypass controls and expose the broader software supply chain.

Security News
Node.js has paused its bug bounty program after funding ended, removing payouts for vulnerability reports but keeping its security process unchanged.