mirror of
https://github.com/tinyhumansai/openhuman.git
synced 2026-07-29 14:02:19 +00:00
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:
+8
-50
@@ -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>
|
||||
);
|
||||
|
||||
@@ -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
@@ -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(),
|
||||
};
|
||||
};
|
||||
|
||||
@@ -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;
|
||||
@@ -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();
|
||||
Reference in New Issue
Block a user