State management pattern and library for Vue.js applications that serves as a centralized store with predictable state mutations
—
Development and debugging features including logging plugin, DevTools integration, and state subscription mechanisms.
Development plugin for logging store mutations and actions to the console.
/**
* Create a logger plugin for development debugging
* @param options - Logger configuration options
* @returns Plugin function for store
*/
function createLogger<S>(options?: LoggerOption<S>): Plugin<S>;
interface LoggerOption<S> {
/** Whether to collapse log groups (default: true) */
collapsed?: boolean;
/** Filter function to determine which mutations to log */
filter?: <P extends Payload>(mutation: P, stateBefore: S, stateAfter: S) => boolean;
/** Transform state before logging */
transformer?: (state: S) => any;
/** Transform mutations before logging */
mutationTransformer?: <P extends Payload>(mutation: P) => any;
/** Filter function to determine which actions to log */
actionFilter?: <P extends Payload>(action: P, state: S) => boolean;
/** Transform actions before logging */
actionTransformer?: <P extends Payload>(action: P) => any;
/** Whether to log mutations (default: true) */
logMutations?: boolean;
/** Whether to log actions (default: true) */
logActions?: boolean;
/** Custom logger object (default: console) */
logger?: Logger;
}
interface Logger extends Partial<Pick<Console, 'groupCollapsed' | 'group' | 'groupEnd'>> {
log(message: string, color: string, payload: any): void;
log(message: string): void;
}
interface Payload {
type: string;
}
type Plugin<S> = (store: Store<S>) => any;Usage Examples:
import { createStore, createLogger } from 'vuex';
// Basic logger setup
const store = createStore({
state: { count: 0 },
mutations: {
increment(state) { state.count++; }
},
plugins: process.env.NODE_ENV !== 'production'
? [createLogger()]
: []
});
// Advanced logger configuration
const loggerPlugin = createLogger({
collapsed: false, // Don't collapse log groups
// Only log mutations that change important state
filter: (mutation, stateBefore, stateAfter) => {
return mutation.type !== 'UPDATE_SCROLL_POSITION';
},
// Transform state to hide sensitive data
transformer: (state) => ({
...state,
user: state.user ? { ...state.user, password: '[HIDDEN]' } : null
}),
// Format mutation display
mutationTransformer: (mutation) => ({
type: mutation.type,
payload: mutation.type.includes('PASSWORD')
? '[HIDDEN]'
: mutation.payload
}),
// Only log specific actions
actionFilter: (action, state) => {
return !action.type.startsWith('analytics/');
},
// Custom action formatting
actionTransformer: (action) => ({
...action,
timestamp: Date.now()
}),
// Disable action logging in production
logActions: process.env.NODE_ENV !== 'production',
// Custom logger
logger: {
log: console.log,
groupCollapsed: console.groupCollapsed,
groupEnd: console.groupEnd
}
});
const store = createStore({
// ... store config
plugins: [loggerPlugin]
});Subscribe to mutations and actions for debugging and monitoring.
/**
* Subscribe to store mutations
* @param fn - Callback function called on each mutation
* @param options - Subscription options
* @returns Unsubscribe function
*/
subscribe<P extends MutationPayload>(
fn: (mutation: P, state: S) => any,
options?: SubscribeOptions
): () => void;
/**
* Subscribe to store actions
* @param fn - Callback function or options object
* @param options - Subscription options
* @returns Unsubscribe function
*/
subscribeAction<P extends ActionPayload>(
fn: SubscribeActionOptions<P, S>,
options?: SubscribeOptions
): () => void;
interface MutationPayload extends Payload {
payload: any;
}
interface ActionPayload extends Payload {
payload: any;
}
interface SubscribeOptions {
/** Add subscriber to beginning of list */
prepend?: boolean;
}
type ActionSubscriber<P, S> = (action: P, state: S) => any;
type ActionErrorSubscriber<P, S> = (action: P, state: S, error: Error) => any;
interface ActionSubscribersObject<P, S> {
before?: ActionSubscriber<P, S>;
after?: ActionSubscriber<P, S>;
error?: ActionErrorSubscriber<P, S>;
}
type SubscribeActionOptions<P, S> = ActionSubscriber<P, S> | ActionSubscribersObject<P, S>;Usage Examples:
import { createStore } from 'vuex';
const store = createStore({
state: { count: 0, logs: [] },
mutations: {
increment(state) { state.count++; },
addLog(state, message) { state.logs.push(message); }
},
actions: {
async fetchData({ commit }) {
const data = await api.getData();
commit('setData', data);
}
}
});
// Subscribe to mutations
const unsubscribeMutations = store.subscribe((mutation, state) => {
console.log('Mutation:', mutation.type);
console.log('Payload:', mutation.payload);
console.log('New state:', state);
// Log mutations to server
analytics.track('store_mutation', {
type: mutation.type,
payload: mutation.payload
});
});
// Subscribe to actions with lifecycle hooks
const unsubscribeActions = store.subscribeAction({
before: (action, state) => {
console.log(`Before action ${action.type}`, action.payload);
// Start performance timing
performance.mark(`action-${action.type}-start`);
},
after: (action, state) => {
console.log(`After action ${action.type}`, state);
// End performance timing
performance.mark(`action-${action.type}-end`);
performance.measure(
`action-${action.type}`,
`action-${action.type}-start`,
`action-${action.type}-end`
);
},
error: (action, state, error) => {
console.error(`Action ${action.type} failed:`, error);
// Log errors to monitoring service
errorReporting.captureException(error, {
action: action.type,
payload: action.payload,
state
});
}
});
// Subscribe with prepend option
const debugSubscriber = store.subscribe((mutation, state) => {
// This will be called first due to prepend: true
console.log('Debug:', mutation);
}, { prepend: true });
// Cleanup subscriptions
const cleanup = () => {
unsubscribeMutations();
unsubscribeActions();
debugSubscriber();
};
// Auto-cleanup on app unmount
window.addEventListener('beforeunload', cleanup);Watch specific parts of the store state for changes.
/**
* Watch a computed value derived from store state
* @param getter - Function that returns the value to watch
* @param cb - Callback function called when value changes
* @param options - Watch options (Vue WatchOptions)
* @returns Unwatch function
*/
watch<T>(
getter: (state: S, getters: any) => T,
cb: (value: T, oldValue: T) => void,
options?: WatchOptions
): () => void;
// Note: WatchOptions is imported from Vue and includes:
// { deep?: boolean; immediate?: boolean; flush?: 'pre' | 'post' | 'sync' }Usage Examples:
import { createStore } from 'vuex';
const store = createStore({
state: {
user: null,
cart: { items: [] },
ui: { theme: 'light' }
},
getters: {
cartTotal: state => state.cart.items.reduce((sum, item) =>
sum + item.price * item.quantity, 0),
itemCount: state => state.cart.items.length
}
});
// Watch specific state property
const unwatchUser = store.watch(
(state) => state.user,
(newUser, oldUser) => {
console.log('User changed:', { newUser, oldUser });
if (newUser && !oldUser) {
// User logged in
analytics.track('user_login', { userId: newUser.id });
} else if (!newUser && oldUser) {
// User logged out
analytics.track('user_logout', { userId: oldUser.id });
}
},
{ deep: true } // Watch object properties
);
// Watch computed getter
const unwatchCartTotal = store.watch(
(state, getters) => getters.cartTotal,
(newTotal, oldTotal) => {
console.log(`Cart total changed: ${oldTotal} -> ${newTotal}`);
// Update localStorage
localStorage.setItem('cartTotal', newTotal.toString());
// Show notification for significant changes
if (newTotal - oldTotal > 100) {
showNotification('Large price change detected!');
}
}
);
// Watch multiple values
const unwatchMultiple = store.watch(
(state, getters) => ({
user: state.user,
itemCount: getters.itemCount,
theme: state.ui.theme
}),
(newValues, oldValues) => {
// React to changes in any watched value
console.log('Multiple values changed:', { newValues, oldValues });
},
{ deep: true }
);
// Watch with immediate option
const unwatchImmediate = store.watch(
(state) => state.ui.theme,
(theme) => {
document.body.className = `theme-${theme}`;
},
{ immediate: true } // Call immediately with current value
);
// Conditional watching
let themeWatcher = null;
const startThemeWatching = () => {
if (!themeWatcher) {
themeWatcher = store.watch(
(state) => state.ui.theme,
(theme) => applyTheme(theme)
);
}
};
const stopThemeWatching = () => {
if (themeWatcher) {
themeWatcher();
themeWatcher = null;
}
};
// Cleanup all watchers
const cleanup = () => {
unwatchUser();
unwatchCartTotal();
unwatchMultiple();
unwatchImmediate();
stopThemeWatching();
};Integration with Vue DevTools browser extension for debugging.
Usage Examples:
import { createApp } from 'vue';
import { createStore } from 'vuex';
// DevTools is automatically enabled in development
const store = createStore({
state: { count: 0 },
mutations: {
increment(state) { state.count++; }
},
// DevTools enabled by default in development
devtools: process.env.NODE_ENV !== 'production'
});
// Explicit DevTools control
const store = createStore({
state: { count: 0 },
mutations: {
increment(state) { state.count++; }
},
devtools: true // Force enable even in production
});
// Custom DevTools configuration
const app = createApp(App);
const store = createStore({
// ... store config
devtools: true
});
app.use(store);
// DevTools will show:
// - State tree with current values
// - Mutation history with time-travel debugging
// - Action dispatch logging
// - Module hierarchy
// - Getter values and dependencies
// Time-travel debugging features:
// 1. Click on any mutation in the timeline to see state at that point
// 2. Use "Commit All" to apply all mutations up to a point
// 3. Use "Revert" to undo mutations
// 4. Export/import state snapshots
// 5. Filter mutations and actions by type or moduleCreate custom plugins for development workflow enhancement.
Usage Examples:
// Performance monitoring plugin
const performancePlugin = (store) => {
const actionTimes = new Map();
store.subscribeAction({
before: (action) => {
actionTimes.set(action.type, performance.now());
},
after: (action) => {
const startTime = actionTimes.get(action.type);
const duration = performance.now() - startTime;
if (duration > 100) {
console.warn(`Slow action detected: ${action.type} took ${duration}ms`);
}
actionTimes.delete(action.type);
}
});
};
// State persistence plugin
const persistPlugin = (store) => {
// Load state from localStorage on store creation
const saved = localStorage.getItem('vuex-state');
if (saved) {
store.replaceState(JSON.parse(saved));
}
// Save state on every mutation
store.subscribe((mutation, state) => {
localStorage.setItem('vuex-state', JSON.stringify(state));
});
};
// Error tracking plugin
const errorTrackingPlugin = (store) => {
store.subscribeAction({
error: (action, state, error) => {
// Send to error tracking service
errorTracker.captureException(error, {
extra: {
action: action.type,
payload: action.payload,
state: JSON.stringify(state)
}
});
}
});
};
// Usage
const store = createStore({
// ... store config
plugins: process.env.NODE_ENV !== 'production'
? [createLogger(), performancePlugin, errorTrackingPlugin]
: [persistPlugin]
});Install with Tessl CLI
npx tessl i tessl/npm-vuex