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

Filippo CaliòFilippo Caliò
6 min read

introducing a react debugging component

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

— fillics (@filippo_calio) August 21, 2025

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:

  1. Component Render Logging - Track when and why components re-render
  2. Store State Monitoring - Monitor all state changes with detailed diffs
  3. 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

  1. Render Logger Hook (src/hooks/useRenderLogger.ts)

    • Component render tracking logic
    • Change detection algorithms
    • Console formatting utilities
  2. Store Logger Middleware (src/store/storeLogger.ts)

    • Zustand middleware implementation
    • State diffing logic
    • Log history management
  3. 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

  1. Check Environment: Ensure NODE_ENV=development
  2. Console Filters: Check browser console filters
  3. Component Integration: Verify useRenderLogger is called
  4. Store Integration: Confirm middleware is applied

Debug Panel Not Visible

  1. Check Import: Ensure DebugPanel is imported in layout
  2. Z-Index Issues: Panel uses z-50, check for conflicts
  3. Development Mode: Panel only appears in development

Performance Issues

  1. Limit Logging: Don't log every component
  2. Clear History: Regularly clear log history
  3. Filter Logs: Use filtering to reduce noise
  4. 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
0
Subscribe to my newsletter

Read articles from Filippo Caliò directly inside your inbox. Subscribe to the newsletter, and don't miss out.

Written by

Filippo Caliò
Filippo Caliò