Skip to content
Open
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
15 changes: 13 additions & 2 deletions apps/web/app/root.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
*/

import type { ReactNode } from "react";
import { useEffect, useState } from "react";
import Script from "next/script";
import { Links, Meta, Outlet, Scripts } from "react-router";
import type { LinksFunction } from "react-router";
Expand Down Expand Up @@ -135,8 +136,18 @@ export default function Root() {
export function HydrateFallback() {
const { resolvedTheme } = useTheme();

// if we are on the server or the theme is not resolved, return an empty div
if (typeof window === "undefined" || resolvedTheme === undefined) return <div />;
// The SPA build (ssr: false) prerenders this component on the server, where
// `typeof window === "undefined"` yields an empty <div />. On the first
// client (hydration) render, next-themes has already resolved the theme
// synchronously from localStorage, so rendering the spinner subtree here
// mismatches the prerendered HTML — React throws error #418 on every page
// load and re-renders the shell client-side. Gate on `mounted` so the
// hydration pass renders the same empty <div /> as the prerender; the
// spinner appears immediately after mount.
const [mounted, setMounted] = useState(false);
useEffect(() => setMounted(true), []);

if (!mounted || resolvedTheme === undefined) return <div />;

return (
<div className="relative flex h-screen w-full items-center justify-center bg-canvas">
Expand Down