Scope Suspense to content area with skeleton fallback

Moved the Suspense boundary from App.tsx (which wrapped the entire app)
into MainLayout around just the Outlet and right sidebar. The left
sidebar, mobile nav, and mobile top bar now persist while lazy page
chunks load. A skeleton fallback fills the content area during loading.
This commit is contained in:
Alex Gleason
2026-02-21 13:37:09 -06:00
parent 3daf38c9fd
commit 5b49ea8ea4
2 changed files with 59 additions and 10 deletions
+2 -4
View File
@@ -4,7 +4,7 @@
import { QueryClient, QueryClientProvider } from "@tanstack/react-query";
import { createHead, UnheadProvider } from '@unhead/react/client';
import { InferSeoMetaPlugin } from '@unhead/addons';
import { Suspense, useEffect } from 'react';
import { useEffect } from 'react';
import NostrProvider from '@/components/NostrProvider';
import { NostrSync } from '@/components/NostrSync';
import { NativeNotifications } from '@/components/NativeNotifications';
@@ -99,9 +99,7 @@ export function App() {
<TooltipProvider>
<Toaster />
<InitialSyncGate>
<Suspense>
<AppRouter />
</Suspense>
<AppRouter />
</InitialSyncGate>
</TooltipProvider>
</NWCProvider>
+57 -6
View File
@@ -1,4 +1,4 @@
import { useState, useMemo } from 'react';
import { Suspense, useState, useMemo } from 'react';
import { Outlet } from 'react-router-dom';
import { LeftSidebar } from '@/components/LeftSidebar';
import { RightSidebar } from '@/components/RightSidebar';
@@ -6,9 +6,60 @@ import { MobileTopBar } from '@/components/MobileTopBar';
import { MobileBottomNav } from '@/components/MobileBottomNav';
import { MobileDrawer } from '@/components/MobileDrawer';
import { FloatingComposeButton } from '@/components/FloatingComposeButton';
import { Skeleton } from '@/components/ui/skeleton';
import { LayoutStore, LayoutStoreContext, useLayoutSnapshot } from '@/contexts/LayoutContext';
import { cn } from '@/lib/utils';
/** Skeleton shown in the content area while a lazy page chunk is loading. */
function PageSkeleton() {
return (
<>
{/* Main column skeleton */}
<main className="flex-1 min-w-0 sidebar:max-w-[600px] sidebar:border-l xl:border-r border-border min-h-screen">
{/* Header skeleton */}
<div className="flex items-center gap-4 px-4 mt-4 mb-5">
<Skeleton className="h-6 w-32" />
</div>
{/* Content skeletons */}
<div className="space-y-4 px-4">
{Array.from({ length: 4 }).map((_, i) => (
<div key={i} className="space-y-3 py-4 border-b border-border">
<div className="flex items-center gap-3">
<Skeleton className="h-10 w-10 rounded-full" />
<div className="space-y-1.5">
<Skeleton className="h-4 w-28" />
<Skeleton className="h-3 w-20" />
</div>
</div>
<div className="space-y-2">
<Skeleton className="h-4 w-full" />
<Skeleton className="h-4 w-4/5" />
</div>
</div>
))}
</div>
</main>
{/* Right sidebar skeleton */}
<aside className="w-[300px] shrink-0 hidden xl:flex flex-col sticky top-0 h-screen pt-5 pb-3 px-5">
<div className="space-y-6">
<div className="space-y-3">
<Skeleton className="h-6 w-24" />
{Array.from({ length: 4 }).map((_, i) => (
<div key={i} className="flex justify-between items-center">
<div className="space-y-1.5">
<Skeleton className="h-4 w-24" />
<Skeleton className="h-3 w-32" />
</div>
<Skeleton className="h-8 w-12" />
</div>
))}
</div>
</div>
</aside>
</>
);
}
/** Inner component that reads layout options from the context store. */
function MainLayoutInner() {
const { rightSidebar, showFAB = false, noBottomSpacer = false, wrapperClassName } = useLayoutSnapshot();
@@ -29,11 +80,11 @@ function MainLayoutInner() {
<LeftSidebar />
</div>
{/* Main content area - swapped by the router */}
<Outlet />
{/* Desktop right sidebar - handled internally with hidden lg:block */}
{rightSidebar ?? <RightSidebar />}
{/* Main content + right sidebar: inside Suspense so the left sidebar persists while lazy pages load */}
<Suspense fallback={<PageSkeleton />}>
<Outlet />
{rightSidebar ?? <RightSidebar />}
</Suspense>
</div>
{/* Mobile bottom nav - only on small screens */}