Creating custom middleware in Zustand is very flexible and can be used to implement various features like logging, state validation, performance monitoring, etc.
Basic custom middleware structure:
javascriptconst customMiddleware = (config) => (set, get, api) => { // Logic executed before original store const originalSet = set; // Wrap set function const wrappedSet = (partial, replace) => { // Execute logic before state update console.log('State will update:', partial); // Call original set const result = originalSet(partial, replace); // Execute logic after state update console.log('State updated:', get()); return result; }; // Create store const store = config(wrappedSet, get, api); // Return enhanced store return store; };
Example 1: Logger middleware
javascriptconst loggerMiddleware = (config) => (set, get, api) => { const originalSet = set; const wrappedSet = (partial, replace) => { const previousState = get(); const result = originalSet(partial, replace); const nextState = get(); console.log('Previous state:', previousState); console.log('Action:', partial); console.log('Next state:', nextState); return result; }; return config(wrappedSet, get, api); }; // Usage const useStore = create( loggerMiddleware((set) => ({ count: 0, increment: () => set((state) => ({ count: state.count + 1 })) })) );
Example 2: State validation middleware
javascriptconst validationMiddleware = (schema) => (config) => (set, get, api) => { const originalSet = set; const wrappedSet = (partial, replace) => { // Validate state update const newState = typeof partial === 'function' ? partial(get()) : partial; const validation = schema.safeParse({ ...get(), ...newState }); if (!validation.success) { console.error('State validation failed:', validation.error); throw new Error('Invalid state update'); } return originalSet(partial, replace); }; return config(wrappedSet, get, api); }; // Usage import { z } from 'zod'; const storeSchema = z.object({ count: z.number().min(0), user: z.object({ id: z.string(), name: z.string().min(1) }).nullable() }); const useStore = create( validationMiddleware(storeSchema)((set) => ({ count: 0, user: null, increment: () => set((state) => ({ count: state.count + 1 })), decrement: () => set((state) => ({ count: Math.max(0, state.count - 1) })) })) );
Example 3: Performance monitoring middleware
javascriptconst performanceMiddleware = (config) => (set, get, api) => { const originalSet = set; const renderCounts = {}; const wrappedSet = (partial, replace) => { const startTime = performance.now(); const result = originalSet(partial, replace); const endTime = performance.now(); const duration = endTime - startTime; if (duration > 10) { console.warn(`Slow state update: ${duration.toFixed(2)}ms`, partial); } return result; }; const store = config(wrappedSet, get, api); // Track component render counts const originalSubscribe = api.subscribe; api.subscribe = (listener, selector) => { const wrappedListener = (state, previousState) => { const key = selector ? selector.toString() : 'full-store'; renderCounts[key] = (renderCounts[key] || 0) + 1; if (renderCounts[key] % 10 === 0) { console.log(`Render count for ${key}:`, renderCounts[key]); } listener(state, previousState); }; return originalSubscribe(wrappedListener, selector); }; return store; }; // Usage const useStore = create( performanceMiddleware((set) => ({ count: 0, increment: () => set((state) => ({ count: state.count + 1 })) })) );
Example 4: Undo/Redo middleware
javascriptconst undoRedoMiddleware = (config) => (set, get, api) => { let history = []; let future = []; const MAX_HISTORY = 50; const originalSet = set; const wrappedSet = (partial, replace) => { const previousState = get(); const result = originalSet(partial, replace); const nextState = get(); // Save to history history.push(previousState); if (history.length > MAX_HISTORY) { history.shift(); } // Clear future future = []; return result; }; const store = config(wrappedSet, get, api); // Add undo functionality store.undo = () => { if (history.length === 0) return; const previousState = history.pop(); future.push(get()); originalSet(previousState, true); }; // Add redo functionality store.redo = () => { if (future.length === 0) return; const nextState = future.pop(); history.push(get()); originalSet(nextState, true); }; // Clear history store.clearHistory = () => { history = []; future = []; }; return store; }; // Usage const useStore = create( undoRedoMiddleware((set, get) => ({ count: 0, increment: () => set((state) => ({ count: state.count + 1 })), decrement: () => set((state) => ({ count: state.count - 1 })) })) ); // Use in component function Counter() { const { count, increment, decrement } = useStore(); const undo = useStore((state) => state.undo); const redo = useStore((state) => state.redo); return ( <div> <h1>Count: {count}</h1> <button onClick={increment}>Increment</button> <button onClick={decrement}>Decrement</button> <button onClick={undo}>Undo</button> <button onClick={redo}>Redo</button> </div> ); }
Key points:
- Custom middleware is a higher-order function that receives config and returns a new configuration function
- Can wrap set, get, and api to enhance functionality
- Middleware execution order is important, outer middleware executes first
- Can add additional features in middleware like logging, validation, performance monitoring
- Middleware can return enhanced store with new methods or properties