When your app depends on multiple async providers (Google Maps, session, etc.), use a single LoadingGuard to show a loading state until all of them are ready.
// components/templates/LoadingGuard.tsx
'use client';
import { useSession } from 'next-auth/react';
import { useApiLoadingStatus } from '@vis.gl/react-google-maps';
export function LoadingGuard({ children }: { children: React.ReactNode }) {
const { status: sessionStatus } = useSession();
const mapsStatus = useApiLoadingStatus();
const isLoading = sessionStatus === 'loading' || mapsStatus !== 'LOADED';
if (isLoading) {
return <LoadingIndicator />;
}
return <>{children}</>;
}
The guard checks both SessionProvider and APIProvider loading states. Children only render when everything is ready.
Place the guard inside the providers it checks:
// app/layout.tsx
'use client';
import { SessionProvider } from 'next-auth/react';
import { APIProvider } from '@vis.gl/react-google-maps';
import { LoadingGuard } from 'components/templates/LoadingGuard';
export default function Layout({ children }) {
return (
<APIProvider apiKey={process.env.NEXT_PUBLIC_GOOGLE_MAPS_API_KEY!}>
<SessionProvider>
<LoadingGuard>{children}</LoadingGuard>
</SessionProvider>
</APIProvider>
);
}
The guard must be inside APIProvider and SessionProvider to access their hooks.
You can combine a provider and its loading guard into a single component. This guarantees that children always have access to loaded data, eliminating null checks.
// auth/SessionLoadingGuard.tsx
import { SessionProvider } from 'next-auth/react';
import { usePermissions } from '@app-admin/auth/usePermissions';
import { LoadingCircular } from '@repo/ui/LoadingCircular';
export const SessionLoadingGuard = ({ children }: { children: React.ReactNode }) => {
return (
<SessionProvider>
<ChildrenWrapper>{children}</ChildrenWrapper>
</SessionProvider>
);
};
const ChildrenWrapper = ({ children }: { children: React.ReactNode }) => {
const { isLoading } = usePermissions();
if (isLoading) {
return <LoadingCircular />;
}
return <>{children}</>;
};
Now any component using useSession or usePermissions inside SessionLoadingGuard can assume the session is loaded:
// Before: null checks everywhere
const { data: session } = useSession();
if (!session) return null;
const userId = session.user.id;
// After: session is always available
const { data: session } = useSession();
const userId = session!.user.id;
This pattern moves the loading logic to the boundary, keeping child components simple.