How to Debug React Components and Store: A Comprehensive Guide for Zustand


introducing a react debugging component
— fillics (@filippo_calio) August 21, 2025
lately ive been using Zustand (made by @dai_shi) as a state management library for @reactjs
needed a way to track state updates in real time, so i built this debugging panel that shows them and also component render tracking updated pic.twitter.com/3B1emKvf0n
Note: The tweet above contains a demonstration video showing the debugging panel in action. Watch it to see real-time state tracking and component render monitoring.
🎯 Overview
This implementation provides three core debugging features:
- Component Render Logging - Track when and why components re-render
- Store State Monitoring - Monitor all state changes with detailed diffs
- Real-time Debug Panel - Visual debugging interface with live state inspection
🏗️ Architecture
Core Components
src/
├── hooks/
│ └── useRenderLogger.ts # Component render tracking hook
├── store/
│ ├── storeLogger.ts # Zustand middleware for state logging
│ └── store.ts # Main store with logging integration
└── components/
└── DebugPanel.tsx # Visual debugging interface
🚀 Features
1. Component Render Logging
Purpose: Track component re-renders, detect unnecessary renders, and monitor performance.
Key Features:
- Automatic render counting
- Props/state change detection
- Performance warnings
- Color-coded console output
- Development-only execution
Usage:
import { useRenderLogger, useQuickRenderLog } from '@/hooks/useRenderLogger'
function MyComponent({ userId }: { userId: string }) {
const [count, setCount] = useState(0)
// Detailed logging with props and state tracking
useRenderLogger('MyComponent', { userId }, { count })
// Or simple render counting
useQuickRenderLog('MyComponent')
return <div>Component content</div>
}
Console Output:
🔄 MyComponent render #3
📊 Render Info: { timestamp: "2024-01-15T10:30:25.123Z", renderCount: 3, props: { userId: "123" }, state: { count: 5 } }
🔄 Props Changes: [{ key: "userId", previous: "122", current: "123" }]
🔄 State Changes: [{ key: "count", previous: 4, current: 5 }]
2. Store State Monitoring
Purpose: Monitor all Zustand store mutations with detailed change tracking.
Key Features:
- Automatic state change detection
- Deep diff analysis
- Action name inference
- Performance warnings for large states
- Complete mutation history
Implementation:
import { create } from 'zustand'
import { logger } from './storeLogger'
interface StoreState {
count: number
user: { name: string; email: string }
increment: () => void
updateUser: (user: Partial<StoreState['user']>) => void
}
export const useStore = create<StoreState>()(
logger(
(set, get) => ({
count: 0,
user: { name: '', email: '' },
increment: () => set((state) => ({ count: state.count + 1 })),
updateUser: (userData) => set((state) => ({
user: { ...state.user, ...userData }
})),
}),
"MyStore" // Store name for logging
)
)
Console Output:
🏪 MyStore State Change: increment
⏰ Timestamp: 2024-01-15T10:30:25.123Z
📝 Changes: [{ path: "count", previous: 4, current: 5 }]
📊 Full State: { previous: {...}, new: {...} }
3. Real-time Debug Panel
Purpose: Visual debugging interface with live state inspection and log management.
Key Features:
- Live store state visualization
- Mutation log history with filtering
- Performance metrics
- Export functionality
- Minimizable/closeable interface
Integration:
// In your layout.tsx or main component
import { DebugPanel } from '@/components/DebugPanel'
export default function Layout({ children }) {
return (
<div>
{children}
<DebugPanel />
</div>
)
}
Interface Tabs:
- State: Real-time store state JSON view
- Logs: Filterable mutation history
- Performance: Render counts and metrics
📋 Implementation Checklist
Step 1: Install Dependencies
All dependencies are already included in a standard Next.js + Zustand setup. No additional packages required.
Step 2: Create Core Files
Render Logger Hook (
src/hooks/useRenderLogger.ts
)- Component render tracking logic
- Change detection algorithms
- Console formatting utilities
Store Logger Middleware (
src/store/storeLogger.ts
)- Zustand middleware implementation
- State diffing logic
- Log history management
Debug Panel Component (
src/components/DebugPanel.tsx
)- Visual debugging interface
- Real-time state display
- Log filtering and export
Step 3: Integrate with Existing Store
// Before
export const useStore = create<StoreState>((set, get) => ({...}))
// After
export const useStore = create<StoreState>()(
logger(
(set, get) => ({...}),
"StoreName"
)
)
Step 4: Add to Layout
import { DebugPanel } from '@/components/DebugPanel'
export default function RootLayout({ children }) {
return (
<html>
<body>
{children}
<DebugPanel />
</body>
</html>
)
}
Step 5: Instrument Components
// Add to components you want to monitor
import { useRenderLogger } from '@/hooks/useRenderLogger'
function MyComponent() {
const storeValue = useStore(state => state.someValue)
useRenderLogger('MyComponent', {}, { storeValue })
return <div>...</div>
}
🎛️ Configuration Options
Render Logger Options
interface RenderLogOptions {
logProps?: boolean // Log component props (default: true)
logState?: boolean // Log component state (default: true)
logContext?: boolean // Log React context (default: false)
color?: string // Console log color (default: '#3B82F6')
enabled?: boolean // Enable/disable logging (default: development only)
}
Debug Panel Options
interface DebugPanelProps {
defaultOpen?: boolean // Start with panel open (default: false)
position?: 'top-left' | 'top-right' | 'bottom-left' | 'bottom-right'
}
🔧 Advanced Usage
Custom Event Listeners
Listen to store changes programmatically:
useEffect(() => {
const handleStateChange = (event: CustomEvent) => {
console.log('Store changed:', event.detail)
}
window.addEventListener('store-state-change', handleStateChange)
return () => window.removeEventListener('store-state-change', handleStateChange)
}, [])
Log History Management
import { getLogHistory, clearLogHistory, filterLogHistory, exportLogHistory } from '@/store/storeLogger'
// Get all logs
const allLogs = getLogHistory()
// Filter logs by action
const filteredLogs = filterLogHistory({ action: 'increment' })
// Clear history
clearLogHistory()
// Export as JSON
const exportData = exportLogHistory()
Performance Monitoring
// Monitor specific metrics
const renderInfo = useRenderLogger('ExpensiveComponent', props, state, {
enabled: process.env.NODE_ENV === 'development'
})
// Check if component is re-rendering unnecessarily
if (renderInfo.renderCount > 10 && !renderInfo.isFirstRender) {
console.warn('Component may be re-rendering too frequently')
}
🎯 Best Practices
When to Use Render Logging
✅ Do Use:
- Components with complex state logic
- Components that seem to re-render often
- Performance-critical components
- During debugging sessions
❌ Don't Use:
- Every single component (creates noise)
- Components that render very frequently (e.g., animations)
- Production builds (automatically disabled)
Store Logging Best Practices
✅ Do:
- Use descriptive store names
- Monitor stores with complex state
- Review logs during development
- Export logs for debugging complex issues
❌ Don't:
- Log sensitive data (passwords, tokens)
- Keep extensive log history in memory
- Enable in production
Debug Panel Usage
✅ Do:
- Keep panel minimized during normal development
- Use filtering to focus on specific issues
- Export logs for team collaboration
- Check performance tab for optimization opportunities
❌ Don't:
- Leave panel open during performance testing
- Ignore performance warnings
- Keep large log histories
🚨 Troubleshooting
Logs Not Appearing
- Check Environment: Ensure
NODE_ENV=development
- Console Filters: Check browser console filters
- Component Integration: Verify
useRenderLogger
is called - Store Integration: Confirm middleware is applied
Debug Panel Not Visible
- Check Import: Ensure
DebugPanel
is imported in layout - Z-Index Issues: Panel uses
z-50
, check for conflicts - Development Mode: Panel only appears in development
Performance Issues
- Limit Logging: Don't log every component
- Clear History: Regularly clear log history
- Filter Logs: Use filtering to reduce noise
- Disable in Production: Ensure logging is development-only
📊 Performance Impact
- Development: Minimal impact (~1-2ms per render)
- Production: Zero impact (automatically disabled)
- Memory: ~100KB for typical log history
- Bundle Size: ~15KB gzipped (dev only)
🔄 Updates and Maintenance
Adding New Metrics
Extend the debug panel to track custom metrics:
// In DebugPanel.tsx
const customMetrics = {
apiCalls: trackApiCalls(),
errorCount: trackErrors(),
userInteractions: trackInteractions()
}
Custom Log Formats
Modify storeLogger.ts
to customize log output:
// Custom formatting function
function formatLogEntry(entry: LogEntry): void {
// Your custom formatting logic
}
📝 License and Support
This debugging implementation is designed for development use and should be adapted to your specific needs. The system is built to be:
- Zero-impact in production
- Easy to integrate
- Highly customizable
- Performance-conscious
Subscribe to my newsletter
Read articles from Filippo Caliò directly inside your inbox. Subscribe to the newsletter, and don't miss out.
Written by
