mirror of
https://github.com/willchen96/mike.git
synced 2026-07-26 23:51:08 +02:00
fix(frontend): unify full-screen loaders to remove hydration mismatch
The provider Suspense fallback, the MFA gate loader, and the (pages) layout auth loader were three near-identical spinners with different Tailwind classes. Server and client can resolve those gates differently on first paint (auth settles from localStorage before the boundary hydrates), so the differing attributes surfaced as a hydration mismatch on nearly every dev page load. One shared FullScreenLoader makes every branch render byte-identical DOM. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
parent
62da94be67
commit
cb2306c5d6
4 changed files with 23 additions and 26 deletions
|
|
@ -8,6 +8,7 @@ import { ChatHistoryProvider } from "@/app/contexts/ChatHistoryContext";
|
|||
import { SidebarContext } from "@/app/contexts/SidebarContext";
|
||||
import { PageChromeContext } from "@/app/contexts/PageChromeContext";
|
||||
import { AppSidebar } from "@/app/components/shared/AppSidebar";
|
||||
import { FullScreenLoader } from "@/app/components/shared/FullScreenLoader";
|
||||
|
||||
export default function MikeLayout({
|
||||
children,
|
||||
|
|
@ -75,11 +76,7 @@ export default function MikeLayout({
|
|||
}, [authLoading, isAuthenticated, router]);
|
||||
|
||||
if (authLoading) {
|
||||
return (
|
||||
<div className="flex h-screen items-center justify-center">
|
||||
<div className="h-6 w-6 animate-spin rounded-full border-2 border-gray-300 border-t-gray-700" />
|
||||
</div>
|
||||
);
|
||||
return <FullScreenLoader />;
|
||||
}
|
||||
|
||||
if (!isAuthenticated) return null;
|
||||
|
|
|
|||
|
|
@ -4,23 +4,16 @@ import { Suspense } from "react";
|
|||
import { AuthProvider } from "@/app/contexts/AuthContext";
|
||||
import { UserProfileProvider } from "@/app/contexts/UserProfileContext";
|
||||
import { MfaLoginGate } from "@/app/components/shared/MfaLoginGate";
|
||||
import { FullScreenLoader } from "@/app/components/shared/FullScreenLoader";
|
||||
|
||||
export function Providers({ children }: { children: React.ReactNode }) {
|
||||
return (
|
||||
<AuthProvider>
|
||||
<UserProfileProvider>
|
||||
<Suspense fallback={<ProviderLoader />}>
|
||||
<Suspense fallback={<FullScreenLoader />}>
|
||||
<MfaLoginGate>{children}</MfaLoginGate>
|
||||
</Suspense>
|
||||
</UserProfileProvider>
|
||||
</AuthProvider>
|
||||
);
|
||||
}
|
||||
|
||||
function ProviderLoader() {
|
||||
return (
|
||||
<div className="flex min-h-dvh items-center justify-center bg-gray-50/80">
|
||||
<div className="h-6 w-6 animate-spin rounded-full border-2 border-gray-200 border-t-gray-700" />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
|
|
|||
14
frontend/src/app/components/shared/FullScreenLoader.tsx
Normal file
14
frontend/src/app/components/shared/FullScreenLoader.tsx
Normal file
|
|
@ -0,0 +1,14 @@
|
|||
/**
|
||||
* The single full-screen loading state. Every gate in the provider/layout
|
||||
* chain (Suspense fallback, MFA gate, auth-loading layout) must render this
|
||||
* exact markup: the server and client can resolve those gates differently on
|
||||
* first paint, and identical DOM is what keeps that from being a hydration
|
||||
* mismatch.
|
||||
*/
|
||||
export function FullScreenLoader() {
|
||||
return (
|
||||
<div className="flex min-h-dvh items-center justify-center bg-gray-50/80">
|
||||
<div className="h-6 w-6 animate-spin rounded-full border-2 border-gray-200 border-t-gray-700" />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
|
@ -4,6 +4,7 @@ import { useEffect, useState, type ReactNode } from "react";
|
|||
import { usePathname, useRouter, useSearchParams } from "next/navigation";
|
||||
import { useAuth } from "@/app/contexts/AuthContext";
|
||||
import { useUserProfile } from "@/app/contexts/UserProfileContext";
|
||||
import { FullScreenLoader } from "@/app/components/shared/FullScreenLoader";
|
||||
import { needsMfaVerification } from "../popups/MfaVerificationPopup";
|
||||
|
||||
type GateState = "idle" | "checking" | "required" | "verified";
|
||||
|
|
@ -89,7 +90,7 @@ export function MfaLoginGate({ children }: { children: ReactNode }) {
|
|||
return gateState === "verified" ? (
|
||||
<>{children}</>
|
||||
) : (
|
||||
<FullScreenGateLoader />
|
||||
<FullScreenLoader />
|
||||
);
|
||||
}
|
||||
|
||||
|
|
@ -98,15 +99,15 @@ export function MfaLoginGate({ children }: { children: ReactNode }) {
|
|||
return <>{children}</>;
|
||||
}
|
||||
if (gateState === "verified" && isVerifyPage) {
|
||||
return <FullScreenGateLoader />;
|
||||
return <FullScreenLoader />;
|
||||
}
|
||||
if (gateState === "verified") {
|
||||
return <>{children}</>;
|
||||
}
|
||||
if (gateState === "required" && !isVerifyPage) {
|
||||
return <FullScreenGateLoader />;
|
||||
return <FullScreenLoader />;
|
||||
}
|
||||
return <FullScreenGateLoader />;
|
||||
return <FullScreenLoader />;
|
||||
}
|
||||
|
||||
return <>{children}</>;
|
||||
|
|
@ -120,14 +121,6 @@ function safeNextPath(value: string | null) {
|
|||
return value;
|
||||
}
|
||||
|
||||
function FullScreenGateLoader() {
|
||||
return (
|
||||
<div className="flex min-h-dvh items-center justify-center bg-gray-50/80">
|
||||
<div className="h-6 w-6 animate-spin rounded-full border-2 border-gray-200 border-t-gray-700" />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export function markMfaVerifiedForGate() {
|
||||
window.sessionStorage.setItem(MFA_VERIFIED_AT_KEY, String(Date.now()));
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue