Is there a general solution to avoid blocking using cookies with CachedComponents and SSR? #86445
Replies: 1 comment 2 replies
-
|
Hi @victorpavlenko! 👋 I feel your pain. This is the classic "Dynamic Hole" vs. "App Shell" conflict in the Next.js App Router. The issue isn't that the technologies don't work together; it's strictly about where you are forcing the browser to wait. The Problem: You are "Blocking" the Shell 🛑In your code, you are calculating the "Dynamic" part (Cookies + API Fetch) before you render the // ❌ Your current flow:
// 1. Wait for Cookies (Async)
// 2. Wait for API Fetch (Async)
// 3. START Rendering <LayoutWrapper> (Header, Sidebar, etc.)
// Result: User sees "Loading..." (the Suspense fallback) for EVERYTHING.Because The Solution: Lift the Shell (Granular Suspense) 🧠To fix this, you need to move the static parts (the Layout) outside of the async boundary, and only wrap the user-dependent parts in Suspense. This allows Next.js (and PPR) to send the Step-by-step Refactor:1. Create a dedicated component for the User Data: // components/UserFeed.tsx (or similar)
import { HydrationBoundary, dehydrate } from "@tanstack/react-query"
import { getQueryClient } from "@/lib/queryClient"
import { cookies } from "next/headers"
import Component from "@/entrypoints/main/home" // Renaming 'Component' to something specific is good practice!
export default async function UserFeed() {
const queryClient = getQueryClient()
const cookieStore = await cookies() // This makes THIS component dynamic
const token = cookieStore.get("accessToken")?.value
await queryClient.prefetchQuery({
queryKey: ["user"],
queryFn: async () => {
const response = await fetch(`${process.env.NEXT_PUBLIC_API_ENDPOINT}auth/self`, {
headers: { Authorization: `Bearer ${token}` }
})
return (await response.json()).data
},
})
return (
<HydrationBoundary state={dehydrate(queryClient)}>
<Component />
</HydrationBoundary>
)
}2. Update your Page to render the Layout FIRST: // app/page.tsx
import { Suspense } from "react"
import LayoutWrapper from "@/entrypoints/main/layout"
import UserFeed from "@/components/UserFeed"
import { Skeleton } from "@/components/ui/skeleton" // Much better than "Loading..." text!
export default function HomePage() {
return (
/* ✅ Layout renders INSTANTLY because it is not awaited */
<LayoutWrapper>
<Suspense fallback={<Skeleton className="h-96 w-full" />}>
<UserFeed />
</Suspense>
</LayoutWrapper>
)
}"But what if my Layout needs the user data?" (e.g., Avatar in Header)If your // Inside LayoutWrapper.tsx
<nav>
<Logo />
<Suspense fallback={<AvatarSkeleton />}>
<UserAvatar /> {/* Fetches its own data independently */}
</Suspense>
</nav>This pattern—streaming parallel data requirements—is how you unlock the true power of the App Router and avoid that "all-or-nothing" loading state! 🚀 Have a look at this video to understand better Let me know if that fixes it! (If it does, purely optional, but marking this as the answer helps others find the solution too!)" |
Beta Was this translation helpful? Give feedback.
Uh oh!
There was an error while loading. Please reload this page.
-
Everything works individually.
SSR works.
PPR works.
Cookies work.
But when you need to do all of this together for authorization, the first thing the user sees is Loading...
Smart people, I know you exist. Can you help me come up with a solution to get around this?
Beta Was this translation helpful? Give feedback.
All reactions