From 8676afbc794a7c50f804c2a2ed60431ffe9f94cc Mon Sep 17 00:00:00 2001 From: Steven Enamakel Date: Wed, 28 Jan 2026 04:37:04 +0530 Subject: [PATCH] Refactor App component to utilize SocketProvider and AppRoutes - Replaced direct route definitions in the App component with a new AppRoutes component for improved organization and maintainability. - Introduced SocketProvider to manage socket connections based on JWT token, enhancing real-time communication capabilities. - Updated the useSocket hook to rely on Redux state for connection status, streamlining socket management. These changes enhance the application's architecture by separating concerns and improving the overall structure of routing and socket management. --- src/App.tsx | 58 ++------------ src/AppRoutes.tsx | 55 +++++++++++++ src/hooks/useSocket.ts | 22 ++---- src/providers/SocketProvider.tsx | 40 ++++++++++ src/services/socketService.ts | 132 +++++++++++++++++++++++++++++++ 5 files changed, 243 insertions(+), 64 deletions(-) create mode 100644 src/AppRoutes.tsx create mode 100644 src/providers/SocketProvider.tsx create mode 100644 src/services/socketService.ts diff --git a/src/App.tsx b/src/App.tsx index 47d2e20d8..0a06a9648 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -1,61 +1,19 @@ -import { BrowserRouter as Router, Routes, Route } from 'react-router-dom'; +import { BrowserRouter as Router } from 'react-router-dom'; import { Provider } from 'react-redux'; import { PersistGate } from 'redux-persist/integration/react'; import { store, persistor } from './store'; -import Welcome from './pages/Welcome'; -import Login from './pages/Login'; -import Onboarding from './pages/onboarding/Onboarding'; -import Home from './pages/Home'; -import PublicRoute from './components/PublicRoute'; -import ProtectedRoute from './components/ProtectedRoute'; -import DefaultRedirect from './components/DefaultRedirect'; +import SocketProvider from './providers/SocketProvider'; +import AppRoutes from './AppRoutes'; function App() { return ( - - - {/* Public routes - redirect to /home or /onboarding if logged in */} - - - - } - /> - - - - } - /> - - {/* Protected routes */} - - - - } - /> - - - - } - /> - - {/* Default redirect based on auth status */} - } /> - - + + + + + ); diff --git a/src/AppRoutes.tsx b/src/AppRoutes.tsx new file mode 100644 index 000000000..f6199d629 --- /dev/null +++ b/src/AppRoutes.tsx @@ -0,0 +1,55 @@ +import { Routes, Route } from 'react-router-dom'; +import Welcome from './pages/Welcome'; +import Login from './pages/Login'; +import Onboarding from './pages/onboarding/Onboarding'; +import Home from './pages/Home'; +import PublicRoute from './components/PublicRoute'; +import ProtectedRoute from './components/ProtectedRoute'; +import DefaultRedirect from './components/DefaultRedirect'; + +const AppRoutes = () => { + return ( + + {/* Public routes - redirect to /home or /onboarding if logged in */} + + + + } + /> + + + + } + /> + + {/* Protected routes */} + + + + } + /> + + + + } + /> + + {/* Default redirect based on auth status */} + } /> + + ); +}; + +export default AppRoutes; diff --git a/src/hooks/useSocket.ts b/src/hooks/useSocket.ts index be234726c..cafbc333f 100644 --- a/src/hooks/useSocket.ts +++ b/src/hooks/useSocket.ts @@ -1,13 +1,12 @@ import { useEffect, useRef } from 'react'; import { socketService } from '../services/socketService'; +import { useAppSelector } from '../store/hooks'; import type { Socket } from 'socket.io-client'; -interface UseSocketOptions { - autoConnect?: boolean; -} - /** * React hook for using the Socket.IO connection + * Note: The socket connection is managed by SocketProvider based on JWT token. + * This hook provides access to the socket instance and methods. * * @example * ```tsx @@ -24,15 +23,11 @@ interface UseSocketOptions { * }, [on, off]); * ``` */ -export const useSocket = (options: UseSocketOptions = {}) => { - const { autoConnect = true } = options; +export const useSocket = () => { const listenersRef = useRef void }>>([]); + const socketStatus = useAppSelector((state) => state.socket.status); useEffect(() => { - if (autoConnect) { - socketService.connect(); - } - return () => { // Cleanup: remove all listeners registered through this hook listenersRef.current.forEach(({ event, callback }) => { @@ -40,7 +35,7 @@ export const useSocket = (options: UseSocketOptions = {}) => { }); listenersRef.current = []; }; - }, [autoConnect]); + }, []); const emit = (event: string, data?: unknown) => { socketService.emit(event, data); @@ -68,12 +63,11 @@ export const useSocket = (options: UseSocketOptions = {}) => { return { socket: socketService.getSocket() as Socket | null, - isConnected: socketService.isConnected(), + isConnected: socketStatus === 'connected', + status: socketStatus, emit, on, off, once, - connect: () => socketService.connect(), - disconnect: () => socketService.disconnect(), }; }; diff --git a/src/providers/SocketProvider.tsx b/src/providers/SocketProvider.tsx new file mode 100644 index 000000000..d8844cc62 --- /dev/null +++ b/src/providers/SocketProvider.tsx @@ -0,0 +1,40 @@ +import { useEffect, useRef } from 'react'; +import { useAppSelector } from '../store/hooks'; +import { socketService } from '../services/socketService'; + +/** + * SocketProvider manages the socket connection based on JWT token + * - Connects when token is set + * - Disconnects when token is unset + */ +const SocketProvider = ({ children }: { children: React.ReactNode }) => { + const token = useAppSelector((state) => state.auth.token); + const previousTokenRef = useRef(null); + + useEffect(() => { + const previousToken = previousTokenRef.current; + + // Token was set - connect + if (token && token !== previousToken) { + socketService.connect(token); + previousTokenRef.current = token; + } + + // Token was unset - disconnect + if (!token && previousToken) { + socketService.disconnect(); + previousTokenRef.current = null; + } + }, [token]); + + // Cleanup on unmount + useEffect(() => { + return () => { + socketService.disconnect(); + }; + }, []); + + return <>{children}; +}; + +export default SocketProvider; diff --git a/src/services/socketService.ts b/src/services/socketService.ts new file mode 100644 index 000000000..730263284 --- /dev/null +++ b/src/services/socketService.ts @@ -0,0 +1,132 @@ +import { io, Socket } from 'socket.io-client'; +import { BACKEND_URL } from '../utils/config'; +import { store } from '../store'; +import { setStatus, setSocketId, reset } from '../store/socketSlice'; + +class SocketService { + private socket: Socket | null = null; + private token: string | null = null; + + /** + * Connect to the socket server with authentication + */ + connect(token: string): void { + // Don't connect if already connected with the same token + if (this.socket?.connected && this.token === token) { + return; + } + + // Disconnect existing connection if token changed + if (this.socket && this.token !== token) { + this.disconnect(); + } + + // Don't connect if socket exists and is not disconnected (already connecting or connected) + if (this.socket && !this.socket.disconnected) { + return; + } + + this.token = token; + + // Update status to connecting + store.dispatch(setStatus('connecting')); + + // Create socket connection with auth token + this.socket = io(BACKEND_URL, { + auth: { + token, + }, + transports: ['websocket', 'polling'], + reconnection: true, + reconnectionDelay: 1000, + reconnectionAttempts: 5, + }); + + // Connection event handlers + this.socket.on('connect', () => { + const socketId = this.socket?.id || null; + store.dispatch(setStatus('connected')); + store.dispatch(setSocketId(socketId)); + }); + + this.socket.on('disconnect', () => { + store.dispatch(setStatus('disconnected')); + store.dispatch(setSocketId(null)); + }); + + this.socket.on('connect_error', (error) => { + console.error('[Socket] Connection error:', error); + store.dispatch(setStatus('disconnected')); + }); + } + + /** + * Disconnect from the socket server + */ + disconnect(): void { + if (this.socket) { + this.socket.disconnect(); + this.socket = null; + this.token = null; + store.dispatch(reset()); + } + } + + /** + * Get the current socket instance + */ + getSocket(): Socket | null { + return this.socket; + } + + /** + * Check if socket is connected + */ + isConnected(): boolean { + return this.socket?.connected || false; + } + + /** + * Emit an event to the server + */ + emit(event: string, data?: unknown): void { + if (this.socket?.connected) { + this.socket.emit(event, data); + } else { + console.warn(`[Socket] Cannot emit '${event}': socket not connected`); + } + } + + /** + * Listen to an event from the server + */ + on(event: string, callback: (...args: unknown[]) => void): void { + if (this.socket) { + this.socket.on(event, callback); + } + } + + /** + * Remove an event listener + */ + off(event: string, callback?: (...args: unknown[]) => void): void { + if (this.socket) { + if (callback) { + this.socket.off(event, callback); + } else { + this.socket.off(event); + } + } + } + + /** + * Listen to an event once + */ + once(event: string, callback: (...args: unknown[]) => void): void { + if (this.socket) { + this.socket.once(event, callback); + } + } +} + +export const socketService = new SocketService();