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.
This commit is contained in:
Steven Enamakel
2026-01-28 04:37:04 +05:30
parent 35cf5d7bf3
commit 8676afbc79
5 changed files with 243 additions and 64 deletions
+8 -50
View File
@@ -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 (
<Provider store={store}>
<PersistGate loading={null} persistor={persistor}>
<Router>
<Routes>
{/* Public routes - redirect to /home or /onboarding if logged in */}
<Route
path="/"
element={
<PublicRoute>
<Welcome />
</PublicRoute>
}
/>
<Route
path="/login"
element={
<PublicRoute>
<Login />
</PublicRoute>
}
/>
{/* Protected routes */}
<Route
path="/onboarding"
element={
<ProtectedRoute requireAuth={true} requireOnboarded={false}>
<Onboarding />
</ProtectedRoute>
}
/>
<Route
path="/home"
element={
<ProtectedRoute requireAuth={true} requireOnboarded={true} redirectTo="/onboarding">
<Home />
</ProtectedRoute>
}
/>
{/* Default redirect based on auth status */}
<Route path="*" element={<DefaultRedirect />} />
</Routes>
</Router>
<SocketProvider>
<Router>
<AppRoutes />
</Router>
</SocketProvider>
</PersistGate>
</Provider>
);
+55
View File
@@ -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 (
<Routes>
{/* Public routes - redirect to /home or /onboarding if logged in */}
<Route
path="/"
element={
<PublicRoute>
<Welcome />
</PublicRoute>
}
/>
<Route
path="/login"
element={
<PublicRoute>
<Login />
</PublicRoute>
}
/>
{/* Protected routes */}
<Route
path="/onboarding"
element={
<ProtectedRoute requireAuth={true} requireOnboarded={false}>
<Onboarding />
</ProtectedRoute>
}
/>
<Route
path="/home"
element={
<ProtectedRoute requireAuth={true} requireOnboarded={true} redirectTo="/onboarding">
<Home />
</ProtectedRoute>
}
/>
{/* Default redirect based on auth status */}
<Route path="*" element={<DefaultRedirect />} />
</Routes>
);
};
export default AppRoutes;
+8 -14
View File
@@ -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<Array<{ event: string; callback: (...args: unknown[]) => 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(),
};
};
+40
View File
@@ -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<string | null>(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;
+132
View File
@@ -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();