Skip to content

Commit e50a3fd

Browse files
fix: Fetch the session on the client to not slow down the server response
1 parent a7f27fb commit e50a3fd

File tree

5 files changed

+99
-22
lines changed

5 files changed

+99
-22
lines changed

src/app/api/get-session/route.ts

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
import { getSessionFromCookie } from "@/utils/auth"
2+
import { NextResponse } from "next/server"
3+
import { tryCatch } from "@/lib/try-catch"
4+
5+
export async function GET() {
6+
const { data, error } = await tryCatch(getSessionFromCookie())
7+
8+
const headers = new Headers()
9+
headers.set("Cache-Control", "no-store, no-cache, must-revalidate, proxy-revalidate, max-age=0")
10+
headers.set("Pragma", "no-cache")
11+
headers.set("Expires", "0")
12+
13+
if (error) {
14+
return NextResponse.json(null, {
15+
headers
16+
})
17+
}
18+
19+
return NextResponse.json(data, {
20+
headers
21+
})
22+
}

src/app/layout.tsx

Lines changed: 9 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -10,8 +10,8 @@ import { TooltipProvider } from "@/components/ui/tooltip";
1010
import NextTopLoader from 'nextjs-toploader'
1111
import { SITE_NAME, SITE_DESCRIPTION, SITE_URL } from "@/constants";
1212
import { StartupStudioStickyBanner } from "@/components/startup-studio-sticky-banner";
13-
import { getSessionFromCookie } from "@/utils/auth";
1413
import { getConfig } from "@/flags";
14+
import { Spinner } from '@/components/ui/spinner';
1515

1616
const inter = Inter({ subsets: ["latin"] });
1717

@@ -53,17 +53,15 @@ export const metadata: Metadata = {
5353
};
5454

5555
// This component will be wrapped in Suspense in the BaseLayout
56-
async function SessionProvider({ children }: { children: React.ReactNode }) {
56+
async function Providers({ children }: { children: React.ReactNode }) {
5757
// These async operations will be handled by Suspense in the parent component
58-
const session = await getSessionFromCookie();
5958
const config = await getConfig();
6059

6160
return (
6261
<ThemeProvider
6362
attribute="class"
6463
defaultTheme="system"
6564
enableSystem
66-
session={session}
6765
config={config}
6866
>
6967
<TooltipProvider
@@ -89,10 +87,10 @@ export default function BaseLayout({
8987
shadow="0 0 10px #000, 0 0 5px #000"
9088
height={4}
9189
/>
92-
<Suspense fallback={<ThemeProviderFallback>{children}</ThemeProviderFallback>}>
93-
<SessionProvider>
90+
<Suspense fallback={<ProvidersFallback />}>
91+
<Providers>
9492
{children}
95-
</SessionProvider>
93+
</Providers>
9694
</Suspense>
9795
<Toaster richColors closeButton position="top-right" expand duration={7000} />
9896
<StartupStudioStickyBanner />
@@ -101,21 +99,17 @@ export default function BaseLayout({
10199
);
102100
}
103101

104-
function ThemeProviderFallback({ children }: { children: React.ReactNode }) {
102+
function ProvidersFallback() {
105103
return (
106104
<ThemeProvider
107105
attribute="class"
108106
defaultTheme="system"
109107
enableSystem
110-
session={null}
111108
config={{ isGoogleSSOEnabled: false, isTurnstileEnabled: false }}
112109
>
113-
<TooltipProvider
114-
delayDuration={100}
115-
skipDelayDuration={50}
116-
>
117-
{children}
118-
</TooltipProvider>
110+
<div className="min-h-screen bg-background flex items-center justify-center">
111+
<Spinner size="large" />
112+
</div>
119113
</ThemeProvider>
120114
);
121115
}

src/components/providers.tsx

Lines changed: 41 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -5,15 +5,16 @@ import { ThemeProvider as NextThemesProvider } from "next-themes"
55
import { HeroUIProvider } from "@heroui/react"
66
import type { SessionValidationResult } from "@/types"
77
import { useSessionStore } from "@/state/session"
8-
import { Suspense, useEffect } from "react"
8+
import { Suspense, useEffect, useRef, RefObject } from "react"
99
import { useConfigStore } from "@/state/config"
1010
import type { getConfig } from "@/flags"
1111
import { EmailVerificationDialog } from "./email-verification-dialog"
1212
import { useTopLoader } from 'nextjs-toploader'
1313
import { usePathname, useRouter, useSearchParams } from "next/navigation"
14+
import { useEventListener } from 'usehooks-ts';
15+
import { useDebounceCallback } from 'usehooks-ts'
1416

1517
type Props = {
16-
session: SessionValidationResult
1718
config: Awaited<ReturnType<typeof getConfig>>
1819
}
1920

@@ -22,14 +23,24 @@ function RouterChecker() {
2223
const pathname = usePathname();
2324
const searchParams = useSearchParams();
2425
const router = useRouter();
26+
const refetchSession = useSessionStore((store) => store.refetchSession)
2527

2628
useEffect(() => {
2729
const _push = router.push.bind(router);
30+
const _refresh = router.refresh.bind(router);
2831

32+
// Monkey patch: https://github.com/vercel/next.js/discussions/42016#discussioncomment-9027313
2933
router.push = (href, options) => {
3034
start();
3135
_push(href, options);
3236
};
37+
38+
// Monkey patch: https://github.com/vercel/next.js/discussions/42016#discussioncomment-9027313
39+
router.refresh = () => {
40+
start();
41+
refetchSession();
42+
_refresh();
43+
};
3344
// eslint-disable-next-line react-hooks/exhaustive-deps
3445
}, [])
3546

@@ -43,17 +54,42 @@ function RouterChecker() {
4354

4455
export function ThemeProvider({
4556
children,
46-
session,
4757
config,
4858
...props
4959
}: React.ComponentProps<typeof NextThemesProvider> & Props) {
60+
const session = useSessionStore((store) => store.session)
5061
const setSession = useSessionStore((store) => store.setSession)
5162
const setConfig = useConfigStore((store) => store.setConfig)
63+
const sessionLastFetched = useSessionStore((store) => store.lastFetched)
64+
const documentRef = useRef(typeof window === 'undefined' ? null : document)
65+
66+
const fetchSession = useDebounceCallback(async () => {
67+
try {
68+
const response = await fetch('/api/get-session')
69+
const session = await response.json() as SessionValidationResult
70+
71+
if (session) {
72+
setSession(session)
73+
}
74+
} catch (error) {
75+
console.error('Failed to fetch session:', error)
76+
}
77+
}, 30)
5278

5379
useEffect(() => {
54-
setSession(session)
80+
if (session && sessionLastFetched) {
81+
return
82+
}
83+
84+
fetchSession()
5585
// eslint-disable-next-line react-hooks/exhaustive-deps
56-
}, [session]);
86+
}, [session, sessionLastFetched])
87+
88+
useEventListener('visibilitychange', () => {
89+
if (document.visibilityState === 'visible') {
90+
fetchSession()
91+
}
92+
}, documentRef as RefObject<Document>)
5793

5894
useEffect(() => {
5995
setConfig(config)

src/lib/try-catch.ts

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
type Success<T> = {
2+
data: T;
3+
error: null;
4+
};
5+
6+
type Failure<E> = {
7+
data: null;
8+
error: E;
9+
};
10+
11+
type Result<T, E = Error> = Success<T> | Failure<E>;
12+
13+
// Taken from https://gist.github.com/t3dotgg/a486c4ae66d32bf17c09c73609dacc5b
14+
export async function tryCatch<T, E = Error>(
15+
promise: Promise<T>,
16+
): Promise<Result<T, E>> {
17+
try {
18+
const data = await promise;
19+
return { data, error: null };
20+
} catch (error) {
21+
return { data: null, error: error as E };
22+
}
23+
}

src/state/session.ts

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -7,10 +7,12 @@ export const useSessionStore = create(
77
{
88
session: null as SessionValidationResult | null,
99
isLoading: true,
10+
lastFetched: null as Date | null,
1011
},
1112
(set) => ({
12-
setSession: (session: SessionValidationResult) => set({ session, isLoading: false }),
13-
clearSession: () => set({ session: null, isLoading: false }),
13+
setSession: (session: SessionValidationResult) => set({ session, isLoading: false, lastFetched: new Date() }),
14+
clearSession: () => set({ session: null, isLoading: false, lastFetched: null }),
15+
refetchSession: () => set({ isLoading: true, lastFetched: null }),
1416
})
1517
)
1618
)

0 commit comments

Comments
 (0)