乐闻世界logo
搜索文章和话题

How to use middleware and interceptors in MobX?

2月21日 15:49

MobX middleware and interceptors provide powerful functionality to execute custom logic before and after state changes. Here's a detailed guide to using MobX middleware and interceptors:

1. Interceptors

Basic Usage

Interceptors allow you to intercept and modify operations before state changes.

javascript
import { observable, intercept } from 'mobx'; const store = observable({ count: 0, items: [] }); // Intercept count changes const dispose = intercept(store, 'count', (change) => { console.log('Before change:', change); // Can modify the change if (change.newValue < 0) { change.newValue = 0; // Don't allow negative numbers } // Can cancel the change if (change.newValue > 100) { return null; // Cancel the change } return change; // Allow the change }); store.count = 5; // Before change: { type: 'update', object: store, name: 'count', newValue: 5 } console.log(store.count); // 5 store.count = -1; // Before change: { type: 'update', object: store, name: 'count', newValue: -1 } console.log(store.count); // 0 (modified by interceptor) store.count = 101; // Before change: { type: 'update', object: store, name: 'count', newValue: 101 } console.log(store.count); // 0 (cancelled by interceptor) dispose(); // Clean up interceptor

Intercept Array Operations

javascript
const items = observable([1, 2, 3]); intercept(items, (change) => { console.log('Array change:', change); // Intercept push operation if (change.type === 'add') { if (typeof change.newValue !== 'number') { throw new Error('Only numbers allowed'); } } return change; }); items.push(4); // Array change: { type: 'add', object: items, name: '3', newValue: 4 } items.push('invalid'); // Error: Only numbers allowed

Intercept Map Operations

javascript
const map = observable(new Map()); intercept(map, (change) => { console.log('Map change:', change); // Intercept set operation if (change.type === 'update' || change.type === 'add') { if (change.name === 'secret') { throw new Error('Cannot set secret key'); } } return change; }); map.set('key1', 'value1'); // Map change: { type: 'add', object: map, name: 'key1', newValue: 'value1' } map.set('secret', 'value'); // Error: Cannot set secret key

2. Observers

Basic Usage

Observers allow you to execute custom logic after state changes.

javascript
import { observable, observe } from 'mobx'; const store = observable({ count: 0, items: [] }); // Observe count changes const dispose = observe(store, 'count', (change) => { console.log('After change:', change); console.log('Old value:', change.oldValue); console.log('New value:', change.newValue); }); store.count = 5; // After change: { type: 'update', object: store, name: 'count', oldValue: 0, newValue: 5 } dispose(); // Clean up observer

Observe Array Changes

javascript
const items = observable([1, 2, 3]); observe(items, (change) => { console.log('Array changed:', change); if (change.type === 'splice') { console.log('Added:', change.added); console.log('Removed:', change.removed); console.log('Index:', change.index); } }); items.push(4); // Array changed: { type: 'splice', object: items, added: [4], removed: [], index: 3 } items.splice(1, 1); // Array changed: { type: 'splice', object: items, added: [], removed: [2], index: 1 }

Observe All Object Changes

javascript
const store = observable({ count: 0, name: 'John', items: [] }); // Observe all property changes const dispose = observe(store, (change) => { console.log(`${change.name} changed from ${change.oldValue} to ${change.newValue}`); }); store.count = 1; // count changed from 0 to 1 store.name = 'Jane'; // name changed from John to Jane dispose();

3. Middleware

Creating Custom Middleware

javascript
import { observable, action, runInAction } from 'mobx'; function loggingMiddleware(store, methodName, actionFn) { return function(...args) { console.log(`[Action] ${methodName} called with:`, args); const startTime = performance.now(); const result = actionFn.apply(this, args); const endTime = performance.now(); console.log(`[Action] ${methodName} completed in ${endTime - startTime}ms`); return result; }; } class Store { @observable count = 0; constructor() { makeAutoObservable(this); } @action increment = () => { this.count++; }; @action decrement = () => { this.count--; }; } // Apply middleware const store = new Store(); const originalIncrement = store.increment.bind(store); store.increment = loggingMiddleware(store, 'increment', originalIncrement);

Using Action Hooks

javascript
import { action, configure } from 'mobx'; configure({ // Enable action hooks enforceActions: 'always' }); // Global action hook const originalAction = action.bound; action.bound = function(target, propertyKey, descriptor) { console.log(`[Action] ${propertyKey} is being defined`); return originalAction(target, propertyKey, descriptor); };

Error Handling Middleware

javascript
function errorHandlingMiddleware(store, methodName, actionFn) { return async function(...args) { try { return await actionFn.apply(this, args); } catch (error) { console.error(`[Error] ${methodName} failed:`, error); // Can store error in store if (store.errorStore) { store.errorStore.addError(error); } throw error; } }; } class Store { @observable data = null; @observable error = null; constructor() { makeAutoObservable(this); } @action fetchData = async () => { this.data = await fetch('/api/data').then(r => r.json()); }; } // Apply error handling middleware const store = new Store(); store.fetchData = errorHandlingMiddleware(store, 'fetchData', store.fetchData);

4. Implementing Undo/Redo with Interceptors and Observers

javascript
class HistoryStore { @observable past = []; @observable future = []; constructor(targetStore) { this.targetStore = targetStore; makeAutoObservable(this); this.setupInterceptors(); } setupInterceptors() { // Intercept all state changes Object.keys(this.targetStore).forEach(key => { if (this.targetStore[key] && typeof this.targetStore[key] === 'object') { intercept(this.targetStore, key, (change) => { // Save current state to past this.past.push({ key, oldValue: change.oldValue, timestamp: Date.now() }); // Clear future this.future = []; return change; }); } }); } @action undo = () => { if (this.past.length === 0) return; const lastChange = this.past.pop(); this.future.push(lastChange); // Restore old value this.targetStore[lastChange.key] = lastChange.oldValue; }; @action redo = () => { if (this.future.length === 0) return; const nextChange = this.future.pop(); this.past.push(nextChange); // Restore new value this.targetStore[nextChange.key] = nextChange.newValue; }; @computed get canUndo() { return this.past.length > 0; } @computed get canRedo() { return this.future.length > 0; } }

5. Performance Monitoring Middleware

javascript
function performanceMonitoringMiddleware(store, methodName, actionFn) { return function(...args) { const startTime = performance.now(); const result = actionFn.apply(this, args); const endTime = performance.now(); const duration = endTime - startTime; // Record performance data if (!store.performanceMetrics) { store.performanceMetrics = {}; } if (!store.performanceMetrics[methodName]) { store.performanceMetrics[methodName] = { count: 0, totalTime: 0, maxTime: 0, minTime: Infinity }; } const metrics = store.performanceMetrics[methodName]; metrics.count++; metrics.totalTime += duration; metrics.maxTime = Math.max(metrics.maxTime, duration); metrics.minTime = Math.min(metrics.minTime, duration); // Warn about slow operations if (duration > 100) { console.warn(`[Performance] ${methodName} took ${duration.toFixed(2)}ms`); } return result; }; }

6. Permission Control Middleware

javascript
function permissionMiddleware(store, methodName, actionFn, permissions) { return function(...args) { const user = store.userStore?.user; if (!user) { throw new Error('User not authenticated'); } if (permissions && !user.permissions.includes(permissions)) { throw new Error(`User does not have permission: ${permissions}`); } return actionFn.apply(this, args); }; } class Store { @observable data = []; constructor() { makeAutoObservable(this); } @action addItem = (item) => { this.data.push(item); }; @action deleteItem = (id) => { this.data = this.data.filter(item => item.id !== id); }; } // Apply permission middleware const store = new Store(); store.addItem = permissionMiddleware(store, 'addItem', store.addItem, 'write'); store.deleteItem = permissionMiddleware(store, 'deleteItem', store.deleteItem, 'delete');

7. Logging Middleware

javascript
function loggingMiddleware(store, methodName, actionFn) { return function(...args) { const logEntry = { timestamp: new Date().toISOString(), action: methodName, args: JSON.parse(JSON.stringify(args)), result: null, error: null }; try { const result = actionFn.apply(this, args); logEntry.result = JSON.parse(JSON.stringify(result)); return result; } catch (error) { logEntry.error = { message: error.message, stack: error.stack }; throw error; } finally { // Send log to server or store locally if (store.logStore) { store.logStore.addLog(logEntry); } } }; }

8. Debounce and Throttle Middleware

javascript
function debounceMiddleware(store, methodName, actionFn, delay = 300) { let timeoutId = null; return function(...args) { if (timeoutId) { clearTimeout(timeoutId); } timeoutId = setTimeout(() => { actionFn.apply(this, args); timeoutId = null; }, delay); }; } function throttleMiddleware(store, methodName, actionFn, delay = 300) { let lastCallTime = 0; return function(...args) { const now = Date.now(); const timeSinceLastCall = now - lastCallTime; if (timeSinceLastCall >= delay) { actionFn.apply(this, args); lastCallTime = now; } }; } class SearchStore { @observable query = ''; @observable results = []; constructor() { makeAutoObservable(this); } @action performSearch = async (query) => { this.results = await api.search(query); }; } // Apply debounce middleware const searchStore = new SearchStore(); searchStore.performSearch = debounceMiddleware( searchStore, 'performSearch', searchStore.performSearch, 300 );

Summary

MobX middleware and interceptors provide powerful functionality:

  1. Interceptors: Intercept and modify operations before state changes
  2. Observers: Execute custom logic after state changes
  3. Middleware: Wrap actions to add extra functionality
  4. Common applications: Undo/redo, performance monitoring, permission control, logging, debounce/throttle

Using these features correctly can build more powerful and flexible MobX applications.

标签:Mobx