xtablo-source/apps/main/src/App.tsx
Arthur Belleville 949888ec63
fix: resolve insertBefore DOM reconciliation error in React 19
UserStoreProvider was switching between three different return structures
(LoadingSpinner / children directly / Context.Provider wrapping children),
causing full fiber tree restructures in React 19 concurrent mode. When
batched with other state updates (e.g. UpgradeBlockContext loading),
React called insertBefore with a reference node already detached from
the DOM.

- Always render UserStoreContext.Provider so tree structure is stable;
  spinner vs children toggle happens inside it
- Use useRef to hold a stable Zustand store (update via setState instead
  of recreating on every render)
- Move inline <style> from App.tsx JSX into main.css to avoid React 19
  style-tag edge cases with conditional sibling rendering

Co-Authored-By: Claude Sonnet 4.6 (1M context) <noreply@anthropic.com>
2026-03-30 23:07:47 +02:00

70 lines
2.3 KiB
TypeScript

import { SessionProvider } from "@xtablo/shared/contexts/SessionContext";
import { ThemeProvider } from "@xtablo/shared/contexts/ThemeContext";
import { Toaster } from "@xtablo/ui/components/sonner";
import { BrowserRouter as Router, useRoutes } from "react-router-dom";
import { CookieBanner } from "./components/CookieBanner";
import { PendingSignupCheckout } from "./components/PendingSignupCheckout";
import { PlanAnnouncement } from "./components/PlanAnnouncement";
import { TrialUpsellModal } from "./components/TrialUpsellModal";
import { UpgradePanel } from "./components/UpgradePanel";
import { UpgradeBlockProvider } from "./contexts/UpgradeBlockContext";
import { useCookieConsent } from "./hooks/useCookieConsent";
import { useDatadogRumViewName } from "./hooks/useDatadogRumViewName";
import { publicRoutes } from "./lib/publicRoutes";
import { routes } from "./lib/routes";
import { supabase } from "./lib/supabase";
import { NotFoundPage } from "./pages/NotFoundPage";
import { UserStoreProvider } from "./providers/UserStoreProvider";
const Routes = () => {
useDatadogRumViewName();
const publicElement = useRoutes(publicRoutes);
const appElement = useRoutes(routes);
// If public route matched, render it (outside UserStoreProvider)
if (publicElement) {
return publicElement;
}
// If app route matched, render it inside UserStoreProvider and UpgradeBlockProvider
if (appElement) {
return (
<UserStoreProvider>
<UpgradeBlockProvider>
<PendingSignupCheckout />
<PlanAnnouncement />
<UpgradePanel />
<TrialUpsellModal />
{appElement}
</UpgradeBlockProvider>
</UserStoreProvider>
);
}
// Neither matched, show 404
return <NotFoundPage />;
};
export const App = () => {
const { showBanner, acceptAll, rejectAll, saveConsent } = useCookieConsent();
return (
<ThemeProvider>
<SessionProvider supabase={supabase}>
<Toaster />
<Router>
<div className="min-h-screen bg-background">
<Routes />
{showBanner && (
<CookieBanner
onAcceptAll={acceptAll}
onRejectAll={rejectAll}
onSavePreferences={saveConsent}
/>
)}
</div>
</Router>
</SessionProvider>
</ThemeProvider>
);
};