T
TanStack•13mo ago
afraid-scarlet

Deeply passing down context

I have a React hook which provides a wrapper around fetch which automatically retrieves an access token via another hook. Within previous custom useQuery() hooks, I could just use that hook, since everything was React hooks. Now within TanStack Router, I don't have that option anymore. I know that I can pass the hook result through context and then pass the wrapped fetch function through to *QueryOptions() methods, but that seems extremely repetitive. Is there a better make the result of my original hook available to all my fetch*() functions without having to pass it down in every single place?
20 Replies
wise-white
wise-white•13mo ago
you can inject the hook into the context via <RouterProvider> check out this example
wise-white
wise-white•13mo ago
wise-white
wise-white•13mo ago
function InnerApp() {
const auth = useAuth()
return <RouterProvider router={router} context={{ auth }} />
}
function InnerApp() {
const auth = useAuth()
return <RouterProvider router={router} context={{ auth }} />
}
afraid-scarlet
afraid-scarletOP•13mo ago
That's what I currently do, but the follwing is exatly what I want to avoid:
export const Route = createFileRoute("/")({
component: Root,
loader: async ({context: {queryClient, fetch}}) => {
await queryClient.ensureQueryData(clientsQueryOptions(fetch));
},
});
export const Route = createFileRoute("/")({
component: Root,
loader: async ({context: {queryClient, fetch}}) => {
await queryClient.ensureQueryData(clientsQueryOptions(fetch));
},
});
const {data: {data: clients}} = useSuspenseQuery(clientsQueryOptions(Route.useRouteContext().fetch));
const {data: {data: clients}} = useSuspenseQuery(clientsQueryOptions(Route.useRouteContext().fetch));
I'd like to not have to pass it down from everywhere.
wise-white
wise-white•13mo ago
did you see my reply?
afraid-scarlet
afraid-scarletOP•13mo ago
I did, but the linked example doesn't show how to access the auth in the fetch functions without passing it through the loader
wise-white
wise-white•13mo ago
can you please provide a complete minimal example by forking one of the existing examples on stackblitz? also would be good to see a comparison on how you solved it with pure react-query
afraid-scarlet
afraid-scarletOP•13mo ago
So this is what I previously did: Query definition:
const postQuery = (postId: string) => {
// Returns a function which matches `fetch` signature but injects
// authentication headers into the request which it gets from the context
// of a third party authentication library.
const fetch = useAuthenticatedFetch();

return useQuery({
queryKey: ["post", postId],
queryFn: ({signal}) => {
const response = fetch(`http://api.example.com/posts/${postId}`);
return await response.json();
},
});
};
const postQuery = (postId: string) => {
// Returns a function which matches `fetch` signature but injects
// authentication headers into the request which it gets from the context
// of a third party authentication library.
const fetch = useAuthenticatedFetch();

return useQuery({
queryKey: ["post", postId],
queryFn: ({signal}) => {
const response = fetch(`http://api.example.com/posts/${postId}`);
return await response.json();
},
});
};
Within components:
const PostComponent = () => {
const postQuery = usePostQuery("some-post-id");

if (postQuery.isError) {
throw postQuery.error;
}

if (postQuery.isPending) {
return <Spinner />;
}

// do things with `postQuery.data` now
};
const PostComponent = () => {
const postQuery = usePostQuery("some-post-id");

if (postQuery.isError) {
throw postQuery.error;
}

if (postQuery.isPending) {
return <Spinner />;
}

// do things with `postQuery.data` now
};
What I'd like to do is this:
const PostComponent = () => {
const {data: post} = useSuspenseQuery(postQueryData("some-post-id"));

// do things with `post` now
};

export const Route = createFileRoute("/posts/$postId")({
component: PostComponent,
loader: ({context: {queryClient}) => queryClient.ensureQueryData(postQueryData("some-post-id)),
});
const PostComponent = () => {
const {data: post} = useSuspenseQuery(postQueryData("some-post-id"));

// do things with `post` now
};

export const Route = createFileRoute("/posts/$postId")({
component: PostComponent,
loader: ({context: {queryClient}) => queryClient.ensureQueryData(postQueryData("some-post-id)),
});
Basically not having to have the route itself know about the injection of the fetch. I only see two paths right now: - either inject the fetch into the context, and then pull it in every loader and component in order to pass it to every xQueryData() functions (which I really want to avoid) - or put the fetch function into a global singleton variable and let the fetchX functions just use that.
wise-white
wise-white•13mo ago
out of interest, which 3rd party library provides this hook?
afraid-scarlet
afraid-scarletOP•13mo ago
auth0-react provides the hook to get the authentication token. What I then do is this to avoid repetition:
import { useAuth0 } from "@auth0/auth0-react";
import { useCallback } from "react";

export class AuthenticatedFetchError extends Error {}
export type AuthenticatedFetch = typeof fetch;

const useAuthenticatedFetch = (): AuthenticatedFetch => {
const { getAccessTokenSilently, loginWithRedirect } = useAuth0();

return useCallback(
async (input: RequestInfo | URL, init?: RequestInit) => {
let accessToken: string;

try {
accessToken = await getAccessTokenSilently();
} catch {
await loginWithRedirect({
appState: {
returnTo: window.location.href,
},
});
throw new AuthenticatedFetchError(
"Refresh token missing, performing login redirect",
);
}

const modifiedInit = init ?? {};
modifiedInit.headers = new Headers(modifiedInit.headers);
modifiedInit.headers.set("Authorization", `Bearer ${accessToken}`);

return await fetch(input, modifiedInit);
},
[getAccessTokenSilently, loginWithRedirect],
);
};

export default useAuthenticatedFetch;
import { useAuth0 } from "@auth0/auth0-react";
import { useCallback } from "react";

export class AuthenticatedFetchError extends Error {}
export type AuthenticatedFetch = typeof fetch;

const useAuthenticatedFetch = (): AuthenticatedFetch => {
const { getAccessTokenSilently, loginWithRedirect } = useAuth0();

return useCallback(
async (input: RequestInfo | URL, init?: RequestInit) => {
let accessToken: string;

try {
accessToken = await getAccessTokenSilently();
} catch {
await loginWithRedirect({
appState: {
returnTo: window.location.href,
},
});
throw new AuthenticatedFetchError(
"Refresh token missing, performing login redirect",
);
}

const modifiedInit = init ?? {};
modifiedInit.headers = new Headers(modifiedInit.headers);
modifiedInit.headers.set("Authorization", `Bearer ${accessToken}`);

return await fetch(input, modifiedInit);
},
[getAccessTokenSilently, loginWithRedirect],
);
};

export default useAuthenticatedFetch;
wise-white
wise-white•13mo ago
how many of those cutom hooks such as usePostQuery do you have?
afraid-scarlet
afraid-scarletOP•13mo ago
in bigger projects? around 30-40
wise-white
wise-white•13mo ago
how about creating a query key factory that binds the authenticated fetch. then inject the query key factory into router context.
afraid-scarlet
afraid-scarletOP•13mo ago
You mean a query options factory?
wise-white
wise-white•13mo ago
sorry yes
wise-white
wise-white•13mo ago
i had this here in mind, hence the name https://github.com/lukemorales/query-key-factory
GitHub
GitHub - lukemorales/query-key-factory: A library for creating type...
A library for creating typesafe standardized query keys, useful for cache management in @tanstack/query - lukemorales/query-key-factory
wise-white
wise-white•13mo ago
and they call it "query key factory" although it is rather a "query options factory"
afraid-scarlet
afraid-scarletOP•13mo ago
hmm interesting This does give me some ideas, thanks, I'll look into that once I'm back into work tomorrow 🙂
wise-white
wise-white•13mo ago
let me know how it worked out
afraid-scarlet
afraid-scarletOP•13mo ago
Will do! Meanwhile I gotta wait to see if I get a response on this bug I discovered 😅 https://github.com/TanStack/query/issues/8039
GitHub
useSuspenseQuery has incorrect behavior on select errors · Issu...
Describe the bug useSuspenseQuery seems to have incorrect behavior when select function throws an error. Right now if the select function throws an error, data is set to undefined, which goes again...

Did you find this page helpful?