T
TanStack•15mo ago
typical-coral

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
correct-apricot
correct-apricot•15mo ago
you can inject the hook into the context via <RouterProvider> check out this example
correct-apricot
correct-apricot•15mo ago
correct-apricot
correct-apricot•15mo ago
function InnerApp() {
const auth = useAuth()
return <RouterProvider router={router} context={{ auth }} />
}
function InnerApp() {
const auth = useAuth()
return <RouterProvider router={router} context={{ auth }} />
}
typical-coral
typical-coralOP•15mo 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.
correct-apricot
correct-apricot•15mo ago
did you see my reply?
typical-coral
typical-coralOP•15mo 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
correct-apricot
correct-apricot•15mo 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
typical-coral
typical-coralOP•15mo 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.
correct-apricot
correct-apricot•15mo ago
out of interest, which 3rd party library provides this hook?
typical-coral
typical-coralOP•15mo 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;
correct-apricot
correct-apricot•15mo ago
how many of those cutom hooks such as usePostQuery do you have?
typical-coral
typical-coralOP•15mo ago
in bigger projects? around 30-40
correct-apricot
correct-apricot•15mo ago
how about creating a query key factory that binds the authenticated fetch. then inject the query key factory into router context.
typical-coral
typical-coralOP•15mo ago
You mean a query options factory?
correct-apricot
correct-apricot•15mo ago
sorry yes
correct-apricot
correct-apricot•15mo 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
correct-apricot
correct-apricot•15mo ago
and they call it "query key factory" although it is rather a "query options factory"
typical-coral
typical-coralOP•15mo ago
hmm interesting This does give me some ideas, thanks, I'll look into that once I'm back into work tomorrow 🙂
correct-apricot
correct-apricot•15mo ago
let me know how it worked out
typical-coral
typical-coralOP•15mo 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?