T
TanStack2y ago
foreign-sapphire

Best Practices for Using tanstack-react-query with GraphQL in a Monorepo Structure

Hey 👋🏽 , I am using tanstack-react-query in a monorepo. I have a package called shared-features. This package contains features, which uses react-query. I'm using GraphQL, so I can't just use a defaultQueryFn. I need a way to inject a query-fn. Here's my current setup at the main-app, which should consume the shared-features-package.
// apps/main/src/features/query.js
import { QueryClient, QueryCache, QueryClientProvider } from "@tanstack/react-query";
import { getToggleIsLoggedInAction } from "shared-features/login";
export { QueryClientProvider };
import { request } from "graphql-request";

export const rq = (requestDoc, variables) =>
request(import.meta.env.VITE_GQL_URL, requestDoc, variables).catch(
(error) => {
//prepare the error
return Promise.reject(preparedError);
}
);

export const queryClient = new QueryClient({
defaultOptions: {
queries: {
retry: false,
},
},
queryCache: new QueryCache({
onError: (error) => {
if (error?.response?.status === 401) {
getToggleIsLoggedInAction()(false);
}
},
}),
});
// apps/main/src/features/query.js
import { QueryClient, QueryCache, QueryClientProvider } from "@tanstack/react-query";
import { getToggleIsLoggedInAction } from "shared-features/login";
export { QueryClientProvider };
import { request } from "graphql-request";

export const rq = (requestDoc, variables) =>
request(import.meta.env.VITE_GQL_URL, requestDoc, variables).catch(
(error) => {
//prepare the error
return Promise.reject(preparedError);
}
);

export const queryClient = new QueryClient({
defaultOptions: {
queries: {
retry: false,
},
},
queryCache: new QueryCache({
onError: (error) => {
if (error?.response?.status === 401) {
getToggleIsLoggedInAction()(false);
}
},
}),
});
Here's how I make use of the queries:
import { useQuery } from "@tanstack/react-query";
import { gql } from "graphql-request";
import { rq } from "@/features/query";

const query = gql`
query Customers($filter: CustomersFilterInput!) {
customers(filter: $filter) {
fName
lName
}
}
`;
export function useCustomerQuery({ filter }) {
return useQuery({
queryKey: ["customers", "list", filter],
queryFn: async () => {
return rq(query, { filter }).then((data) => data.customers);
},
});
}
import { useQuery } from "@tanstack/react-query";
import { gql } from "graphql-request";
import { rq } from "@/features/query";

const query = gql`
query Customers($filter: CustomersFilterInput!) {
customers(filter: $filter) {
fName
lName
}
}
`;
export function useCustomerQuery({ filter }) {
return useQuery({
queryKey: ["customers", "list", filter],
queryFn: async () => {
return rq(query, { filter }).then((data) => data.customers);
},
});
}
In the package shared-features I dream of hooks like this one
export function useCustomerQuery({ filter }) {
const quClient = useQueryClient();
return useQuery({
queryKey: ["customers", filter],
queryFn: quClient.injectedQueryFn(props).then((data) => data.customers),
});
}
export function useCustomerQuery({ filter }) {
const quClient = useQueryClient();
return useQuery({
queryKey: ["customers", filter],
queryFn: quClient.injectedQueryFn(props).then((data) => data.customers),
});
}
Do you have a solution for my issue? Thanks!
13 Replies
foreign-sapphire
foreign-sapphireOP2y ago
Hey @TkDodo 🔮, Can you please help me here? https://discord.com/channels/719702312431386674/1225012341448376371/1225012341448376371 If possible, I would like to use react-query features and not use a custom context. Thanks
protestant-coral
protestant-coral2y ago
What's the question here? What is injectedQueryFn supposed to be? I don't even see a real question... please do better
foreign-sapphire
foreign-sapphireOP2y ago
Oh, sorry. I mean, I look for a feature to achieve a queryFn injection, so I can share my custom useQuery hooks in a monorepo structure. The pseudo code using injectedQueryFn should just show my use-case. I cannot use the defaultQueryFn-ft, because I have to pass the gql-query, not just the queryKey. So my question is: What's the recommended way to provide custom useQuery - hooks via a package in a monorepo? Or, how do you solve this? Surely you also share features that use useQuery in a monorepo and have different endpoints, or different ways to request the endpoints. Adding a custom value to the QueryClient would be a solution, but I could not achieve that so far. @TkDodo 🔮 Can you follow my request now? Do I have to solve my problem outside of react-query, for example with a React context?
protestant-coral
protestant-coral2y ago
sorry, no, I don't understand. Is this graphQL specific? Why can't you just add a static queryFn to your custom hook? instead of:
queryFn: quClient.injectedQueryFn(props).then((data) => data.customers),
queryFn: quClient.injectedQueryFn(props).then((data) => data.customers),
do:
import { someFunction } from 'somewhere'

queryFn: someFunction(props).then((data) => data.customers)
import { someFunction } from 'somewhere'

queryFn: someFunction(props).then((data) => data.customers)
why do you need the queryClient for that ?
foreign-sapphire
foreign-sapphireOP2y ago
Cause someFunction is context-specific. someFunction is different in the library showcase than in the consuming app. hook in the shared-lib:
const gqlQuery = gql`
query Customers($filter: CustomersFilterInput!) {
customers(filter: $filter) {
fName
lName
}
}
`;

export function useCustomerQuery({ filter }) {
const quClient = useQueryClient();
return useQuery({
queryKey: ["customers", filter],
queryFn: // here I need a content-specific function, which could take the filter or queryKey and the gqlQuery,
});
}
const gqlQuery = gql`
query Customers($filter: CustomersFilterInput!) {
customers(filter: $filter) {
fName
lName
}
}
`;

export function useCustomerQuery({ filter }) {
const quClient = useQueryClient();
return useQuery({
queryKey: ["customers", filter],
queryFn: // here I need a content-specific function, which could take the filter or queryKey and the gqlQuery,
});
}
In app:
import { request } from "graphql-request";

const someFunction = (gqlQuery, variables) =>
request(import.meta.env.VITE_GQL_URL, gqlQuery, variables).catch(
(error) => {
//prepare the error
return Promise.reject(preparedError);
}
);
import { request } from "graphql-request";

const someFunction = (gqlQuery, variables) =>
request(import.meta.env.VITE_GQL_URL, gqlQuery, variables).catch(
(error) => {
//prepare the error
return Promise.reject(preparedError);
}
);
In the showcase of the shared lib:
import { request } from "graphql-request";

const someFunction = (gqlQuery, variables) =>
request("https://my-gql-placeholder/endpoint", gqlQuery, variables);
import { request } from "graphql-request";

const someFunction = (gqlQuery, variables) =>
request("https://my-gql-placeholder/endpoint", gqlQuery, variables);
protestant-coral
protestant-coral2y ago
then you just need to provide your own context and have someFunction come from useSomeContext() rather than an import. This is standard dependency injection
foreign-sapphire
foreign-sapphireOP2y ago
I understand, I was hoping that I could manage without my own context. How do you do this if you have different endpoints in different apps that consume the same shared custom userQuery hooks?
protestant-coral
protestant-coral2y ago
you provide an api client to your app as a singleton, which has a baseURL set. Then your queryFns call api.request(...)
foreign-sapphire
foreign-sapphireOP2y ago
and you inject that api client via react context at the apps?
protestant-coral
protestant-coral2y ago
that's the react way to provide things. Why does it sound like you think that's a bad thing? useQueryClient() also uses context ...
foreign-sapphire
foreign-sapphireOP2y ago
I don't think, that this is a bad thing. React context is great.
I just thought it would be cool to include the api client in react-query. But the more I think about it, it's better outside of react-query anyway. That means your shared-features lib also provides the context. And you also use this context for userQuery-hooks directly in the app?
protestant-coral
protestant-coral2y ago
this is hypothetical because I don't have such a structure
foreign-sapphire
foreign-sapphireOP2y ago
Good morning @TkDodo 🔮 , thanks for your help. I have now made an apiClient available via a custom react context in order to be able to use my library useQuery-hooks in the app.
<APIClientProvider value={apiClient}>
<QueryClientProvider client={queryClient}>
<RouterProvider router={router}></RouterProvider>
</QueryClientProvider>
</APIClientProvider>
<APIClientProvider value={apiClient}>
<QueryClientProvider client={queryClient}>
<RouterProvider router={router}></RouterProvider>
</QueryClientProvider>
</APIClientProvider>
export function useCustomerQuery({ filter }) {
const apiClient = useAPIClient();

return useQuery({
queryKey: ["customers", "list", filter],
queryFn: async () => {
return apiClient
.rq(query, { filter }).then((data) => data.filterCustomers);
},
});
}
export function useCustomerQuery({ filter }) {
const apiClient = useAPIClient();

return useQuery({
queryKey: ["customers", "list", filter],
queryFn: async () => {
return apiClient
.rq(query, { filter }).then((data) => data.filterCustomers);
},
});
}
I still have a problem with the QueryOptions-functions, because it is not a hook.
export const currentUserQueryOptions = () => {
const apiClient = useAPIClient();
// 18:21 error React Hook "useAPIClient" is called in function "currentUserQueryOptions" that is neither a React function component nor a custom React Hook function. React component names must start with an uppercase letter. React Hook names must start with the word "use" react-hooks/rules-of-hooks

return queryOptions({
queryKey: ["users", "detail", "me"],
staleTime: 1000 * 1800,
queryFn: () =>
apiClient.rq(currentUserQuery).then((data) => data.currentUser),
});
};
export const currentUserQueryOptions = () => {
const apiClient = useAPIClient();
// 18:21 error React Hook "useAPIClient" is called in function "currentUserQueryOptions" that is neither a React function component nor a custom React Hook function. React component names must start with an uppercase letter. React Hook names must start with the word "use" react-hooks/rules-of-hooks

return queryOptions({
queryKey: ["users", "detail", "me"],
staleTime: 1000 * 1800,
queryFn: () =>
apiClient.rq(currentUserQuery).then((data) => data.currentUser),
});
};
Do you have a solution for that? Thanks

Did you find this page helpful?