T
TanStack•8mo ago
deep-jade

App unmounts on page navigation

With Auth Provider when doing page navigation with createRootRouteWithContext seems like <Outlet /> gets remounted each time. I hit this issue with Convex Auth, so I tried to write my own Provider with Supabase and noticed same issue. It results in disappearance of shared components like <AppSidebar /> and re-checking auth each time page navigation happens. _authed.tsx:
export const Route = createFileRoute('/_authed')({
beforeLoad: ({ context }) => {
const { session } = context.auth;
if (!session) {
throw redirect({ to: '/signin' });
}
},
component: AuthenticatedLayoutRoute,
});

function AuthenticatedLayoutRoute() {
return <Outlet />;
}
export const Route = createFileRoute('/_authed')({
beforeLoad: ({ context }) => {
const { session } = context.auth;
if (!session) {
throw redirect({ to: '/signin' });
}
},
component: AuthenticatedLayoutRoute,
});

function AuthenticatedLayoutRoute() {
return <Outlet />;
}
_layout.tsx for authed pages
export const Route = createFileRoute('/_authed/_layout')({
component: RouteComponent,
});

function RouteComponent() {
return (
<SidebarProvider>
<AppSidebar />
<main className="flex-1">
<SidebarTrigger />
<Outlet />
</main>
</SidebarProvider>
);
}
export const Route = createFileRoute('/_authed/_layout')({
component: RouteComponent,
});

function RouteComponent() {
return (
<SidebarProvider>
<AppSidebar />
<main className="flex-1">
<SidebarTrigger />
<Outlet />
</main>
</SidebarProvider>
);
}
app.tsx:
function InnerApp() {
const auth = useAuth();
return <RouterProvider router={router} context={{ auth }} />;
}

export default function App() {
return (
<AuthProvider supabase={supabase}>
<InnerApp />
</AuthProvider>
);
}
function InnerApp() {
const auth = useAuth();
return <RouterProvider router={router} context={{ auth }} />;
}

export default function App() {
return (
<AuthProvider supabase={supabase}>
<InnerApp />
</AuthProvider>
);
}
35 Replies
deep-jade
deep-jadeOP•8mo ago
AuthProvider.tsx:
export const AuthProvider = ({
children,
supabase,
}: {
children: React.ReactNode;
supabase: SupabaseClient;
}) => {
const [session, setSession] = useState<Session | null>(null);

useEffect(() => {
supabase.auth.getSession().then(({ data: { session } }) => {
setSession(session);
});

const {
data: { subscription },
} = supabase.auth.onAuthStateChange((_, session) => {
setSession(session);
});

return () => subscription.unsubscribe();
}, [supabase.auth]);

const signInWithGoogle = async () => {
await supabase.auth.signInWithOAuth({
provider: 'google',
options: { redirectTo: '/notes' },
});
};

const signInWithGithub = async () => {
await supabase.auth.signInWithOAuth({
provider: 'github',
options: { redirectTo: '/notes' },
});
};

const signInWithMagicLink = async (email: string) => {
await supabase.auth.signInWithOtp({ email });
};

const signOut = async () => {
await supabase.auth.signOut();
};

return (
<AuthContext.Provider
value={{
session,
signInWithGoogle,
signInWithGithub,
signInWithMagicLink,
signOut,
}}
>
{children}
</AuthContext.Provider>
);
};
export const AuthProvider = ({
children,
supabase,
}: {
children: React.ReactNode;
supabase: SupabaseClient;
}) => {
const [session, setSession] = useState<Session | null>(null);

useEffect(() => {
supabase.auth.getSession().then(({ data: { session } }) => {
setSession(session);
});

const {
data: { subscription },
} = supabase.auth.onAuthStateChange((_, session) => {
setSession(session);
});

return () => subscription.unsubscribe();
}, [supabase.auth]);

const signInWithGoogle = async () => {
await supabase.auth.signInWithOAuth({
provider: 'google',
options: { redirectTo: '/notes' },
});
};

const signInWithGithub = async () => {
await supabase.auth.signInWithOAuth({
provider: 'github',
options: { redirectTo: '/notes' },
});
};

const signInWithMagicLink = async (email: string) => {
await supabase.auth.signInWithOtp({ email });
};

const signOut = async () => {
await supabase.auth.signOut();
};

return (
<AuthContext.Provider
value={{
session,
signInWithGoogle,
signInWithGithub,
signInWithMagicLink,
signOut,
}}
>
{children}
</AuthContext.Provider>
);
};
useAuth.ts:
export const useAuth = () => useContext(AuthContext);
export const useAuth = () => useContext(AuthContext);
rare-sapphire
rare-sapphire•8mo ago
can you please create a complete minimal example by forking one of the existing stackblitz examples?
deep-jade
deep-jadeOP•8mo ago
@Manuel Schiller It could be quite tough as it needs Convex or Supabase integrations to showcase that. But maybe let's start from another foot. I found this repo: https://github.com/get-convex/convex-saas/tree/main I tried to reflect the code from there for Convex Auth. My _authed.tsx layout example:
import { createFileRoute, Outlet, useNavigate } from '@tanstack/react-router';
import { useConvexAuth } from 'convex/react';
import { useEffect } from 'react';

export const Route = createFileRoute('/_authed')({
component: AuthenticatedLayout,
});

function AuthenticatedLayout() {
const { isAuthenticated, isLoading } = useConvexAuth();
const navigate = useNavigate();

useEffect(() => {
// Redirect to sigin page if user is not authenticated.
if (!isLoading && !isAuthenticated) {
navigate({ to: '/signin' });
}
}, [isLoading, isAuthenticated, navigate]);

if (isLoading && !isAuthenticated) {
return null;
}

return <Outlet />;
}
import { createFileRoute, Outlet, useNavigate } from '@tanstack/react-router';
import { useConvexAuth } from 'convex/react';
import { useEffect } from 'react';

export const Route = createFileRoute('/_authed')({
component: AuthenticatedLayout,
});

function AuthenticatedLayout() {
const { isAuthenticated, isLoading } = useConvexAuth();
const navigate = useNavigate();

useEffect(() => {
// Redirect to sigin page if user is not authenticated.
if (!isLoading && !isAuthenticated) {
navigate({ to: '/signin' });
}
}, [isLoading, isAuthenticated, navigate]);

if (isLoading && !isAuthenticated) {
return null;
}

return <Outlet />;
}
But this code results in unmounting <Outlet /> each time I navigate. This means that AppSidebar I have within _authed/_layout.tsx is dissapering each time I do page navigation.
import { AppSidebar } from '@/components/navigation/app-sidebar';
import { SidebarProvider, SidebarTrigger } from '@/components/ui/sidebar';
import { createFileRoute, Outlet } from '@tanstack/react-router';

export const Route = createFileRoute('/_authed/_layout')({
component: RouteComponent,
});

function RouteComponent() {
return (
<SidebarProvider>
<AppSidebar />
<main className="flex-1">
<SidebarTrigger />
<Outlet />
</main>
</SidebarProvider>
);
}
import { AppSidebar } from '@/components/navigation/app-sidebar';
import { SidebarProvider, SidebarTrigger } from '@/components/ui/sidebar';
import { createFileRoute, Outlet } from '@tanstack/react-router';

export const Route = createFileRoute('/_authed/_layout')({
component: RouteComponent,
});

function RouteComponent() {
return (
<SidebarProvider>
<AppSidebar />
<main className="flex-1">
<SidebarTrigger />
<Outlet />
</main>
</SidebarProvider>
);
}
GitHub
GitHub - get-convex/convex-saas: A production-ready Convex Stack fo...
A production-ready Convex Stack for your next SaaS application with Convex Auth, Stripe, TanStack, Resend, Tailwindcss, and shadcn. - get-convex/convex-saas
rare-sapphire
rare-sapphire•8mo ago
when you say "unmount", what do you mean? re-render?
deep-jade
deep-jadeOP•8mo ago
yes
rare-sapphire
rare-sapphire•8mo ago
or does it actually unmount and re-mount?
deep-jade
deep-jadeOP•8mo ago
re-render would be probably the closest description. Basically when doing page navigation from one authenticated route to the other, AppSidebar dissapears.
rare-sapphire
rare-sapphire•8mo ago
you should narrow the terminology down. add some logs to find if it just re-renders or actually re-mounts
useEffect(() => {
console.log('mounted')

return () => {
console.log('unmounted')
}
}, []);
useEffect(() => {
console.log('mounted')

return () => {
console.log('unmounted')
}
}, []);
deep-jade
deep-jadeOP•8mo ago
deep-jade
deep-jadeOP•8mo ago
logs
No description
rare-sapphire
rare-sapphire•8mo ago
log the isLoading value from useConvextAuth as well to make sure it's stable
deep-jade
deep-jadeOP•8mo ago
here you go
rare-sapphire
rare-sapphire•8mo ago
inside the other useEffect as well
deep-jade
deep-jadeOP•8mo ago
here you go, thanks for help:
rare-sapphire
rare-sapphire•8mo ago
so you see that isLoading toggles and thus causes the effect to run
rare-sapphire
rare-sapphire•8mo ago
i suspect this is due to the configuration of queryClient https://github.com/get-convex/convex-saas/blob/main/src/app.tsx#L14
GitHub
convex-saas/src/app.tsx at main · get-convex/convex-saas
A production-ready Convex Stack for your next SaaS application with Convex Auth, Stripe, TanStack, Resend, Tailwindcss, and shadcn. - get-convex/convex-saas
rare-sapphire
rare-sapphire•8mo ago
query considers data to be stale by default
rare-sapphire
rare-sapphire•8mo ago
Important Defaults | TanStack Query React Docs
Out of the box, TanStack Query is configured with aggressive but sane defaults. Sometimes these defaults can catch new users off guard or make learning/debugging difficult if they are unknown by the u...
rare-sapphire
rare-sapphire•8mo ago
so upon each navigation, this beforeLoad refetches the data in the background, but causes the isLoading flag to toggle https://github.com/get-convex/convex-saas/blob/main/src/routes/_app.tsx#L10
GitHub
convex-saas/src/routes/_app.tsx at main · get-convex/convex-saas
A production-ready Convex Stack for your next SaaS application with Convex Auth, Stripe, TanStack, Resend, Tailwindcss, and shadcn. - get-convex/convex-saas
deep-jade
deep-jadeOP•8mo ago
I don't have that beforeLoad in my app at all
rare-sapphire
rare-sapphire•8mo ago
did you not copy that template? let's continue when you have a complete minimal example please
deep-jade
deep-jadeOP•8mo ago
GitHub
GitHub - KarolusD/convex-tanstack-router-auth
Contribute to KarolusD/convex-tanstack-router-auth development by creating an account on GitHub.
deep-jade
deep-jadeOP•8mo ago
Hope this helps with debugging. I will be following this topic. Thank you for your help in advance. ideas?
rare-sapphire
rare-sapphire•8mo ago
cc @ballingt what's the expectation how useConvexAuth() should behave here?
absent-sapphire
absent-sapphire•8mo ago
Let's make a small repro like @Manuel Schiller is suggesting, since you're seeing this in Clerk + Convex, Convex Auth + Convex, or Supabase there's something deeper here looking ah sorry you did that with https://github.com/KarolusD/convex-tanstack-router-auth, checking it out now useConvexAuth() just exposes a {isLoading, isAuthenticated} boolean from higher in the React element tree, provided by <ConvexProviderWithAuth> which is usually wrapped by ConvexProviderWithClerk or ConvexAuthProvider or similar @Karolus do you have a convex version handy? What pages should I navigate between to see this?
deep-jade
deep-jadeOP•8mo ago
/notes and /tasks
deep-jade
deep-jadeOP•8mo ago
GitHub
GitHub - KarolusD/convex-tanstack-router-auth
Contribute to KarolusD/convex-tanstack-router-auth development by creating an account on GitHub.
deep-jade
deep-jadeOP•8mo ago
ignore Tauri, just run pnpm run dev
absent-sapphire
absent-sapphire•8mo ago
@Karolus You're using a tags, so these are full reloads
deep-jade
deep-jadeOP•8mo ago
^^ rookie mistake
rare-sapphire
rare-sapphire•8mo ago
omg sorry @ballingt to bother you... could have seen that myself thought it was some convex issue...
absent-sapphire
absent-sapphire•8mo ago
I want to make this kind of thing work with Start, where auth can block! But yeah keeping everything client side there will be some loading
deep-jade
deep-jadeOP•8mo ago
sorry guys for trouble, I was testing the auth a component where just copy pasta from shadcn, so I forgot about that
absent-sapphire
absent-sapphire•8mo ago
nice, yeah that fixes
deep-jade
deep-jadeOP•8mo ago
thanks 😄 solved

Did you find this page helpful?