diff --git a/app/(app)/(auth)/layout.tsx b/app/(app)/(auth)/layout.tsx index cc30d85f..7f098d46 100644 --- a/app/(app)/(auth)/layout.tsx +++ b/app/(app)/(auth)/layout.tsx @@ -14,7 +14,7 @@ * would just no-op). */ -import { useEffect } from "react"; +import { Suspense, useEffect } from "react"; import { useRouter, useSearchParams } from "next/navigation"; import { useAuth } from "@/components/auth/auth-provider"; @@ -23,6 +23,24 @@ export default function AuthLayout({ }: { children: React.ReactNode; }) { + // useSearchParams forces a CSR bailout during prerender, so Next.js + // requires a Suspense boundary anywhere it's read. The gate is the + // only consumer here — isolate it so the rest of the layout can still + // SSR normally. + return ( +
+ {children}}> + {children} + +
+ ); +} + +function AuthShell({ children }: { children: React.ReactNode }) { + return
{children}
; +} + +function AuthGate({ children }: { children: React.ReactNode }) { const { user, loading } = useAuth(); const router = useRouter(); const params = useSearchParams(); @@ -35,16 +53,8 @@ export default function AuthLayout({ // Don't flash the form for an instant before the redirect fires. if (!loading && user) { - return ( -
-

Redirecting…

-
- ); + return

Redirecting…

; } - return ( -
-
{children}
-
- ); + return {children}; } diff --git a/components/social/collection-scroll-card.tsx b/components/social/collection-scroll-card.tsx index 90d31d56..401df0d8 100644 --- a/components/social/collection-scroll-card.tsx +++ b/components/social/collection-scroll-card.tsx @@ -54,22 +54,23 @@ export function CollectionScrollCard({ }) { const scrollRef = useRef(null); const cardRef = useRef(null); - const [posts, setPosts] = useState(initialPosts ?? []); + // `fetched` only holds posts pulled by the lazy IntersectionObserver + // fetch. When the parent supplies `initialPosts`, we derive `posts` + // from it directly — no mirror-into-state effect required, which means + // fresh data from the parent (e.g. after an edit) shows up on the very + // next render without a setState-cascade. + const [fetched, setFetched] = useState([]); const [fetchState, setFetchState] = useState( initialPosts ? "loaded" : "idle", ); const [activeIndex, setActiveIndex] = useState(0); - // Reconcile when the parent supplies fresh posts (e.g. after editing). - useEffect(() => { - if (!initialPosts) return; - setPosts(initialPosts); - setFetchState("loaded"); - }, [initialPosts]); + const posts = initialPosts ?? fetched; // Fetch posts the first time the carousel enters the viewport (with a // 200px rootMargin so we kick it off just before it's visible). Skipped - // entirely when `initialPosts` was provided. + // entirely when `initialPosts` was provided (fetchState starts at + // "loaded" in that case so the early-return below fires). useEffect(() => { if (fetchState !== "idle") return; const el = cardRef.current; @@ -83,7 +84,7 @@ export function CollectionScrollCard({ collections .get(c.id) .then(({ posts: rows }) => { - setPosts(rows ?? []); + setFetched(rows ?? []); setFetchState("loaded"); }) .catch(() => setFetchState("error"));