Now.js Framework Documentation
StateManager
StateManager
Overview
StateManager is the global state management system in Now.js Framework that uses a Vuex/Redux-like pattern. It supports modules, mutations, actions, getters, history tracking, and persistence.
When to use:
- Need global state shared across multiple components
- Want to track state changes (debugging)
- Need time-travel debugging
- Want to persist state to localStorage
Why use it:
- ✅ Module-based state organization
- ✅ Mutations for synchronous changes
- ✅ Actions for async operations
- ✅ Getters for computed state
- ✅ Watch and Subscribe for reactivity
- ✅ History tracking and time-travel
- ✅ Automatic persistence
- ✅ Middleware support
Basic Usage
Registering a Module
// Register a module
StateManager.registerModule('counter', {
// Initial state
state: {
count: 0,
step: 1
},
// Synchronous state changes
mutations: {
increment(state, payload) {
state.count += payload || state.step;
},
decrement(state, payload) {
state.count -= payload || state.step;
},
setStep(state, step) {
state.step = step;
}
},
// Async operations
actions: {
async incrementAsync(context, delay = 1000) {
await new Promise(resolve => setTimeout(resolve, delay));
context.commit('increment');
},
async fetchAndSet(context, url) {
const response = await fetch(url);
const data = await response.json();
context.commit('increment', data.value);
}
},
// Computed values
getters: {
doubleCount(state) {
return state.count * 2;
},
isPositive(state) {
return state.count > 0;
}
}
});Using State
// Read state
const count = StateManager.get('counter.count');
console.log(count); // 0
// Commit mutation (synchronous)
StateManager.commit('counter/increment');
StateManager.commit('counter/increment', 5);
// Dispatch action (async)
await StateManager.dispatch('counter/incrementAsync', 500);
// Watch changes
const unwatch = StateManager.watch('counter.count', (newValue) => {
console.log('Count changed:', newValue);
});
// Later: stop watching
unwatch();Modules
Module Structure
StateManager.registerModule('moduleName', {
// State: initial data
state: {
items: [],
loading: false,
error: null
},
// Mutations: sync state changes
mutations: {
setItems(state, items) {
state.items = items;
},
setLoading(state, loading) {
state.loading = loading;
},
setError(state, error) {
state.error = error;
}
},
// Actions: async logic
actions: {
async fetchItems(context) {
context.commit('setLoading', true);
try {
const response = await fetch('/api/items');
const items = await response.json();
context.commit('setItems', items);
} catch (error) {
context.commit('setError', error.message);
} finally {
context.commit('setLoading', false);
}
}
},
// Getters: computed values
getters: {
itemCount(state) {
return state.items.length;
},
activeItems(state) {
return state.items.filter(item => item.active);
}
},
// Watch: track changes
watch: {
'items': (newItems) => {
console.log('Items updated:', newItems.length);
}
},
// Init: called when module is registered
init(context) {
console.log('Module initialized');
context.dispatch('fetchItems');
}
});State Function
When state is a function, it creates a new instance each time:
StateManager.registerModule('form', {
// Use function to prevent shared state
state: () => ({
fields: {},
errors: [],
dirty: false
})
});Force Replace Module
// Replace existing module
StateManager.registerModule('counter', {
state: { count: 100 }
}, { force: true });Mutations
Mutations are synchronous functions that change state:
// Define mutations
mutations: {
// Simple mutation
increment(state) {
state.count++;
},
// With payload
setUser(state, user) {
state.user = user;
},
// With multiple values
addItem(state, { id, name, price }) {
state.items.push({ id, name, price });
}
}
// Call mutations
StateManager.commit('moduleName/mutationName');
StateManager.commit('moduleName/mutationName', payload);❌ Don't Use Async in Mutations
mutations: {
// ❌ Bad - don't use async in mutations
async fetchData(state) {
const data = await fetch('/api/data');
state.data = data;
}
}
// ✅ Use actions instead
actions: {
async fetchData(context) {
const response = await fetch('/api/data');
const data = await response.json();
context.commit('setData', data);
}
}Actions
Actions are async functions that can perform side effects:
actions: {
// Simple action
async loadUser(context, userId) {
context.commit('setLoading', true);
try {
const response = await fetch(`/api/users/${userId}`);
if (!response.ok) throw new Error('Failed to load user');
const user = await response.json();
context.commit('setUser', user);
return user;
} catch (error) {
context.commit('setError', error.message);
throw error;
} finally {
context.commit('setLoading', false);
}
},
// Action that dispatches other actions
async initializeApp(context) {
await context.dispatch('loadUser', 'current');
await context.dispatch('loadSettings');
}
}
// Call actions
const user = await StateManager.dispatch('users/loadUser', 123);Context Object
Action context includes:
actions: {
example(context, payload) {
// State for this module
console.log(context.state);
// Getters for this module
console.log(context.getters.itemCount);
// Commit mutation
context.commit('mutationName', payload);
// Dispatch action
await context.dispatch('otherAction', payload);
// Watch changes
context.watch('path', handler);
}
}Getters
Getters are computed values derived from state:
getters: {
// Simple getter
itemCount(state) {
return state.items.length;
},
// Getter using other getters
doubleCount(state, getters) {
return getters.itemCount * 2;
},
// Filter items
activeItems(state) {
return state.items.filter(item => item.active);
},
// Search function
findById(state) {
return (id) => state.items.find(item => item.id === id);
}
}
// Use getters
const context = StateManager.getModuleContext('moduleName');
console.log(context.getters.itemCount);
console.log(context.getters.findById(123));Watch and Subscribe
Watch
Track state path changes:
// Watch single path
const unwatch = StateManager.watch('counter.count', (newValue) => {
console.log('Count is now:', newValue);
});
// Stop watching
unwatch();Subscribe
Subscribe with additional options:
// Subscribe with options
const unsubscribe = StateManager.subscribe('user.profile', (value) => {
console.log('Profile updated:', value);
}, {
immediate: true, // Call immediately with current value
deep: false // Detect deep changes
});
// Stop subscribing
unsubscribe();Watch in Module Definition
StateManager.registerModule('cart', {
state: {
items: [],
total: 0
},
watch: {
'items': function(newItems, oldItems) {
// Recalculate total
this.commit('updateTotal');
}
}
});Time Travel
StateManager records state change history:
// View history
console.log(StateManager.history);
// [
// { type: 'counter/increment', payload: null, state: {...}, timestamp: 1699...},
// { type: 'counter/increment', payload: 5, state: {...}, timestamp: 1699...}
// ]
// Go back to previous state
StateManager.timeTravel(0); // Go to first entry
// View current index
console.log(StateManager.historyIndex); // 0
// Go forward
StateManager.timeTravel(StateManager.historyIndex + 1);Configuration
await StateManager.init({
// Debug mode - show logs
debug: false,
// Persistence settings
persistence: {
enabled: true,
key: 'app_state', // localStorage key
blacklist: ['temp', 'ui'], // modules not to persist
encrypt: false // encrypt data
},
// History settings
history: {
enabled: true,
maxSize: 50, // max entries
include: ['user', 'data'] // modules to track
},
// Batch updates
batch: {
enabled: true,
delay: 16 // ms (1 frame)
},
// Strict mode - log mutations
strict: true
});API Reference
StateManager.init(options)
Initialize StateManager
| Parameter | Type | Description |
|---|---|---|
options |
object | Configuration options |
Returns: Promise<StateManager>
StateManager.registerModule(name, module, options)
Register a module
| Parameter | Type | Description |
|---|---|---|
name |
string | Module name |
module |
object | Module definition |
options.force |
boolean | Replace existing module |
StateManager.hasModule(name)
Check if module exists
| Parameter | Type | Description |
|---|---|---|
name |
string | Module name |
Returns: boolean
StateManager.get(path)
Get state value
| Parameter | Type | Description |
|---|---|---|
path |
string | Path like 'module.property' |
Returns: any
const count = StateManager.get('counter.count');
const user = StateManager.get('auth.user');StateManager.set(path, value)
Set state value directly (use sparingly)
| Parameter | Type | Description |
|---|---|---|
path |
string | Path |
value |
any | New value |
StateManager.commit(type, payload)
Call a mutation
| Parameter | Type | Description |
|---|---|---|
type |
string | 'moduleName/mutationName' |
payload |
any | Data to pass to mutation |
StateManager.commit('counter/increment');
StateManager.commit('counter/setCount', 100);StateManager.dispatch(type, payload)
Call an action
| Parameter | Type | Description |
|---|---|---|
type |
string | 'moduleName/actionName' |
payload |
any | Data to pass to action |
Returns: Promise<any>
await StateManager.dispatch('auth/login', { email, password });
const users = await StateManager.dispatch('users/fetchAll');StateManager.watch(path, callback)
Watch for changes
| Parameter | Type | Description |
|---|---|---|
path |
string | State path |
callback |
function | Handler function |
Returns: function - Unwatch function
StateManager.subscribe(path, callback, options)
Subscribe with options
| Parameter | Type | Description |
|---|---|---|
path |
string | State path |
callback |
function | Handler function |
options.immediate |
boolean | Call immediately |
options.deep |
boolean | Deep comparison |
Returns: function - Unsubscribe function
StateManager.timeTravel(index)
Go to state in history
| Parameter | Type | Description |
|---|---|---|
index |
number | History index |
StateManager.reset()
Reset all state to initial values
StateManager.persistState()
Save state to localStorage
StateManager.use(middleware)
Add middleware
| Parameter | Type | Description |
|---|---|---|
middleware |
object | Middleware object |
Real-World Examples
Authentication Module
StateManager.registerModule('auth', {
state: () => ({
user: null,
token: null,
loading: false,
error: null
}),
mutations: {
setUser(state, user) {
state.user = user;
},
setToken(state, token) {
state.token = token;
},
setLoading(state, loading) {
state.loading = loading;
},
setError(state, error) {
state.error = error;
},
logout(state) {
state.user = null;
state.token = null;
}
},
actions: {
async login(context, { email, password }) {
context.commit('setLoading', true);
context.commit('setError', null);
try {
const response = await fetch('/api/auth/login', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ email, password })
});
if (!response.ok) {
throw new Error('Login failed');
}
const { user, token } = await response.json();
context.commit('setUser', user);
context.commit('setToken', token);
return user;
} catch (error) {
context.commit('setError', error.message);
throw error;
} finally {
context.commit('setLoading', false);
}
},
async logout(context) {
await fetch('/api/auth/logout', { method: 'POST' });
context.commit('logout');
}
},
getters: {
isLoggedIn(state) {
return !!state.user && !!state.token;
},
userName(state) {
return state.user?.name || 'Guest';
}
}
});
// Usage
await StateManager.dispatch('auth/login', {
email: 'user@example.com',
password: 'password123'
});
const isLoggedIn = StateManager.getModuleContext('auth').getters.isLoggedIn;Shopping Cart Module
StateManager.registerModule('cart', {
state: () => ({
items: [],
coupon: null
}),
mutations: {
addItem(state, product) {
const existing = state.items.find(item => item.id === product.id);
if (existing) {
existing.quantity++;
} else {
state.items.push({ ...product, quantity: 1 });
}
},
removeItem(state, productId) {
state.items = state.items.filter(item => item.id !== productId);
},
updateQuantity(state, { productId, quantity }) {
const item = state.items.find(item => item.id === productId);
if (item) {
item.quantity = Math.max(0, quantity);
if (item.quantity === 0) {
state.items = state.items.filter(item => item.id !== productId);
}
}
},
setCoupon(state, coupon) {
state.coupon = coupon;
},
clearCart(state) {
state.items = [];
state.coupon = null;
}
},
getters: {
itemCount(state) {
return state.items.reduce((sum, item) => sum + item.quantity, 0);
},
subtotal(state) {
return state.items.reduce((sum, item) => sum + (item.price * item.quantity), 0);
},
discount(state, getters) {
if (!state.coupon) return 0;
return getters.subtotal * (state.coupon.discount / 100);
},
total(state, getters) {
return getters.subtotal - getters.discount;
}
}
});Common Pitfalls
⚠️ 1. Don't Modify State Directly
// ❌ Bad - modifying state directly
StateManager.state.counter.count++;
// ✅ Good - use mutation
StateManager.commit('counter/increment');⚠️ 2. Watch Must Cleanup
// ❌ Memory leak
function setupComponent() {
StateManager.watch('counter.count', updateUI);
}
// ✅ Cleanup when component is removed
function setupComponent() {
const unwatch = StateManager.watch('counter.count', updateUI);
return () => unwatch(); // Cleanup function
}⚠️ 3. Actions Must Handle Errors
// ❌ Not handling errors
actions: {
async fetchData(context) {
const data = await fetch('/api/data').then(r => r.json());
context.commit('setData', data);
}
}
// ✅ Handle errors
actions: {
async fetchData(context) {
try {
const response = await fetch('/api/data');
if (!response.ok) throw new Error('Fetch failed');
const data = await response.json();
context.commit('setData', data);
} catch (error) {
context.commit('setError', error.message);
throw error; // Re-throw so caller can handle
}
}
}Related Documentation
- ComponentManager - Component lifecycle
- ReactiveManager - Reactive state
- StorageManager - Storage utilities