Replies: 1 comment 2 replies
|
do not redirect or navigate on a isomorphic function, this creates hydration errors all the time, try to navigate from inside the compoenent. i hope this can help you: i am using Tanstack Start and Tanstack query with axios, in my case the full app is protected, (there is no public routes) but you can use the same way to make some routes public and others protected, just create a pathless route and make it the gate, then put all protected routes inside it. this is the gate, any thing under this layout will be protected, beforeLoad will run with every navigation. import { createFileRoute, redirect } from "@tanstack/react-router";
import { exchange } from "@/shared/api/api";
export const Route = createFileRoute("/_layout")({
beforeLoad: async ({ location }) => {
await exchange().catch(() => {
throw redirect({
href: `${import.meta.env.VITE_AUTH_URL}/?from=${encodeURIComponent(location.href)}`,
});
});
},
});this is a part of my api client, where i exchange and handle 401 errors and refresh, any other errors should not be handled at this layer. export const api = axios.create({
baseURL: import.meta.env.VITE_API_URL,
withCredentials: true,
});
api.interceptors.response.use(null, async (error: AxiosError) => {
if (!isAxiosError(error) || error.response?.status !== 401) {
return Promise.reject(error);
}
const orignalRequest: RetriedConfig | undefined = error.config;
if (!orignalRequest || orignalRequest.retried) {
return Promise.reject(error);
}
orignalRequest.retried = true;
try {
await exchange(true);
} catch (err) {
error.cause = err instanceof Error ? err : undefined;
return Promise.reject(error);
}
return api(orignalRequest);
});
// exchange
const chain = async (): Promise<boolean> => {
const { data } = await axios.get<{ token: string }>(
`${import.meta.env.VITE_AUTH_URL}/api/auth/token`,
{ withCredentials: true }
);
const { data: session } = await axios.post<{ data: { status: string } }>(
`${import.meta.env.VITE_API_URL}/auth/exchange`,
null,
{
headers: { Authorization: `Bearer ${data.token}` },
withCredentials: true,
}
);
};
let inFlight: Promise<void> | null = null;
let live = false;
export const exchange = (force = false): Promise<void> => {
if (live && !force) {
return Promise.resolve();
}
inFlight ??= chain()
.then(
() => {
live = true;
},
(error: unknown) => {
live = false;
throw error;
}
)
.finally(() => {
inFlight = null;
});
return inFlight;
};then to make the setup preloading safe, i handle errors in 2 places, 1- route error component, 2- component query (i made a custom hook for it), with this setup, i can make sure side effects fires only when the user makes a real navigation. this is the custom hook: import {
type DefaultError,
type QueryKey,
type UseSuspenseQueryOptions,
type UseSuspenseQueryResult,
useSuspenseQuery,
} from "@tanstack/react-query";
import { useEffect } from "react";
import { toast } from "sonner";
import { errorMessage } from "./api";
export function useApiQuery<
TQueryFnData = unknown,
TError = DefaultError,
TData = TQueryFnData,
TQueryKey extends QueryKey = QueryKey,
>(
options: UseSuspenseQueryOptions<TQueryFnData, TError, TData, TQueryKey>
): UseSuspenseQueryResult<TData, TError> {
const query = useSuspenseQuery(options);
const message = query.error ? errorMessage(query.error) : null;
useEffect(() => {
if (message) {
toast.error(message, { id: message });
}
}, [message]);
return query;
}and this is the error component (this is a global one that handles all route errors) you can make a error component for each route if you need, i used to do that, but i made this to solve the preloading issue. export function RouteError({ error }: ErrorComponentProps) {
const message = errorMessage(error);
useEffect(() => {
toast.error(message, { id: message });
}, [message]);
return <p>{message}</p>;
}in this setup: user opens the app, beforeLoad will run and check if he is authorized: now the user is authorized and can use the app, when a route has a loader, import { noop } from "@tanstack/react-query";
import { createFileRoute } from "@tanstack/react-router";
import { sessionQuery } from "@/features/student/api/student";
import Student from "@/features/student/pages/Student";
export const Route = createFileRoute("/_layout/_sidebar/student")({
loader: ({ context }) => {
context.queryClient.query(sessionQuery).catch(noop);
},
component: Student,
});the component: import { useApiQuery } from "@/shared/api/useApiQuery";
import { sessionQuery } from "../api/student";
const Student = () => {
const { data } = useApiQuery(sessionQuery);
return (
<>
<div>{data.user_id}</div>
<div>{data.scope}</div>
<div>this is just for testing the data access layer</div>
</>
);
};
export default Student;now: they can not conflict with each other, because you only have one component rendred at a time, the route component, pending component, error component. i hope this examples help you or give an idea how to handle the 401 and other errors, i still make changes to this setup and will be happy to discuss more if it does not help you or you still have cases that needs to be handled. also you can give this to AI and ask it to create a prototype for your app, then if the prototype solves your problems, you can build on it and create your custom setup. |
Uh oh!
There was an error while loading. Please reload this page.
Uh oh!
There was an error while loading. Please reload this page.
Our app is using our micro-services suite. One of them is responsible for authentication (using Oauth2). Requests to protected endpoints must include Bearer Token. I'm trying to figure out a way to handle 401 responses and redirect to the login flow (which is started by /oauth2/login-required route in the app).
I created an
authenticatedRequestthat does thisThis way, if the user is not authenticated (first visit), or the token was revoked, or for whatever other reason the endpoint returned 401, I know it's time to redirect to /oauth2/login-required (using refresh tokens is out of scope here).
My initial tought was to have the error handled by
queryCaheThen I'm trying to defer data loading in my component
This results in the following error
I'm starting to realize that since I intend to stream the queried data, the server has already replied with 200. And then I'm changing my mind and replying with 307, so that's one possible source for the disconnect.
Is there a way to achieve what I want using start + router + query?
My next try was going to be adding error boundaries, but I'm not sure this would solve the issue, since it looks like my issue is rather related to the server, and not React-land.
All reactions