T
TanStack12mo ago
quickest-silver

Cookie based authentication approach

Hello all. I have a cookie based authentication approach. In my login page and in my _app layout i have a beforeLoad function like this: login.tsx
beforeLoad: async ({ context, search }) => {
const user = await context.queryClient.ensureQueryData({
queryKey: ['auth-user'],
queryFn: AuthService.me,
staleTime: Infinity,
});

if (user) {
throw redirect({
to: search.redirect,
});
}
},
beforeLoad: async ({ context, search }) => {
const user = await context.queryClient.ensureQueryData({
queryKey: ['auth-user'],
queryFn: AuthService.me,
staleTime: Infinity,
});

if (user) {
throw redirect({
to: search.redirect,
});
}
},
_app.tsx
beforeLoad: async ({ context, location }) => {
const user = await context.queryClient.ensureQueryData({
queryKey: ['auth-user'],
queryFn: AuthService.me,
staleTime: Infinity,
});

if (!user) {
throw redirect({
to: '/login',
search: {
// Use the current location to power a redirect after login
// (Do not use `router.state.resolvedLocation` as it can
// potentially lag behind the actual current location)
redirect: location.href,
},
});
}
},
beforeLoad: async ({ context, location }) => {
const user = await context.queryClient.ensureQueryData({
queryKey: ['auth-user'],
queryFn: AuthService.me,
staleTime: Infinity,
});

if (!user) {
throw redirect({
to: '/login',
search: {
// Use the current location to power a redirect after login
// (Do not use `router.state.resolvedLocation` as it can
// potentially lag behind the actual current location)
redirect: location.href,
},
});
}
},
Is there a better approach on this using tanstack query & tanstack router?
5 Replies
genetic-orange
genetic-orange12mo ago
"better" in which way?
quickest-silver
quickest-silverOP12mo ago
maybe passing the auth state in context? using suspense? i mean a cleaner (not better sorry) way or a "by the book" approach
evident-indigo
evident-indigo7mo ago
@Manuel Schiller do You think @Zzyzx approach is valid? I'm trying to make 'context' approach and here's what I have:
export interface AuthContext {
isAuthenticated: boolean;
isLoading: boolean;
}

const AuthContext = React.createContext<AuthContext | null>(null);

export function AuthProvider({ children }: { children: React.ReactNode }) {
const { data: user, isLoading: isUserLoading } = useUserQuery();

const isAuthenticated = !!user;

console.log("Auth State → isAuthenticated:", isAuthenticated, "User:", user);

return (
<AuthContext.Provider value={{ isAuthenticated, isLoading: isUserLoading }}>
{children}
</AuthContext.Provider>
);
}

export function useAuth() {
const context = React.useContext(AuthContext);
if (!context) {
throw new Error("useAuth must be used within an AuthProvider");
}
return context;
}
export interface AuthContext {
isAuthenticated: boolean;
isLoading: boolean;
}

const AuthContext = React.createContext<AuthContext | null>(null);

export function AuthProvider({ children }: { children: React.ReactNode }) {
const { data: user, isLoading: isUserLoading } = useUserQuery();

const isAuthenticated = !!user;

console.log("Auth State → isAuthenticated:", isAuthenticated, "User:", user);

return (
<AuthContext.Provider value={{ isAuthenticated, isLoading: isUserLoading }}>
{children}
</AuthContext.Provider>
);
}

export function useAuth() {
const context = React.useContext(AuthContext);
if (!context) {
throw new Error("useAuth must be used within an AuthProvider");
}
return context;
}
main.tsx:
const router = createRouter({
routeTree,
defaultPreload: "intent",
// Since we're using React Query, we don't want loader calls to ever be stale
// This will ensure that the loader is always called when the route is preloaded or visited
defaultPreloadStaleTime: 0,
scrollRestoration: true,
context: { isAuthenticated: false, isLoading: true },
});

// Register things for typesafety
declare module "@tanstack/react-router" {
interface Register {
router: typeof router;
}
}

function InnerApp() {
const { isAuthenticated, isLoading } = useAuth();

if (isLoading) {
return null;
}
return <RouterProvider router={router} context={{ isAuthenticated }} />;
}

function App() {
return (
<AuthProvider>
<InnerApp />
</AuthProvider>
);
}

const rootElement = document.getElementById("app")!;

if (!rootElement.innerHTML) {
const root = ReactDOM.createRoot(rootElement);
root.render(
<QueryClientProvider client={queryClient}>
<App />
</QueryClientProvider>
);
}
const router = createRouter({
routeTree,
defaultPreload: "intent",
// Since we're using React Query, we don't want loader calls to ever be stale
// This will ensure that the loader is always called when the route is preloaded or visited
defaultPreloadStaleTime: 0,
scrollRestoration: true,
context: { isAuthenticated: false, isLoading: true },
});

// Register things for typesafety
declare module "@tanstack/react-router" {
interface Register {
router: typeof router;
}
}

function InnerApp() {
const { isAuthenticated, isLoading } = useAuth();

if (isLoading) {
return null;
}
return <RouterProvider router={router} context={{ isAuthenticated }} />;
}

function App() {
return (
<AuthProvider>
<InnerApp />
</AuthProvider>
);
}

const rootElement = document.getElementById("app")!;

if (!rootElement.innerHTML) {
const root = ReactDOM.createRoot(rootElement);
root.render(
<QueryClientProvider client={queryClient}>
<App />
</QueryClientProvider>
);
}
what do You think?
genetic-orange
genetic-orange7mo ago
there is no right or wrong here it totally depends on what you are using you can of course use query to fetch and cache or you can store your state in a react context and feed it into router
evident-indigo
evident-indigo7mo ago
Ahh I think I understand, so if i'll go for 'fetch and cache /me' for example then @Zzyzx approach is totally fine Hi @Manuel Schiller actually i have a question to @Zzyzx approach.
export const Route = createFileRoute("/login/")({
component: LoginComponent,
beforeLoad: async ({ context, search }) => {
try {
const data = await context.queryClient.ensureQueryData({
queryKey: ["user"],
queryFn: getUser,
staleTime: Infinity,
});
console.log("data:", data);
if (data) {
throw redirect({
to: "/dashboard",
});
}
} catch (e) {
if (e?.isRedirect) {
throw redirect({
to: "/dashboard",
});
}
}
},
});
export const Route = createFileRoute("/login/")({
component: LoginComponent,
beforeLoad: async ({ context, search }) => {
try {
const data = await context.queryClient.ensureQueryData({
queryKey: ["user"],
queryFn: getUser,
staleTime: Infinity,
});
console.log("data:", data);
if (data) {
throw redirect({
to: "/dashboard",
});
}
} catch (e) {
if (e?.isRedirect) {
throw redirect({
to: "/dashboard",
});
}
}
},
});
As You can see in beforeLoad i'm using try/catch block because of queryClient.ensureQueryData and if statement in catch block to ensure the error comes from router api, not request. Do You think is that okay? I mean if it's not breaking any rules or something?

Did you find this page helpful?