diff --git a/worklenz-frontend/src/App.tsx b/worklenz-frontend/src/App.tsx
index 00e53090..1116b739 100644
--- a/worklenz-frontend/src/App.tsx
+++ b/worklenz-frontend/src/App.tsx
@@ -5,7 +5,6 @@ import i18next from 'i18next';
// Components
import ThemeWrapper from './features/theme/ThemeWrapper';
-import PreferenceSelector from './components/PreferenceSelector';
// Routes
import router from './app/routes';
@@ -14,7 +13,6 @@ import router from './app/routes';
import { useAppSelector } from './hooks/useAppSelector';
import { initMixpanel } from './utils/mixpanelInit';
import { initializeCsrfToken } from './api/api-client';
-import { useRoutePreloader } from './utils/routePreloader';
// Types & Constants
import { Language } from './features/i18n/localesSlice';
@@ -28,9 +26,9 @@ import { SuspenseFallback } from './components/suspense-fallback/suspense-fallba
* 1. React.memo() - Prevents unnecessary re-renders
* 2. useMemo() - Memoizes expensive computations
* 3. useCallback() - Memoizes event handlers
- * 4. Route preloading - Preloads critical routes
- * 5. Lazy loading - Components loaded on demand
- * 6. Suspense boundaries - Better loading states
+ * 4. Lazy loading - All route components loaded on demand
+ * 5. Suspense boundaries - Better loading states
+ * 6. Optimized guard components with memoization
*/
const App: React.FC = memo(() => {
const themeMode = useAppSelector(state => state.themeReducer.mode);
@@ -39,25 +37,6 @@ const App: React.FC = memo(() => {
// Memoize mixpanel initialization to prevent re-initialization
const mixpanelToken = useMemo(() => import.meta.env.VITE_MIXPANEL_TOKEN as string, []);
- // Preload critical routes for better navigation performance
- useRoutePreloader([
- {
- path: '/worklenz/home',
- loader: () => import('./pages/home/home-page'),
- priority: 'high'
- },
- {
- path: '/worklenz/projects',
- loader: () => import('./pages/projects/project-list'),
- priority: 'high'
- },
- {
- path: '/worklenz/schedule',
- loader: () => import('./pages/schedule/schedule'),
- priority: 'medium'
- }
- ]);
-
useEffect(() => {
initMixpanel(mixpanelToken);
}, [mixpanelToken]);
@@ -95,7 +74,12 @@ const App: React.FC = memo(() => {
return (
}>
-
+
);
diff --git a/worklenz-frontend/src/app/routes/auth-routes.tsx b/worklenz-frontend/src/app/routes/auth-routes.tsx
index 2eca96a9..5cddb925 100644
--- a/worklenz-frontend/src/app/routes/auth-routes.tsx
+++ b/worklenz-frontend/src/app/routes/auth-routes.tsx
@@ -1,20 +1,20 @@
+import { lazy, Suspense } from 'react';
import AuthLayout from '@/layouts/AuthLayout';
-import LoginPage from '@/pages/auth/login-page';
-import SignupPage from '@/pages/auth/signup-page';
-import ForgotPasswordPage from '@/pages/auth/forgot-password-page';
-import LoggingOutPage from '@/pages/auth/logging-out';
-import AuthenticatingPage from '@/pages/auth/authenticating';
import { Navigate } from 'react-router-dom';
-import VerifyResetEmailPage from '@/pages/auth/verify-reset-email';
-import { Suspense } from 'react';
import { SuspenseFallback } from '@/components/suspense-fallback/suspense-fallback';
+// Lazy load auth page components for better code splitting
+const LoginPage = lazy(() => import('@/pages/auth/login-page'));
+const SignupPage = lazy(() => import('@/pages/auth/signup-page'));
+const ForgotPasswordPage = lazy(() => import('@/pages/auth/forgot-password-page'));
+const LoggingOutPage = lazy(() => import('@/pages/auth/logging-out'));
+const AuthenticatingPage = lazy(() => import('@/pages/auth/authenticating'));
+const VerifyResetEmailPage = lazy(() => import('@/pages/auth/verify-reset-email'));
+
const authRoutes = [
{
path: '/auth',
- element: (
-
- ),
+ element: ,
children: [
{
path: '',
@@ -22,27 +22,51 @@ const authRoutes = [
},
{
path: 'login',
- element: ,
+ element: (
+ }>
+
+
+ ),
},
{
path: 'signup',
- element: ,
+ element: (
+ }>
+
+
+ ),
},
{
path: 'forgot-password',
- element: ,
+ element: (
+ }>
+
+
+ ),
},
{
path: 'logging-out',
- element: ,
+ element: (
+ }>
+
+
+ ),
},
{
path: 'authenticating',
- element: ,
+ element: (
+ }>
+
+
+ ),
},
{
path: 'verify-reset-email/:user/:hash',
- element: ,
+ element: (
+ }>
+
+
+ ),
},
],
},
diff --git a/worklenz-frontend/src/app/routes/index.tsx b/worklenz-frontend/src/app/routes/index.tsx
index 7d6d6826..d9361804 100644
--- a/worklenz-frontend/src/app/routes/index.tsx
+++ b/worklenz-frontend/src/app/routes/index.tsx
@@ -1,4 +1,5 @@
import { createBrowserRouter, Navigate, RouteObject, useLocation } from 'react-router-dom';
+import { lazy, Suspense, memo, useMemo } from 'react';
import rootRoutes from './root-routes';
import authRoutes from './auth-routes';
import mainRoutes, { licenseExpiredRoute } from './main-routes';
@@ -8,116 +9,195 @@ import reportingRoutes from './reporting-routes';
import { useAuthService } from '@/hooks/useAuth';
import { AuthenticatedLayout } from '@/layouts/AuthenticatedLayout';
import ErrorBoundary from '@/components/ErrorBoundary';
-import NotFoundPage from '@/pages/404-page/404-page';
+import { SuspenseFallback } from '@/components/suspense-fallback/suspense-fallback';
import { ISUBSCRIPTION_TYPE } from '@/shared/constants';
-import LicenseExpired from '@/pages/license-expired/license-expired';
+
+// Lazy load the NotFoundPage component for better code splitting
+const NotFoundPage = lazy(() => import('@/pages/404-page/404-page'));
interface GuardProps {
children: React.ReactNode;
}
-export const AuthGuard = ({ children }: GuardProps) => {
- const isAuthenticated = useAuthService().isAuthenticated();
- const location = useLocation();
-
- if (!isAuthenticated) {
- return ;
- }
-
- return <>{children}>;
+// Route-based code splitting utility
+const withCodeSplitting = (Component: React.LazyExoticComponent>) => {
+ return memo(() => (
+ }>
+
+
+ ));
};
-export const AdminGuard = ({ children }: GuardProps) => {
- const isAuthenticated = useAuthService().isAuthenticated();
- const isOwnerOrAdmin = useAuthService().isOwnerOrAdmin();
- const currentSession = useAuthService().getCurrentSession();
- const isFreePlan = currentSession?.subscription_type === ISUBSCRIPTION_TYPE.FREE;
+// Memoized guard components with defensive programming
+export const AuthGuard = memo(({ children }: GuardProps) => {
+ const authService = useAuthService();
const location = useLocation();
- if (!isAuthenticated) {
- return ;
- }
-
- if (!isOwnerOrAdmin || isFreePlan) {
- return ;
- }
-
- return <>{children}>;
-};
-
-export const LicenseExpiryGuard = ({ children }: GuardProps) => {
- const isAuthenticated = useAuthService().isAuthenticated();
- const currentSession = useAuthService().getCurrentSession();
- const location = useLocation();
- const isAdminCenterRoute = location.pathname.includes('/worklenz/admin-center');
- const isLicenseExpiredRoute = location.pathname === '/worklenz/license-expired';
-
- // Don't check or redirect if we're already on the license-expired page
- if (isLicenseExpiredRoute) {
- return <>{children}>;
- }
-
- // Check if trial is expired more than 7 days or if is_expired flag is set
- const isLicenseExpiredMoreThan7Days = () => {
- // Quick bail if no session data is available
- if (!currentSession) {
- return false;
- }
-
- // Check is_expired flag first
- if (currentSession.is_expired) {
- // If no trial_expire_date exists but is_expired is true, defer to backend check
- if (!currentSession.trial_expire_date) {
- return true;
+ const shouldRedirect = useMemo(() => {
+ try {
+ // Defensive check to ensure authService and its methods exist
+ if (!authService || typeof authService.isAuthenticated !== 'function') {
+ return false; // Don't redirect if auth service is not ready
}
-
- // If there is a trial_expire_date, check if it's more than 7 days past
- const today = new Date();
- const expiryDate = new Date(currentSession.trial_expire_date);
- const diffTime = today.getTime() - expiryDate.getTime();
- const diffDays = Math.ceil(diffTime / (1000 * 60 * 60 * 24));
-
- // Redirect if more than 7 days past expiration
- return diffDays > 7;
+ return !authService.isAuthenticated();
+ } catch (error) {
+ console.error('Error in AuthGuard:', error);
+ return false; // Don't redirect on error, let the app handle it
}
-
- // If not marked as expired but has trial_expire_date, do a date check
- if (currentSession.subscription_type === ISUBSCRIPTION_TYPE.TRIAL && currentSession.trial_expire_date) {
- const today = new Date();
- const expiryDate = new Date(currentSession.trial_expire_date);
+ }, [authService]);
- const diffTime = today.getTime() - expiryDate.getTime();
- const diffDays = Math.ceil(diffTime / (1000 * 60 * 60 * 24));
+ if (shouldRedirect) {
+ return ;
+ }
+
+ return <>{children}>;
+});
+
+AuthGuard.displayName = 'AuthGuard';
+
+export const AdminGuard = memo(({ children }: GuardProps) => {
+ const authService = useAuthService();
+ const location = useLocation();
+
+ const guardResult = useMemo(() => {
+ try {
+ // Defensive checks to ensure authService and its methods exist
+ if (!authService ||
+ typeof authService.isAuthenticated !== 'function' ||
+ typeof authService.isOwnerOrAdmin !== 'function' ||
+ typeof authService.getCurrentSession !== 'function') {
+ return null; // Don't redirect if auth service is not ready
+ }
+
+ if (!authService.isAuthenticated()) {
+ return { redirect: '/auth', state: { from: location } };
+ }
+
+ const currentSession = authService.getCurrentSession();
+ const isFreePlan = currentSession?.subscription_type === ISUBSCRIPTION_TYPE.FREE;
- // If expired more than 7 days, redirect
- return diffDays > 7;
- }
-
- // No expiration data found
- return false;
- };
+ if (!authService.isOwnerOrAdmin() || isFreePlan) {
+ return { redirect: '/worklenz/unauthorized' };
+ }
+
+ return null;
+ } catch (error) {
+ console.error('Error in AdminGuard:', error);
+ return null; // Don't redirect on error
+ }
+ }, [authService, location]);
+
+ if (guardResult) {
+ return ;
+ }
+
+ return <>{children}>;
+});
+
+AdminGuard.displayName = 'AdminGuard';
+
+export const LicenseExpiryGuard = memo(({ children }: GuardProps) => {
+ const authService = useAuthService();
+ const location = useLocation();
+
+ const shouldRedirect = useMemo(() => {
+ try {
+ // Defensive checks to ensure authService and its methods exist
+ if (!authService ||
+ typeof authService.isAuthenticated !== 'function' ||
+ typeof authService.getCurrentSession !== 'function') {
+ return false; // Don't redirect if auth service is not ready
+ }
+
+ if (!authService.isAuthenticated()) return false;
+
+ const isAdminCenterRoute = location.pathname.includes('/worklenz/admin-center');
+ const isLicenseExpiredRoute = location.pathname === '/worklenz/license-expired';
+
+ // Don't check or redirect if we're already on the license-expired page
+ if (isLicenseExpiredRoute) return false;
+
+ const currentSession = authService.getCurrentSession();
+
+ // Check if trial is expired more than 7 days or if is_expired flag is set
+ const isLicenseExpiredMoreThan7Days = () => {
+ // Quick bail if no session data is available
+ if (!currentSession) return false;
+
+ // Check is_expired flag first
+ if (currentSession.is_expired) {
+ // If no trial_expire_date exists but is_expired is true, defer to backend check
+ if (!currentSession.trial_expire_date) return true;
+
+ // If there is a trial_expire_date, check if it's more than 7 days past
+ const today = new Date();
+ const expiryDate = new Date(currentSession.trial_expire_date);
+ const diffTime = today.getTime() - expiryDate.getTime();
+ const diffDays = Math.ceil(diffTime / (1000 * 60 * 60 * 24));
+
+ // Redirect if more than 7 days past expiration
+ return diffDays > 7;
+ }
+
+ // If not marked as expired but has trial_expire_date, do a date check
+ if (currentSession.subscription_type === ISUBSCRIPTION_TYPE.TRIAL && currentSession.trial_expire_date) {
+ const today = new Date();
+ const expiryDate = new Date(currentSession.trial_expire_date);
+
+ const diffTime = today.getTime() - expiryDate.getTime();
+ const diffDays = Math.ceil(diffTime / (1000 * 60 * 60 * 24));
+
+ // If expired more than 7 days, redirect
+ return diffDays > 7;
+ }
+
+ // No expiration data found
+ return false;
+ };
+
+ return isLicenseExpiredMoreThan7Days() && !isAdminCenterRoute;
+ } catch (error) {
+ console.error('Error in LicenseExpiryGuard:', error);
+ return false; // Don't redirect on error
+ }
+ }, [authService, location.pathname]);
- // Add this explicit check and log the result
- const shouldRedirect = isAuthenticated && isLicenseExpiredMoreThan7Days() && !isAdminCenterRoute;
if (shouldRedirect) {
return ;
}
return <>{children}>;
-};
+});
-export const SetupGuard = ({ children }: GuardProps) => {
- const isAuthenticated = useAuthService().isAuthenticated();
+LicenseExpiryGuard.displayName = 'LicenseExpiryGuard';
+
+export const SetupGuard = memo(({ children }: GuardProps) => {
+ const authService = useAuthService();
const location = useLocation();
- if (!isAuthenticated) {
+ const shouldRedirect = useMemo(() => {
+ try {
+ // Defensive check to ensure authService and its methods exist
+ if (!authService || typeof authService.isAuthenticated !== 'function') {
+ return false; // Don't redirect if auth service is not ready
+ }
+ return !authService.isAuthenticated();
+ } catch (error) {
+ console.error('Error in SetupGuard:', error);
+ return false; // Don't redirect on error
+ }
+ }, [authService]);
+
+ if (shouldRedirect) {
return ;
}
return <>{children}>;
-};
+});
-// Helper to wrap routes with guards
+SetupGuard.displayName = 'SetupGuard';
+
+// Optimized route wrapping function with Suspense boundaries
const wrapRoutes = (
routes: RouteObject[],
Guard: React.ComponentType<{ children: React.ReactNode }>
@@ -125,7 +205,11 @@ const wrapRoutes = (
return routes.map(route => {
const wrappedRoute = {
...route,
- element: {route.element},
+ element: (
+ }>
+ {route.element}
+
+ ),
};
if (route.children) {
@@ -140,9 +224,8 @@ const wrapRoutes = (
});
};
-// Static license expired component that doesn't rely on translations or authentication
-const StaticLicenseExpired = () => {
-
+// Optimized static license expired component
+const StaticLicenseExpired = memo(() => {
return (
{
);
-};
+});
+StaticLicenseExpired.displayName = 'StaticLicenseExpired';
+
+// Create route arrays (moved outside of useMemo to avoid hook violations)
const publicRoutes = [
...rootRoutes,
...authRoutes,
notFoundRoute
];
+
const protectedMainRoutes = wrapRoutes(mainRoutes, AuthGuard);
const adminRoutes = wrapRoutes(reportingRoutes, AdminGuard);
const setupRoutes = wrapRoutes([accountSetupRoute], SetupGuard);
-// Apply LicenseExpiryGuard to all protected routes
+// License expiry check function
const withLicenseExpiryCheck = (routes: RouteObject[]): RouteObject[] => {
return routes.map(route => {
const wrappedRoute = {
...route,
- element: {route.element},
+ element: (
+ }>
+ {route.element}
+
+ ),
};
if (route.children) {
@@ -213,10 +304,21 @@ const withLicenseExpiryCheck = (routes: RouteObject[]): RouteObject[] => {
const licenseCheckedMainRoutes = withLicenseExpiryCheck(protectedMainRoutes);
+// Create optimized router with future flags for better performance
const router = createBrowserRouter([
{
- element: ,
- errorElement: ,
+ element: (
+
+
+
+ ),
+ errorElement: (
+
+ }>
+
+
+
+ ),
children: [
...licenseCheckedMainRoutes,
...adminRoutes,
@@ -225,6 +327,15 @@ const router = createBrowserRouter([
],
},
...publicRoutes,
-]);
+], {
+ // Enable React Router future features for better performance
+ future: {
+ v7_relativeSplatPath: true,
+ v7_fetcherPersist: true,
+ v7_normalizeFormMethod: true,
+ v7_partialHydration: true,
+ v7_skipActionErrorRevalidation: true
+ }
+});
export default router;
diff --git a/worklenz-frontend/src/app/routes/main-routes.tsx b/worklenz-frontend/src/app/routes/main-routes.tsx
index 8647fac2..225fd9a7 100644
--- a/worklenz-frontend/src/app/routes/main-routes.tsx
+++ b/worklenz-frontend/src/app/routes/main-routes.tsx
@@ -1,32 +1,49 @@
import { RouteObject } from 'react-router-dom';
+import { lazy, Suspense } from 'react';
import MainLayout from '@/layouts/MainLayout';
-import HomePage from '@/pages/home/home-page';
-import ProjectList from '@/pages/projects/project-list';
import settingsRoutes from './settings-routes';
import adminCenterRoutes from './admin-center-routes';
-import Schedule from '@/pages/schedule/schedule';
-import ProjectTemplateEditView from '@/pages/settings/project-templates/projectTemplateEditView/ProjectTemplateEditView';
-import LicenseExpired from '@/pages/license-expired/license-expired';
-import ProjectView from '@/pages/projects/projectView/project-view';
-import Unauthorized from '@/pages/unauthorized/unauthorized';
import { useAuthService } from '@/hooks/useAuth';
import { Navigate, useLocation } from 'react-router-dom';
+import { SuspenseFallback } from '@/components/suspense-fallback/suspense-fallback';
-// Define AdminGuard component first
+// Lazy load page components for better code splitting
+const HomePage = lazy(() => import('@/pages/home/home-page'));
+const ProjectList = lazy(() => import('@/pages/projects/project-list'));
+const Schedule = lazy(() => import('@/pages/schedule/schedule'));
+const ProjectTemplateEditView = lazy(() => import('@/pages/settings/project-templates/projectTemplateEditView/ProjectTemplateEditView'));
+const LicenseExpired = lazy(() => import('@/pages/license-expired/license-expired'));
+const ProjectView = lazy(() => import('@/pages/projects/projectView/project-view'));
+const Unauthorized = lazy(() => import('@/pages/unauthorized/unauthorized'));
+
+// Define AdminGuard component with defensive programming
const AdminGuard = ({ children }: { children: React.ReactNode }) => {
- const isAuthenticated = useAuthService().isAuthenticated();
- const isOwnerOrAdmin = useAuthService().isOwnerOrAdmin();
+ const authService = useAuthService();
const location = useLocation();
- if (!isAuthenticated) {
- return ;
- }
+ try {
+ // Defensive checks to ensure authService and its methods exist
+ if (!authService ||
+ typeof authService.isAuthenticated !== 'function' ||
+ typeof authService.isOwnerOrAdmin !== 'function') {
+ // If auth service is not ready, render children (don't block)
+ return <>{children}>;
+ }
- if (!isOwnerOrAdmin) {
- return ;
- }
+ if (!authService.isAuthenticated()) {
+ return ;
+ }
- return <>{children}>;
+ if (!authService.isOwnerOrAdmin()) {
+ return ;
+ }
+
+ return <>{children}>;
+ } catch (error) {
+ console.error('Error in AdminGuard (main-routes):', error);
+ // On error, render children to prevent complete blocking
+ return <>{children}>;
+ }
};
const mainRoutes: RouteObject[] = [
@@ -35,18 +52,56 @@ const mainRoutes: RouteObject[] = [
element: ,
children: [
{ index: true, element: },
- { path: 'home', element: },
- { path: 'projects', element: },
+ {
+ path: 'home',
+ element: (
+ }>
+
+
+ )
+ },
+ {
+ path: 'projects',
+ element: (
+ }>
+
+
+ )
+ },
{
path: 'schedule',
- element:
+ element: (
+ }>
+
+
+
+
+ )
+ },
+ {
+ path: `projects/:projectId`,
+ element: (
+ }>
+
+
+ )
},
- { path: `projects/:projectId`, element: },
{
path: `settings/project-templates/edit/:templateId/:templateName`,
- element: ,
+ element: (
+ }>
+
+
+ ),
+ },
+ {
+ path: 'unauthorized',
+ element: (
+ }>
+
+
+ )
},
- { path: 'unauthorized', element: },
...settingsRoutes,
...adminCenterRoutes,
],
@@ -58,7 +113,14 @@ export const licenseExpiredRoute: RouteObject = {
path: '/worklenz',
element: ,
children: [
- { path: 'license-expired', element: }
+ {
+ path: 'license-expired',
+ element: (
+ }>
+
+
+ )
+ }
]
};