I am building a medium sized project on React with Vite. The websocket connection gets closed and re-established when navigate to another page using the sidebar
main.tsx :-
import ReactDOM from 'react-dom/client';
import App from './App';
import './index.css';
import { AuthProvider } from '@/contexts/AuthContext';
import React from 'react';
import { WebSocketProvider } from '@/contexts/WebSocketContext';
const Root = React.memo(() => (
<React.StrictMode>
<AuthProvider>
<WebSocketProvider>
<App />
</WebSocketProvider>
</AuthProvider>
</React.StrictMode>
));
ReactDOM.createRoot(document.getElementById('root')!).render(<Root />);
router.tsx :-
import { createBrowserRouter, RouterProvider } from 'react-router-dom';
import SignInPage from '@/pages/SignInPage';
import TasksPage from '@/pages/Tasks/TasksPage';
import CreateTaskPage from '@/pages/Tasks/CreateTaskPage';
import DashboardPage from '@/pages/Dashboard/DashboardPage';
import AuthRedirect from '@/components/AuthComponents/AuthRedirect';
import ProtectedRoute from '@/components/AuthComponents/ProtectedRoute';
import TaskViewPage from '@/pages/Tasks/TaskViewPage';
import EditTaskPage from '@/pages/Tasks/EditTaskPage';
import MembersPage from '@/pages/Members/Members/MembersPage';
import CreateMemberPage from '@/pages/Members/Members/CreateMemberPage';
import MemberViewPage from '@/pages/Members/Members/MemberViewPage';
import MemberEditPage from '@/pages/Members/Members/MemberEditPage';
import MemberDashboardPage from '@/pages/Members/Dashboard/MemberDashboardPage';
import MemberActivity from '@/pages/Activity/MemberActivity';
import MyActivity from '@/pages/Activity/MyActivity';
import NotificationsPage from '@/pages/Activity/NotificationsPage';
import Alerts from '@/pages/Activity/Alerts';
import Account from '@/pages/Account/Account';
import NotFoundPage from "@/pages/404";
const router = createBrowserRouter([
{
path: '/',
element: <AuthRedirect />,
},
{
path: 'sign-in',
element: <SignInPage />,
},
{
path: 'tasks',
element: (
<ProtectedRoute>
<TasksPage />
</ProtectedRoute>
),
},
{
path: 'create-task',
element: (
<ProtectedRoute>
<CreateTaskPage />
</ProtectedRoute>
),
},
{
path: 'dashboard',
element: (
<ProtectedRoute>
<DashboardPage />
</ProtectedRoute>
),
},
{
path: 'task/:id',
element: (
<ProtectedRoute>
<TaskViewPage />
</ProtectedRoute>
),
},
{
path: 'task/edit/:id',
element: (
<ProtectedRoute>
<EditTaskPage />
</ProtectedRoute>
),
},
{
path: 'members',
element: (
<ProtectedRoute>
<MembersPage />
</ProtectedRoute>
),
},
{
path: '/members/create',
element: (
<ProtectedRoute>
<CreateMemberPage />
</ProtectedRoute>
),
},
{
path: '/member/:id',
element: (
<ProtectedRoute>
<MemberViewPage />
</ProtectedRoute>
),
},
{
path: '/member/edit/:id',
element: (
<ProtectedRoute>
<MemberEditPage />
</ProtectedRoute>
),
},
{
path: '/members/dashboard',
element: (
<ProtectedRoute>
<MemberDashboardPage />
</ProtectedRoute>
),
},
{
path: '/activity/members',
element: (
<ProtectedRoute>
<MemberActivity />
</ProtectedRoute>
),
},
{
path: '/activity/member',
element: (
<ProtectedRoute>
<MyActivity />
</ProtectedRoute>
),
},
{
path: '/activity/notifications',
element: (
<ProtectedRoute>
<NotificationsPage />
</ProtectedRoute>
),
},
{
path: '/activity/alerts',
element: (
<ProtectedRoute>
<Alerts />
</ProtectedRoute>
),
},
{
path: '/account',
element: (
<ProtectedRoute>
<Account />
</ProtectedRoute>
),
},
{
path: "*",
element: (
<ProtectedRoute>
<NotFoundPage />
</ProtectedRoute>
)
}
]);
export function Router() {
return <RouterProvider router={router} />;
}
WebSocketContext.tsx :-
import React, { createContext, ReactNode, useContext, useEffect, useState } from 'react';
import { getMemberId } from '@/utils/auth';
import { Notification } from '@/types/NotificationTypes';
import { nanoid } from 'nanoid';
interface WebSocketContextType {
notifications: Notification[];
addNotification: (newNotification: Notification) => void;
removeNotification: (notificationId: string) => void;
}
const WebSocketContext = createContext<WebSocketContextType | undefined>(undefined);
export const WebSocketProvider: React.FC<{ children: ReactNode }> = ({ children }) => {
const [notifications, setNotifications] = useState<Notification[]>([]);
const userId = getMemberId();
const addNotification = (newNotification: Notification) => {
newNotification = { ...newNotification, id: nanoid(), vanish: true };
setNotifications([...notifications, newNotification]);
setTimeout(() => {
removeNotification(newNotification.id ?? '');
}, 5000);
};
const removeNotification = (notificationId: string) => {
setNotifications((notifications) =>
notifications.filter((notification) => notification.id !== notificationId)
);
};
useEffect(() => {
if (!userId) {
console.error('User ID is not available.');
return;
}
const socket = new WebSocket(`wss://apiuverp.stepnex.tech/notifications/ws/${userId}`);
socket.onopen = () => {
console.log('WebSocket connection established');
};
socket.onmessage = (event) => {
try {
const newNotification = JSON.parse(event.data);
if (
newNotification &&
newNotification.description &&
newNotification.priority &&
newNotification.url &&
newNotification.id
) {
setNotifications((prevMessages) => [...prevMessages, newNotification]);
const acknowledgment = {
action: 'acknowledge',
notificationId: newNotification.id,
memberId: userId,
};
socket.send(JSON.stringify(acknowledgment));
} else {
console.warn('Received malformed message:', newNotification);
}
} catch (error) {
console.error('Error parsing WebSocket message:', error);
}
};
socket.onerror = (error) => {
console.error('WebSocket error:', error);
};
socket.onclose = (event) => {
console.log('WebSocket connection closed', event);
};
return () => {
socket.close();
};
}, [userId]);
return (
<WebSocketContext.Provider value={{ notifications, addNotification, removeNotification }}>
{children}
</WebSocketContext.Provider>
);
};
export const useWebSocketMessages = () => {
const context = useContext(WebSocketContext);
if (context === undefined) {
throw new Error('useWebSocketMessages must be used within a WebSocketProvider');
}
return context;
};
MainLayout.tsx (where i display notification) :-
'use client';
import React, { useEffect, useState } from 'react';
import { Dialog, DialogBackdrop, DialogPanel, TransitionChild } from '@headlessui/react';
import { Bars3Icon, BellIcon, UserCircleIcon, XMarkIcon } from '@heroicons/react/24/outline';
import Sidebar from '@/components/Common/Sidebar';
import MobileSidebar from '@/components/Common/MobileSidebar';
import TopBar from '@/components/Common/TopBar';
import { useWebSocketMessages } from '@/contexts/WebSocketContext';
import { useNavigate } from 'react-router-dom';
import axiosInstance from '@/axiosInstance';
import { Notification, NotificationPriority } from '@/types/NotificationTypes';
import navigation from '@/utils/NavigationItems';
import { Scope } from '@/types/EmployeeTypes';
import NotificationDropdown from '@/components/Notification/NotificationDropdown';
import { getScope } from '@/utils/auth';
import NotificationComponent from '@/components/Notification/NotificationComponent';
import HighPriorityNotification from '@/components/Notification/HighPriorityNotification';
interface SidebarLayoutProps {
children: React.ReactNode;
pageName: string;
}
export default function MainLayout({ children, pageName }: SidebarLayoutProps) {
const [sidebarOpen, setSidebarOpen] = useState(false);
const [localNotifications, setLocalNotifications] = useState<Notification[]>([]);
const [isDropdownOpen, setIsDropdownOpen] = useState(false);
const { notifications, removeNotification } = useWebSocketMessages();
const [highPriorityNotificationExists, setHighPriorityNotificationExists] =
useState<boolean>(false);
const userScope = getScope();
const handleNotificationIconClick = () => {
setIsDropdownOpen(!isDropdownOpen);
};
useEffect(() => {
setLocalNotifications(notifications);
}, [notifications]);
useEffect(() => {
if (notifications.length > 0) {
const latestMessage = notifications[notifications.length - 1];
console.log('New notification:', latestMessage);
setLocalNotifications((prevNotifications) => [...prevNotifications, latestMessage]);
}
}, [notifications]);
const navigate = useNavigate();
useEffect(() => {
const fetchTasksWithDeadlines = async () => {
try {
const response = await axiosInstance.get('/tasks/dead_task'); // Replace with your API endpoint
if (typeof response.data === 'string' && response.data !== 'null') {
navigate(`/task/${response.data}`);
} else if (response.data === null) {
return;
} else {
console.error('Unexpected response format');
}
} catch (error) {
console.error('Error fetching tasks:', error);
}
};
fetchTasksWithDeadlines();
}, [navigate]);
return (
<div className={'antialiasing'}>
<div className={'h-full bg-gray-400'}>
<div className={'z-50 text-black'}>
<Dialog open={sidebarOpen} onClose={setSidebarOpen} className="relative z-50 lg:hidden">
<DialogBackdrop
transition
className="fixed inset-0 bg-gray-900/80 transition-opacity duration-300 ease-linear data-[closed]:opacity-0"
/>
<div className="fixed inset-0 flex">
<DialogPanel
transition
className="relative mr-16 flex w-full max-w-xs flex-1 transform transition duration-300 ease-in-out data-[closed]:-translate-x-full"
>
<TransitionChild>
<div className="absolute left-full top-0 flex w-16 justify-center pt-5 duration-300 ease-in-out data-[closed]:opacity-0">
<button
type="button"
onClick={() => setSidebarOpen(false)}
className="-m-2.5 p-2.5"
>
<span className="sr-only">Close sidebar</span>
<XMarkIcon aria-hidden="true" className="h-6 w-6 text-white" />
</button>
</div>
</TransitionChild>
{/* Sidebar component, swap this element with another sidebar if you like */}
<MobileSidebar Navigation={navigation} />
</DialogPanel>
</div>
</Dialog>
<TopBar pageName={pageName} />
</div>
{/* Static sidebar for desktop */}
<div className="hidden lg:fixed lg:inset-y-0 lg:z-50 lg:flex lg:w-72 lg:flex-col">
{/* Sidebar component, swap this element with another sidebar if you like */}
<Sidebar />
</div>
<div className="sticky top-0 z-40 flex items-center gap-x-6 bg-white px-4 py-4 shadow-sm sm:px-6 lg:hidden">
<button
type="button"
onClick={() => setSidebarOpen(true)}
className="-m-2.5 p-2.5 text-gray-700 lg:hidden"
>
<span className="sr-only">Open sidebar</span>
<Bars3Icon aria-hidden="true" className="h-6 w-6" />
</button>
<h2 className={'flex flex-row flex-grow'}>{pageName}</h2>
{userScope !== Scope.EM && (
<div>
<span className="inline-flex items-center rounded-md bg-custom-blue-1 px-2 py-1 text-lg font-medium text-custom-blue-2 ring-1 ring-inset ring-blue-700/10">
Admin
</span>
</div>
)}
<div className={'ml-10 flex flex-row justify-end gap-8'}>
<div className="relative">
<div
className="relative hover:bg-gray-100 rounded-full cursor-pointer"
onClick={handleNotificationIconClick}
>
<BellIcon height={32} />
</div>
{isDropdownOpen && (
<div className="absolute right-0 mt-2">
<NotificationDropdown />
</div>
)}
</div>
<UserCircleIcon height={32} />
</div>
</div>
<main className="py-1 lg:pl-64 bg-background-grey min-h-screen relative lg:pt-16">
<div className="px-2 sm:px-4 lg:px-8 relative flex min-h-full flex-col bg-background-grey gap-4 my-4">
{children}
</div>
</main>
</div>
{localNotifications.length > 0 && (
<div className="flex flex-col fixed bottom-4 z-50 w-auto gap-2 right-4">
{notifications.map((notification, index) => {
if (notification.priority === NotificationPriority.HIGH) {
console.log('HIGH PRIORITY NOTIFICATION RECEIVED');
return (
<HighPriorityNotification
setHighPriorityNotificationExists={setHighPriorityNotificationExists}
agenda={notification.agenda}
description={notification.description}
url={notification.url}
/>
);
}
return (
<NotificationComponent
id={notification.id}
removeNotification={removeNotification}
key={index}
priority={notification.priority}
agenda={notification.agenda}
description={notification.description}
success={true}
button_name={notification.button_name}
url={notification.url}
created_at={notification.created_at}
/>
);
})}
</div>
)}
</div>
);
}
Sidebar.tsx :-
import { Disclosure, DisclosureButton, DisclosurePanel } from '@headlessui/react';
import { ChevronRightIcon } from '@heroicons/react/20/solid';
import { useAuth } from '@/contexts/AuthContext';
import { useLocation } from 'react-router-dom';
import React from 'react';
import navigation from '@/utils/NavigationItems';
function classNames(...classes: string[]) {
return classes.filter(Boolean).join(' ');
}
// Example of how to dynamically build the sidebar
export default function Sidebar() {
const { logout } = useAuth();
const location = useLocation();
const currentPath = location.pathname;
return (
<div className="flex grow flex-col gap-y-5 overflow-y-auto border-r border-gray-200 bg-white px-6 py-1 h-screen fixed top-0 left-0 w-80 lg:w-64">
<a href={'/'} className="flex h-16 shrink-0 items-center">
<img alt="Your Company" src="/images/company_logo.png" className="h-auto w-full" />
</a>
<nav className="flex flex-1 flex-col">
<ul role="list" className="flex flex-1 flex-col gap-y-7">
<li>
<ul role="list" className="-mx-2 space-y-1">
{navigation.map((item) => (
<li key={item.name}>
{!item.children ? (
<a
href={item.href}
className={classNames(
currentPath === item.href ? 'bg-gray-100' : 'hover:bg-gray-50',
'group flex gap-x-3 rounded-md p-2 text-sm font-semibold leading-6 text-gray-700'
)}
>
<item.icon aria-hidden="true" className="h-6 w-6 shrink-0 text-gray-400" />
{item.name}
</a>
) : (
<Disclosure as="div">
<DisclosureButton
className={classNames(
currentPath.startsWith(item.href as string)
? 'bg-gray-100'
: 'hover:bg-gray-50',
'group flex w-full items-center gap-x-3 rounded-md p-2 text-left text-sm font-semibold leading-6 text-gray-700'
)}
>
<item.icon aria-hidden="true" className="h-6 w-6 shrink-0 text-gray-400" />
{item.name}
<ChevronRightIcon
aria-hidden="true"
className="ml-auto h-5 w-5 shrink-0 text-gray-400 group-data-[open]:rotate-90 group-data-[open]:text-gray-500"
/>
</DisclosureButton>
<DisclosurePanel as="ul" className="mt-1 px-2">
{item.children.map((subItem) => (
<li key={subItem.name}>
<DisclosureButton
as="a"
href={subItem.href}
className={classNames(
currentPath === subItem.href ? 'bg-gray-100' : 'hover:bg-gray-50',
'block rounded-md py-2 pl-9 pr-2 text-sm leading-6 text-gray-700'
)}
>
{subItem.name}
</DisclosureButton>
</li>
))}
</DisclosurePanel>
</Disclosure>
)}
</li>
))}
</ul>
</li>
<li className="mt-auto">
<button
className="inline-flex w-full items-center gap-x-2 rounded-md bg-[#667A8A] px-3.5 py-2.5 text-sm font-semibold text-white shadow-sm hover:bg-[#8093A4] focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-indigo-600 mb-4"
onClick={() => logout()}
>
<svg
xmlns="http://www.w3.org/2000/svg"
fill="none"
viewBox="0 0 24 24"
strokeWidth={2.5}
stroke="currentColor"
className="w-5 h-5"
>
<path
strokeLinecap="round"
strokeLinejoin="round"
d="M15.75 9V5.25A2.25 2.25 0 0013.5 3h-9a2.25 2.25 0 00-2.25 2.25v13.5A2.25 2.25 0 004.5 21h9a2.25 2.25 0 002.25-2.25V15M9 12h12m0 0l-3-3m3 3l-3 3"
/>
</svg>
Logout
</button>
</li>
</ul>
</nav>
</div>
);
}
I thought maybe the reason could be because i am using anchor tags in navigation instead of Link tags for navigation but even that didnt seem to work. This seems like an issue of unnecessary unmounting and remounting of WebSocketContext. Still not able to find the reason.