Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
57 changes: 31 additions & 26 deletions web/App.tsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import React from 'react';
import React, { Suspense } from 'react';
import { HashRouter, Navigate, Route, Routes } from 'react-router-dom';
import { Layout } from './components/layout/Layout';
import { ThemeWrapper } from './components/layout/ThemeWrapper';
Expand All @@ -8,21 +8,24 @@ import { ToastProvider } from './contexts/ToastContext';
import { ConfirmProvider } from './contexts/ConfirmContext';
import { ToastContainer } from './components/ui/Toast';
import { ErrorBoundary } from './components/ErrorBoundary';
import { Auth } from './pages/Auth';
import { Dashboard } from './pages/Dashboard';
import { Friends } from './pages/Friends';
import { GroupDetails } from './pages/GroupDetails';
import { Groups } from './pages/Groups';
import { Profile } from './pages/Profile';
import { SplitwiseCallback } from './pages/SplitwiseCallback';
import { SplitwiseGroupSelection } from './pages/SplitwiseGroupSelection';
import { SplitwiseImport } from './pages/SplitwiseImport';
import { PageLoader } from './components/ui/PageLoader';

// Lazy load pages
const Auth = React.lazy(() => import('./pages/Auth').then(module => ({ default: module.Auth })));
const Dashboard = React.lazy(() => import('./pages/Dashboard').then(module => ({ default: module.Dashboard })));
const Friends = React.lazy(() => import('./pages/Friends').then(module => ({ default: module.Friends })));
const GroupDetails = React.lazy(() => import('./pages/GroupDetails').then(module => ({ default: module.GroupDetails })));
const Groups = React.lazy(() => import('./pages/Groups').then(module => ({ default: module.Groups })));
const Profile = React.lazy(() => import('./pages/Profile').then(module => ({ default: module.Profile })));
const SplitwiseCallback = React.lazy(() => import('./pages/SplitwiseCallback').then(module => ({ default: module.SplitwiseCallback })));
const SplitwiseGroupSelection = React.lazy(() => import('./pages/SplitwiseGroupSelection').then(module => ({ default: module.SplitwiseGroupSelection })));
const SplitwiseImport = React.lazy(() => import('./pages/SplitwiseImport').then(module => ({ default: module.SplitwiseImport })));

// Protected Route Wrapper
const ProtectedRoute = ({ children }: { children: React.ReactElement }) => {
const { isAuthenticated, isLoading } = useAuth();

if (isLoading) return <div className="h-screen w-full flex items-center justify-center">Loading...</div>;
if (isLoading) return <PageLoader />;

if (!isAuthenticated) {
return <Navigate to="/login" replace />;
Expand All @@ -35,21 +38,23 @@ const AppRoutes = () => {
const { isAuthenticated } = useAuth();

return (
<Routes>
<Route path="/login" element={!isAuthenticated ? <ThemeWrapper><Auth /></ThemeWrapper> : <Navigate to="/dashboard" />} />
<Route path="/signup" element={!isAuthenticated ? <ThemeWrapper><Auth /></ThemeWrapper> : <Navigate to="/dashboard" />} />

<Route path="/dashboard" element={<ProtectedRoute><Dashboard /></ProtectedRoute>} />
<Route path="/groups" element={<ProtectedRoute><Groups /></ProtectedRoute>} />
<Route path="/groups/:id" element={<ProtectedRoute><GroupDetails /></ProtectedRoute>} />
<Route path="/friends" element={<ProtectedRoute><Friends /></ProtectedRoute>} />
<Route path="/profile" element={<ProtectedRoute><Profile /></ProtectedRoute>} />
<Route path="/import/splitwise" element={<ProtectedRoute><SplitwiseImport /></ProtectedRoute>} />
<Route path="/import/splitwise/select-groups" element={<ProtectedRoute><SplitwiseGroupSelection /></ProtectedRoute>} />
<Route path="/import/splitwise/callback" element={<ProtectedRoute><SplitwiseCallback /></ProtectedRoute>} />

<Route path="*" element={<Navigate to={isAuthenticated ? "/dashboard" : "/login"} />} />
</Routes>
<Suspense fallback={<PageLoader />}>
<Routes>
<Route path="/login" element={!isAuthenticated ? <ThemeWrapper><Auth /></ThemeWrapper> : <Navigate to="/dashboard" />} />
<Route path="/signup" element={!isAuthenticated ? <ThemeWrapper><Auth /></ThemeWrapper> : <Navigate to="/dashboard" />} />

<Route path="/dashboard" element={<ProtectedRoute><Dashboard /></ProtectedRoute>} />
<Route path="/groups" element={<ProtectedRoute><Groups /></ProtectedRoute>} />
<Route path="/groups/:id" element={<ProtectedRoute><GroupDetails /></ProtectedRoute>} />
<Route path="/friends" element={<ProtectedRoute><Friends /></ProtectedRoute>} />
<Route path="/profile" element={<ProtectedRoute><Profile /></ProtectedRoute>} />
<Route path="/import/splitwise" element={<ProtectedRoute><SplitwiseImport /></ProtectedRoute>} />
<Route path="/import/splitwise/select-groups" element={<ProtectedRoute><SplitwiseGroupSelection /></ProtectedRoute>} />
<Route path="/import/splitwise/callback" element={<ProtectedRoute><SplitwiseCallback /></ProtectedRoute>} />

<Route path="*" element={<Navigate to={isAuthenticated ? "/dashboard" : "/login"} />} />
</Routes>
</Suspense>
Comment on lines +41 to +57
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🧹 Nitpick | 🔵 Trivial

Single Suspense boundary will flash the loader on every route transition.

With a single <Suspense> wrapping all <Routes>, navigating between two already-loaded routes is fine, but navigating to a not-yet-loaded route will unmount the current page and show the full-screen <PageLoader /> until the chunk arrives. This can feel jarring compared to keeping the previous page visible while the next one loads.

Two mitigation options (non-blocking, can be done as follow-up):

  1. Use startTransition (or react-router's built-in transition support) so React keeps the old UI visible during the lazy load, showing the spinner only if loading takes too long.
  2. Per-route Suspense boundaries inside <Layout> so the shell (nav, sidebar) stays rendered while only the content area shows a loader.

Neither is required for this PR to ship—the current behavior is correct and a clear improvement over no code splitting.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@web/App.tsx` around lines 41 - 57, The Suspense wrapping the entire Routes
(Suspense, Routes, PageLoader) causes full-page loader flashes on route changes;
to fix, move the Suspense boundaries closer to lazy-loaded content (e.g., wrap
each lazy route element or the content Outlet inside your Layout with its own
Suspense) so the app shell (nav/sidebar) stays mounted, and/or use
React.startTransition (or react-router transition APIs) in navigation logic to
keep the previous UI visible while lazy chunks load; update usages around
Suspense, PageLoader, Routes, Layout, ProtectedRoute, and each lazy component
(Dashboard, Groups, GroupDetails, etc.) accordingly.

);
}

Expand Down
10 changes: 10 additions & 0 deletions web/components/ui/PageLoader.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
import React from 'react';
import { Spinner } from './Spinner';

export const PageLoader = () => {
return (
<div className="h-screen w-full flex items-center justify-center">
<Spinner size={48} className="text-emerald-500 dark:text-emerald-400" />
</div>
);
};
Loading