API: store.subscribe()
The store.subscribe() method allows you to listen for changes in a specific part of your SoulState store. This is the low-level mechanism that powers the useStore React hook, but it can also be used directly for non-React integrations, logging, or advanced side effects.
Summary
store.subscribe() registers a listener function that will be called whenever the selected slice of state changes. It returns an unsubscribe function to clean up the listener.
Function Signature
interface Store<T> {
subscribe: <S>(
selector: (state: T) => S,
listener: (selectedState: S, prevSelectedState: S) => void,
options?: { equalityFn?: (a: S, b: S) => boolean }
) => () => void;
}
Parameters
-
selector: (state: T) => S:- A function that receives the entire store state (
state: T) and returns a specific slice of that state (S). - The
listenerwill only be invoked if the value returned by thisselectorchanges.
- A function that receives the entire store state (
-
listener: (selectedState: S, prevSelectedState: S) => void:- A callback function that is executed when the
selectedState(as determined by theselectorandequalityFn) changes. - It receives two arguments:
selectedState(the new value) andprevSelectedState(the previous value).
- A callback function that is executed when the
-
options?: { equalityFn?: (a: S, b: S) => boolean }:- An optional object to configure the subscription.
equalityFn?: (a: S, b: S) => boolean: A function used to compare theselectedStatewithprevSelectedState. If this function returnstrue, thelistenerwill not be called.- Default:
Object.is(strict reference equality). - Commonly used:
shallowfor shallow object comparison.
Return Value
() => void: Anunsubscribefunction. Calling this function will remove the registered listener from the store, preventing memory leaks.
Usage Examples
Basic Subscription
Listen to changes in a single primitive value.
import { counterStore } from '../stores/counterStore';
const unsubscribe = counterStore.subscribe(
(state) => state.count, // Selector: selects the 'count' property
(newCount, prevCount) => { // Listener: logs the change
console.log('Count changed from ' + prevCount + ' to ' + newCount);
}
);
// Trigger some changes
counterStore.set((state) => ({ count: state.count + 1 })); // Logs: Count changed from 0 to 1
// Clean up when done
unsubscribe();
Advanced Subscription with Custom Equality
Use a custom equality function to control when the listener is called.
import { todoStore } from '../stores/todoStore';
const unsubscribe = todoStore.subscribe(
(state) => state.todos.length, // Selector: get number of todos
(newLength, prevLength) => {
console.log('Todo count changed from ' + prevLength + ' to ' + newLength);
},
{
equalityFn: (a, b) => Math.abs(a - b) < 2 // Only notify if change is >= 2
}
);
Subscribing to Multiple Properties
Use shallow equality when selecting multiple properties.
import { userStore } from '../stores/userStore';
import { shallow } from 'soulstate/utils';
const unsubscribe = userStore.subscribe(
(state) => ({ name: state.name, email: state.email }), // Selector: returns new object
(newUserInfo, prevUserInfo) => {
console.log('User info changed:', newUserInfo);
},
{
equalityFn: shallow // Use shallow comparison for object
}
);
Real-time Analytics Example
import { analyticsStore } from '../stores/analyticsStore';
// Track user behavior in real-time
const analyticsUnsubscribe = analyticsStore.subscribe(
(state) => ({ pageViews: state.pageViews, activeUsers: state.activeUsers }),
(metrics, prevMetrics) => {
// Send to analytics service only when metrics change
if (metrics.pageViews !== prevMetrics.pageViews) {
analytics.track('page_view', { count: metrics.pageViews });
}
},
{ equalityFn: shallow }
);
// Later, when analytics are no longer needed
analyticsUnsubscribe();
Performance Characteristics
O(1) Subscription Management
SoulState uses a doubly linked list for subscription management, providing:
- O(1) subscription addition
- O(1) subscription removal
- O(n) notification (where n = number of subscriptions)
// Simplified linked list structure from src/core/subscriptions.ts
interface SubscriptionNode<T, S> {
selector: (state: T) => S;
listener: (selectedState: S, prevSelectedState: S) => void;
equalityFn: (a: S, b: S) => boolean;
lastState: S;
prev: SubscriptionNode<T, any> | null;
next: SubscriptionNode<T, any> | null;
}
Microtask Batching
All subscription notifications are batched using queueMicrotask:
// Multiple updates → single notification batch
counterStore.set({ count: 1 }); // 🚫 No immediate notification
counterStore.set({ count: 2 }); // 🚫 No immediate notification
counterStore.set({ count: 3 }); // 🚫 No immediate notification
// ✅ All three updates notified in single microtask
Anti-patterns
1. Forgetting to Unsubscribe
// ❌ Memory leak: subscription never cleaned up
const startAnalytics = () => {
store.subscribe(selector, listener); // Never unsubscribed
};
// ✅ Always clean up subscriptions
const startAnalytics = () => {
const unsubscribe = store.subscribe(selector, listener);
return unsubscribe; // Return for cleanup
};
2. Expensive Selectors in Tight Loops
// ❌ Expensive operation on every state change
store.subscribe(
(state) => state.data.filter(item => item.active).sort((a, b) => b.id - a.id),
listener // Called on every state change with expensive computation
);
// ✅ Memoize expensive selectors
const expensiveSelector = (state) => {
// Memoization logic here
return computedData;
};
Common Use Cases
1. Logging and Debugging
// Development logging
if (process.env.NODE_ENV === 'development') {
store.subscribe(
state => state,
(state, prevState) => {
console.log('State changed:', { from: prevState, to: state });
},
{ equalityFn: shallow }
);
}
2. Persistence Layer
// Auto-save to localStorage
const persistenceUnsubscribe = store.subscribe(
state => state.userPreferences,
(prefs) => {
localStorage.setItem('userPrefs', JSON.stringify(prefs));
},
{ equalityFn: shallow }
);
3. Integration with Non-React Code
// Update legacy jQuery components
const jqueryUnsubscribe = store.subscribe(
state => state.theme,
(theme) => {
$('#app').attr('data-theme', theme);
}
);
Memory Management
Always call the returned unsubscribe function when you no longer need the subscription. This prevents memory leaks, especially in long-running applications.
React Alternative
For React components, prefer the useStore hook instead of manual subscriptions. The hook automatically handles subscription cleanup when the component unmounts.
Implementation Detail
The subscription system uses a doubly linked list implementation in src/core/subscriptions.ts for optimal O(1) add/remove operations.