Building a Real-Time Dashboard with Next.js
Real-time data visualization is crucial for modern applications. In this tutorial, I'll walk you through building a production-ready dashboard, sharing the lessons I learned while building the BSI Performance Dashboard that monitors 65+ bank branches in real-time.
Why Real-Time Matters
During my internship at Bank Syariah Indonesia, I faced a critical problem: staff spent 2-3 hours every day compiling Excel reports manually. By the time they finished, the data was already outdated. The C-level executives needed real-time insights to make timely decisions.
This is a common challenge across industries:
- Financial institutions need live transaction monitoring
- E-commerce platforms track inventory in real-time
- SaaS companies monitor user activity as it happens
What We'll Build
By the end of this tutorial, you'll have:
- A Next.js dashboard with live data updates
- WebSocket connection with automatic reconnection
- Interactive charts that update in real-time
- Proper error handling and loading states
- Production-ready architecture
Prerequisites
Before we start, make sure you have:
- Node.js 18+ installed
- Basic knowledge of Next.js and React
- Familiarity with TypeScript (helpful but not required)
- A text editor (VS Code recommended)
The Technical Challenge
When building a real-time system, you face several challenges:
- Connection Management - WebSockets can disconnect
- State Synchronization - Multiple data sources updating simultaneously
- Performance - Rendering updates without lag
- Scalability - Handling many concurrent users
Let's tackle each of these systematically.
Step 1: Project Setup
First, create a new Next.js project:
1npx create-next-app@latest realtime-dashboard 2cd realtime-dashboard 3npm install socket.io-client chart.js react-chartjs-2 zustand
Why These Libraries?
- socket.io-client: WebSocket library with automatic reconnection
- chart.js: Powerful charting library
- react-chartjs-2: React wrapper for Chart.js
- zustand: Lightweight state management
Step 2: WebSocket Connection
Create a custom hook for WebSocket management:
1// lib/useWebSocket.ts 2import { useEffect, useState, useRef } from 'react'; 3import { io, Socket } from 'socket.io-client'; 4 5export function useWebSocket(url: string) { 6 const [isConnected, setIsConnected] = useState(false); 7 const [data, setData] = useState<any>(null); 8 const socketRef = useRef<Socket | null>(null); 9 10 useEffect(() => { 11 // Create socket connection 12 const socket = io(url, { 13 reconnection: true, 14 reconnectionDelay: 1000, 15 reconnectionAttempts: 5, 16 transports: ['websocket', 'polling'], 17 }); 18 19 // Connection event handlers 20 socket.on('connect', () => { 21 console.log('✅ WebSocket connected'); 22 setIsConnected(true); 23 }); 24 25 socket.on('disconnect', () => { 26 console.log('❌ WebSocket disconnected'); 27 setIsConnected(false); 28 }); 29 30 socket.on('error', (error) => { 31 console.error('WebSocket error:', error); 32 }); 33 34 // Listen for data updates 35 socket.on('data_update', (newData) => { 36 setData(newData); 37 }); 38 39 socketRef.current = socket; 40 41 // Cleanup on unmount 42 return () => { 43 socket.disconnect(); 44 }; 45 }, [url]); 46 47 return { isConnected, data, socket: socketRef.current }; 48}
Key Features
- Automatic Reconnection: If the connection drops, it automatically tries to reconnect
- Multiple Transports: Falls back to polling if WebSocket fails
- Error Handling: Logs errors for debugging
- Cleanup: Properly disconnects when component unmounts
Step 3: State Management
For the BSI Dashboard, I used Zustand for its simplicity:
1// store/dashboardStore.ts 2import { create } from 'zustand'; 3 4interface DashboardState { 5 branches: BranchData[]; 6 metrics: Metrics; 7 updateBranch: (id: string, data: Partial<BranchData>) => void; 8 updateMetrics: (metrics: Metrics) => void; 9} 10 11export const useDashboardStore = create<DashboardState>((set) => ({ 12 branches: [], 13 metrics: { 14 totalFunding: 0, 15 totalFinancing: 0, 16 nplRatio: 0, 17 }, 18 19 updateBranch: (id, data) => 20 set((state) => ({ 21 branches: state.branches.map((branch) => 22 branch.id === id ? { ...branch, ...data } : branch 23 ), 24 })), 25 26 updateMetrics: (metrics) => 27 set({ metrics }), 28}));
Step 4: Dashboard Component
Now let's build the main dashboard:
1// app/dashboard/page.tsx 2'use client'; 3 4import { useEffect } from 'react'; 5import { useWebSocket } from '@/lib/useWebSocket'; 6import { useDashboardStore } from '@/store/dashboardStore'; 7import { Line } from 'react-chartjs-2'; 8 9export default function Dashboard() { 10 const { isConnected, data } = useWebSocket('ws://localhost:3001'); 11 const { branches, updateBranch, updateMetrics } = useDashboardStore(); 12 13 useEffect(() => { 14 if (data) { 15 // Handle different types of updates 16 switch (data.type) { 17 case 'branch_update': 18 updateBranch(data.branchId, data.values); 19 break; 20 case 'metrics_update': 21 updateMetrics(data.metrics); 22 break; 23 } 24 } 25 }, [data]); 26 27 return ( 28 <div className="p-8"> 29 <div className="flex items-center gap-2 mb-6"> 30 <div className={`h-3 w-3 rounded-full ${isConnected ? 'bg-green-500' : 'bg-red-500'}`} /> 31 <span>{isConnected ? 'Connected' : 'Disconnected'}</span> 32 </div> 33 34 <div className="grid grid-cols-3 gap-6"> 35 {branches.map((branch) => ( 36 <BranchCard key={branch.id} branch={branch} /> 37 ))} 38 </div> 39 </div> 40 ); 41}
Step 5: Backend (Laravel Example)
For the BSI project, I built the backend with Laravel. Here's a simplified version:
1// app/Http/Controllers/DashboardController.php 2use Illuminate\Support\Facades\Redis; 3 4class DashboardController extends Controller 5{ 6 public function broadcastUpdate(Request $request) 7 { 8 $data = [ 9 'type' => 'branch_update', 10 'branchId' => $request->branch_id, 11 'values' => $request->values, 12 'timestamp' => now(), 13 ]; 14 15 // Broadcast to all connected clients 16 Redis::publish('dashboard-updates', json_encode($data)); 17 18 return response()->json(['status' => 'broadcasted']); 19 } 20}
Common Pitfalls & Solutions
1. Memory Leaks
Problem: Not cleaning up WebSocket connections
1// ❌ Bad: Memory leak 2useEffect(() => { 3 const socket = io(url); 4}, []); 5 6// ✅ Good: Proper cleanup 7useEffect(() => { 8 const socket = io(url); 9 return () => socket.disconnect(); 10}, []);
2. Too Many Re-renders
Problem: Updating state on every WebSocket message
1// ❌ Bad: Updates on every message 2socket.on('data', (data) => { 3 setState(data); // Causes re-render 4}); 5 6// ✅ Good: Debounce updates 7import { debounce } from 'lodash'; 8 9const debouncedUpdate = debounce((data) => { 10 setState(data); 11}, 100); 12 13socket.on('data', debouncedUpdate);
3. Not Handling Reconnection
Problem: Lost data during reconnection
1// ✅ Good: Request latest data on reconnect 2socket.on('connect', () => { 3 socket.emit('request_sync', { lastUpdate: lastTimestamp }); 4});
Real-World Performance
After deploying the BSI Dashboard:
- Before: 2-3 hours of manual Excel compilation
- After: Real-time updates in <1 second
- Impact: C-level executives can make decisions 180x faster
Here's the data:
- 📊 65+ branches monitored simultaneously
- ⚡ Sub-second update latency
- 🎯 99.9% uptime over 3 months
- 👥 50+ concurrent users supported
Conclusion
Building a real-time dashboard requires careful attention to:
- WebSocket connection management
- Efficient state updates
- Proper error handling
- Performance optimization
The investment pays off: what used to take hours now happens instantly.
Key Takeaways
- Always cleanup WebSocket connections to prevent memory leaks
- Implement reconnection logic from day one
- Debounce updates to avoid performance issues
- Test with poor connections - 3G, airplane mode, etc.
- Monitor performance in production with real users
What's Next?
Want to level up your dashboard?
- Add authentication to your WebSocket connection
- Implement data caching with React Query
- Build a Node.js server with Socket.io
- Deploy to production with Vercel
Found this helpful? Share it with your network or follow me on LinkedIn for more web development content!
Questions? Drop a comment below or reach out on Twitter.
