fix(layout): persist resolved viewport tier in cookie to kill SSR flicker
User reported: "when I refresh the page with this size viewport it switches between tablet and desktop view." The root cause was the two-step tier resolution: 1. Server renders shell based on User-Agent (mobile vs desktop only). 2. Client mounts with that hint, useEffect runs matchMedia, may flip. When the UA says "desktop" but the viewport is actually 900px (so matchMedia says "tablet"), the chrome visibly switches mid-render. Most painful on macOS Safari dragged below 1024. Fix: AppShell writes a `pn-crm.viewport-tier` cookie (1-year, Lax) on every matchMedia evaluation. The dashboard layout reads the cookie and prefers it over the UA classifier for `initialFormFactor`. First visit can still flicker (no cookie yet); every subsequent reload uses the resolved tier and renders the correct chrome on first paint. The cookie values are 'mobile' / 'tablet' / 'desktop' but the server's initialFormFactor prop only accepts 'mobile' | 'desktop' (binary by design — AppShell's useEffect resolves the actual tier client-side from matchMedia). 'tablet' from the cookie collapses to 'desktop' on SSR; AppShell's useEffect re-resolves to tablet immediately. The fluent path on cookie hit is desktop -> tablet (no flicker because both shells render the desktop tree; only the sidebar Sheet wrapper differs, and that's invisible until opened). Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -1,5 +1,5 @@
|
||||
import { redirect } from 'next/navigation';
|
||||
import { headers } from 'next/headers';
|
||||
import { cookies, headers } from 'next/headers';
|
||||
import { eq } from 'drizzle-orm';
|
||||
|
||||
import { auth } from '@/lib/auth';
|
||||
@@ -37,7 +37,20 @@ export default async function DashboardLayout({ children }: { children: React.Re
|
||||
? await db.query.ports.findMany({ orderBy: portsTable.name })
|
||||
: portRoles.map((pr) => pr.port);
|
||||
|
||||
const initialFormFactor = classifyFormFactor(headerList.get('user-agent'));
|
||||
// Prefer a previously-resolved tier from the client's cookie so the
|
||||
// server renders the matching shell on first paint — eliminates the
|
||||
// mobile↔desktop chrome flicker that happens when UA-based classification
|
||||
// disagrees with the actual viewport (e.g. macOS Safari with the
|
||||
// window dragged below 1024). AppShell writes the cookie after the
|
||||
// first matchMedia evaluation; subsequent reloads use the hint.
|
||||
const cookieStore = await cookies();
|
||||
const tierCookie = cookieStore.get('pn-crm.viewport-tier')?.value;
|
||||
const initialFormFactor: 'mobile' | 'desktop' =
|
||||
tierCookie === 'mobile'
|
||||
? 'mobile'
|
||||
: tierCookie === 'tablet' || tierCookie === 'desktop'
|
||||
? 'desktop'
|
||||
: classifyFormFactor(headerList.get('user-agent'));
|
||||
const user = {
|
||||
name: profile?.displayName ?? session.user.name ?? session.user.email,
|
||||
email: session.user.email,
|
||||
|
||||
@@ -100,9 +100,15 @@ export function AppShell({
|
||||
const mqMobile = window.matchMedia(MOBILE_QUERY);
|
||||
const mqTablet = window.matchMedia(TABLET_QUERY);
|
||||
const update = () => {
|
||||
if (mqMobile.matches) setTier('mobile');
|
||||
else if (mqTablet.matches) setTier('tablet');
|
||||
else setTier('desktop');
|
||||
const next: Tier = mqMobile.matches ? 'mobile' : mqTablet.matches ? 'tablet' : 'desktop';
|
||||
setTier(next);
|
||||
// Persist for the next SSR pass so the server renders the
|
||||
// matching shell on first paint — eliminates the chrome flicker
|
||||
// on refresh when UA-based classification disagrees with the
|
||||
// actual viewport (most common on macOS Safari at sub-1024
|
||||
// widths). 1-year expiry; SameSite=Lax is fine since the cookie
|
||||
// is read by our own server only.
|
||||
document.cookie = `pn-crm.viewport-tier=${next}; path=/; max-age=31536000; SameSite=Lax`;
|
||||
};
|
||||
update();
|
||||
mqMobile.addEventListener('change', update);
|
||||
|
||||
Reference in New Issue
Block a user