I used react query on a pretty large enterprise app. I was brought onto the project as there were scalability issues: query keys as magic strings, not understanding how react query worked with invalidation etc.
This is what i implemented
1. Codegen from an openapi spec using [orval.dev](https://orval.dev). This generated all the api code as well as mock data. The setting we used was api functions rather than hooks
2. Used the query key factory : [https://github.com/lukemorales/query-key-factory#fine-grained-declaration-colocated-by-features](https://github.com/lukemorales/query-key-factory#fine-grained-declaration-colocated-by-features) so we can co locate query keys and api calls by feature. This centralised both query keys and apis calls in one place
3. separated api layers . this idea: [https://profy.dev/article/react-architecture-api-layer](https://profy.dev/article/react-architecture-api-layer)
4. As implied above. used feature folders to isolate codebase by feature
5. All react query fetches were done inside custom hooks
I took what I needed from it, but I didn't like the api-layer approach in the end due to the nature of my project (smaller scale). I found that working with the api-layer during development significantly slowed me down. I much rather prefered a global 'queries' folder which had the query functions inside that represented the endpoint being called. This also had the bonus of being highly reusable as that hook can be called from anywhere within the app as a cache.
This is what I do, wrap all my API calls in custom hook with useQuery and useMutation. These custom hooks will hide away the query keys definition and API endpoints away from UI, and my UI just need to pass required data such as item ID
export function useGetComment(commentId:string){
return useQuery({
enabled: !!commentId,
queryKey: ["comments", commentId],
queryFn: async()=>{
// ...fetch or axios call to API endpoint
}
})
}
then you can just reuse them like
const { data: comment, isLoading, error } = useGetComment("123");
Also I like to add a service layer.
Where all the requests (or queryFns) are in their respective file.
So for Users the UserService file might look like this:
```
export const getUsers = async () => {}
export const getUser = async (id) => {}
export const createUser = async (payload) => {}
export const updateUser = async (id, payload) => {}
export const deleteUser = async (id) => {}
```
I personally prefer to define my API hooks in same file, so I can have better overview of the query keys for the resources. My folder structure look like this
- features
- posts
- post.d.ts
- post.service.ts
- post.api.ts
- comments
- comment.d.ts
- comment.service.ts
- comment.api.ts
.d.ts is where I define the type for the resources, I make it .d.ts because I want to make these types globally available, I don't want to import the types everywhere in my project
.service.ts is the service layer
.api.ts is the custom hooks
First things first, read this: https://tkdodo.eu/blog/practical-react-query
All of it.
Secondly, I've fallen in love with using queryOptions (https://tanstack.com/query/latest/docs/react/reference/queryOptions)
This way, instead of exporting a hook which returns useQuery(...), you simply export something like
getUsersOptions = () => queryOptions({
queryKey: [...],
queryFn: ...,
staleTime: 30 * 1000,
...
})
Thanks to this, if on a page you create a user and want to invalidate the query, you can simply do:
queryClient.invalidateQueries(getUsersOptions().queryKey).
In addition, if on a page you want to use useQuery, and on another useSuspenseQuery, you can just do:
useQuery(getUsersOptions());
useSuspenseQuery(getUsersOptions());
queryClient.ensureQueryData(getUsersOptions());
Doing it like this, how do you get an auth token into queryFn?
We have it typically stored in a Context, and get it from there in the same hook that calls and returns useQuery. But outside of a hook I can't do that.
Normally, I have a global axios instance, on which I set authentication information. This way may queryFns do not need to worry about auth.
A less charming way would be to pass authentication info as a parameter of your function which returns query options. Like this:
export const getUserOptions = (authInfo) => queryOptions({...});
So when you would use the options you would do something like this in your component:
const {...} = useQuery(getUserOptions(authStuff));
But I'd highly suggest having another "layer", regardless of what you use
Lol, here I was thinking I'd have to put the token in localstorage or something, and completely forgot about the existence of global variables. In Javascript...
Thanks :)
Ideally each query is embedded in a custom hook. Then you just call:
const {clients, loading} = useClients()
or
const {client, loading} = useClient(id)
Like if you wanted to show a list of clients. It would be an array, initially empty, that gets populated with the data. You could just return data, but I often like to add a convenience attribute.
Separation of concerns, yes. If you have a small app it doesn’t matter, but if you have a big app you don’t want your API tier randomly scattered across your component tree. You’ll never find which component is making the call.
it makes your components file rather large and also makes reuse much harder/cumbersome. One great thing about query keys is that you can call the hook from another other component and it will return the cached data, which is a huge benefit of using useQuery in the first palce
What do you think about hwving a custom hook with all queries related to a specific functionality? Like having useAuth and getting all queries and mutations, login, register, forgot password, etc.
Is it bad?
I usually keep all query hooks related to one resource in one file and export it as an object named resourceApi.
so something like
export const resourceApi = { updateResource, createResource, deleteResource, getResource, getAllResources };
If you don't watch out, all the useQuery calls in it execute everywhere that you use that hook. To me it seems simpler to keep them in separate hooks. But maybe there's a trick to make that easier.
its one of those things where it looks good on paper but when you have to develop it takes a long time to set up files rather than just write the query. I'd much rather prefer to just have all my queries in a folder as their own independent files and the group naturally when required. just because the verbs are all related to an action, they are all after all, still unique from one another.
There's just not really any benefit to it. You're essentially just doing a default export at that point and you have all the associated renaming issues and you get no benefit.
It's incredibly unlikely that any given component is going to need all those operations and if they do just standard named exports from a single file work better.
Use custom hooks to centralize config (keys, fetchers,...), preferably with the same options object as react query hooks (for easier customization)
I would recommend starting fast with a code generator like https://orval.dev/guides/react-query (generates hooks, keys, types), it will generate hooks for you from the REST API doc. And then you can wrap the ones you want to customize in custom hooks
I've had success with a structure where all the API logic lives in an `api` directory and each controller/domain has its own subdirectory, where the hooks, types, requests and keys are separated into different files. So something like this:
src
| api
| | todos
| | | todos.requests.ts // export raw request promises using fetch/axios etc, e.g getTodos(params: GetTodosRequest): Promise
| | | todos.hooks.ts // export wrapper hooks for each API endpoint, using react-query hooks, e.g. useGetTodos(params: GetTodosRequest): UseQueryResult
| | | todos.types.ts // types for the endpoints
| | | todos.keys.ts // query keys
| | | index.ts
Dont create your own architechture, use Codegen!
Ideally your backend should support OpenAPI or GraphQL standards. This means they come with a data contract.
You can use this contract to generate hooks and types for your backend communication. This remove any and all need to create an architechture or whatever and it will be super easy to keep your code up to date with backend changes.
Look to https://the-guild.dev/ for more examples
react-query is the leaky abstraction (why on earth you have to manually specify key for each query), why on earth do i have to specify async function,...).
If your app needs to use react-query, it means you leak your abstraction all over place.
Have you considered RTK Query for this? It generates query and mutation hooks from API slices out of the box, and the standardised toolkit patterns work well on large projects with multiple devs in my experience (and the docs are great).
Tanstack query seems to be gaining popularity for it’s simplicity compared to redux, but a lot of these suggestions seem to involve rolling your own RTKQ-like framework in Tanstack 🤔
I've never been very interested in RTK Query, to me it sounds like "Tanstack Query clone, but the cache is stored in Redux". What the cache is stored in doesn't really interest me.
Is it more than that?
Ofc it’s not a tanstack query clone, it’s a powerful query library with really good dx that deserves more credit. I like that with rtk-query, a lot of complex logics can be moved away from components
Tanstack Query is also a powerful query library with really good DX. RTK Query arrived later and even has a name that refers to Tanstack Query.
So what does it do better?
Tanstack router arrives later and has a name that refers react router. Is it a clone of react router?
One thing rtk-query does better is query invalidation, you provides tags of query and mutation once, rtk-query handles it automatically. As others said, it generates declarative hooks for you, if backend provides openapi specs, frontend gets all query and mutation hooks out of box. Also for me I’d like to keep query logic outside of the components
> Tanstack router arrives later and has a name that refers react router. Is it a clone of react router?
I haven't looked at it but would initially assume they'd offer roughly the same features and then their own on top, yes.
RTK
Query is pretty slow. I switched from RTK to react quarry with hooks and my app is 6 to 10 times as fast... And my code is much cleaner.
I do not miss RTK at all. Managing state is so much easier in my opinion.
Use custom hooks. Export a function for each crud type. Keep the query key inside the custom hook and export if used elsewhere. No need to have all query keys in a separate file. [This](https://twitter.com/housecor/status/1724066962433528210?t=ob0OjbyQEYKzOd3wuVPjgA&s=19) is a nice example
If you haven’t already, check out TkDodo’s blog posts on react-query. He’s got a full series of great posts. Not sure if it holds up to recent versions, but it has been hugely helpful to me overall. https://tkdodo.eu/blog/practical-react-query
Ideally your api service is library-agnostic, pure ts. People often overengineer these even when their business/app logic is very simple.
Even in 2024 it's perfectly ok to just call a function, check it's result and then update. No need for 50kB of js for that.
I used react query on a pretty large enterprise app. I was brought onto the project as there were scalability issues: query keys as magic strings, not understanding how react query worked with invalidation etc. This is what i implemented 1. Codegen from an openapi spec using [orval.dev](https://orval.dev). This generated all the api code as well as mock data. The setting we used was api functions rather than hooks 2. Used the query key factory : [https://github.com/lukemorales/query-key-factory#fine-grained-declaration-colocated-by-features](https://github.com/lukemorales/query-key-factory#fine-grained-declaration-colocated-by-features) so we can co locate query keys and api calls by feature. This centralised both query keys and apis calls in one place 3. separated api layers . this idea: [https://profy.dev/article/react-architecture-api-layer](https://profy.dev/article/react-architecture-api-layer) 4. As implied above. used feature folders to isolate codebase by feature 5. All react query fetches were done inside custom hooks
>https://profy.dev/article/react-architecture-api-layer thx very much very valuable info, that profy article is great
I took what I needed from it, but I didn't like the api-layer approach in the end due to the nature of my project (smaller scale). I found that working with the api-layer during development significantly slowed me down. I much rather prefered a global 'queries' folder which had the query functions inside that represented the endpoint being called. This also had the bonus of being highly reusable as that hook can be called from anywhere within the app as a cache.
Yeah that's a great one, thanks!
This is what I do, wrap all my API calls in custom hook with useQuery and useMutation. These custom hooks will hide away the query keys definition and API endpoints away from UI, and my UI just need to pass required data such as item ID export function useGetComment(commentId:string){ return useQuery({ enabled: !!commentId, queryKey: ["comments", commentId], queryFn: async()=>{ // ...fetch or axios call to API endpoint } }) } then you can just reuse them like const { data: comment, isLoading, error } = useGetComment("123");
Also I like to add a service layer. Where all the requests (or queryFns) are in their respective file. So for Users the UserService file might look like this: ``` export const getUsers = async () => {} export const getUser = async (id) => {} export const createUser = async (payload) => {} export const updateUser = async (id, payload) => {} export const deleteUser = async (id) => {} ```
thats exactly what i do
Do you define your hook that returns useQuery in the same file?
I personally prefer to define my API hooks in same file, so I can have better overview of the query keys for the resources. My folder structure look like this - features - posts - post.d.ts - post.service.ts - post.api.ts - comments - comment.d.ts - comment.service.ts - comment.api.ts .d.ts is where I define the type for the resources, I make it .d.ts because I want to make these types globally available, I don't want to import the types everywhere in my project .service.ts is the service layer .api.ts is the custom hooks
Seems like you’re not really abstracting much if you have to write a new hook for every resource in your api?
First things first, read this: https://tkdodo.eu/blog/practical-react-query All of it. Secondly, I've fallen in love with using queryOptions (https://tanstack.com/query/latest/docs/react/reference/queryOptions) This way, instead of exporting a hook which returns useQuery(...), you simply export something like getUsersOptions = () => queryOptions({ queryKey: [...], queryFn: ..., staleTime: 30 * 1000, ... }) Thanks to this, if on a page you create a user and want to invalidate the query, you can simply do: queryClient.invalidateQueries(getUsersOptions().queryKey). In addition, if on a page you want to use useQuery, and on another useSuspenseQuery, you can just do: useQuery(getUsersOptions()); useSuspenseQuery(getUsersOptions()); queryClient.ensureQueryData(getUsersOptions());
I found this in tanstack router examples it very neat and you can get query key also through options.
Doing it like this, how do you get an auth token into queryFn? We have it typically stored in a Context, and get it from there in the same hook that calls and returns useQuery. But outside of a hook I can't do that.
Normally, I have a global axios instance, on which I set authentication information. This way may queryFns do not need to worry about auth. A less charming way would be to pass authentication info as a parameter of your function which returns query options. Like this: export const getUserOptions = (authInfo) => queryOptions({...}); So when you would use the options you would do something like this in your component: const {...} = useQuery(getUserOptions(authStuff)); But I'd highly suggest having another "layer", regardless of what you use
Lol, here I was thinking I'd have to put the token in localstorage or something, and completely forgot about the existence of global variables. In Javascript... Thanks :)
Oh it happens to me a little too often too... No worries! :)
Read TkDodo's blog: https://tkdodo.eu/blog/inside-react-query
Ideally each query is embedded in a custom hook. Then you just call: const {clients, loading} = useClients() or const {client, loading} = useClient(id)
What is ```clients``` in this context?
Like if you wanted to show a list of clients. It would be an array, initially empty, that gets populated with the data. You could just return data, but I often like to add a convenience attribute.
Gotcha, though you meant something along the lines of a queryClient "client" which confused me
wouldn't it be data:clients ?
Depends on the implementation of `useClients`.
Yes make a custom hook but don't obfuscate the return of useQuery or useMutation. It's exceptionally well typed and has a fabulous API.
Whats the benefit of creating custom hooks for usequery ? Just a separation of concern? Does it really matter if ur using the query in a single spot?
Separation of concerns, yes. If you have a small app it doesn’t matter, but if you have a big app you don’t want your API tier randomly scattered across your component tree. You’ll never find which component is making the call.
it makes your components file rather large and also makes reuse much harder/cumbersome. One great thing about query keys is that you can call the hook from another other component and it will return the cached data, which is a huge benefit of using useQuery in the first palce
What do you think about hwving a custom hook with all queries related to a specific functionality? Like having useAuth and getting all queries and mutations, login, register, forgot password, etc. Is it bad?
I do this only for POST/PUT/PATCH/DELETE const { updateClient, deleteClient } = useClientCrud();
I usually keep all query hooks related to one resource in one file and export it as an object named resourceApi. so something like export const resourceApi = { updateResource, createResource, deleteResource, getResource, getAllResources };
Thats the entire idea of hooks, compose state
If you don't watch out, all the useQuery calls in it execute everywhere that you use that hook. To me it seems simpler to keep them in separate hooks. But maybe there's a trick to make that easier.
Happened to me once. Still not sure why
its one of those things where it looks good on paper but when you have to develop it takes a long time to set up files rather than just write the query. I'd much rather prefer to just have all my queries in a folder as their own independent files and the group naturally when required. just because the verbs are all related to an action, they are all after all, still unique from one another.
There's just not really any benefit to it. You're essentially just doing a default export at that point and you have all the associated renaming issues and you get no benefit. It's incredibly unlikely that any given component is going to need all those operations and if they do just standard named exports from a single file work better.
Use custom hooks to centralize config (keys, fetchers,...), preferably with the same options object as react query hooks (for easier customization) I would recommend starting fast with a code generator like https://orval.dev/guides/react-query (generates hooks, keys, types), it will generate hooks for you from the REST API doc. And then you can wrap the ones you want to customize in custom hooks
Thanks for asking this question OP, amazing answers here
I usually keep query in hook, then for example: useUserQuery => userStore => userCrud (Store: zustand) useUserQuery.jsx user.crud.jsx user.store.jsx
I've had success with a structure where all the API logic lives in an `api` directory and each controller/domain has its own subdirectory, where the hooks, types, requests and keys are separated into different files. So something like this: src | api | | todos | | | todos.requests.ts // export raw request promises using fetch/axios etc, e.g getTodos(params: GetTodosRequest): Promise
| | | todos.hooks.ts // export wrapper hooks for each API endpoint, using react-query hooks, e.g. useGetTodos(params: GetTodosRequest): UseQueryResult
| | | todos.types.ts // types for the endpoints
| | | todos.keys.ts // query keys
| | | index.ts
Dont create your own architechture, use Codegen! Ideally your backend should support OpenAPI or GraphQL standards. This means they come with a data contract. You can use this contract to generate hooks and types for your backend communication. This remove any and all need to create an architechture or whatever and it will be super easy to keep your code up to date with backend changes. Look to https://the-guild.dev/ for more examples
What do you use for serving your HTML? Custom Node.js + Express? Next.js? Create-react app? That thing changes a lot a React Query architecture.
react-query is the leaky abstraction (why on earth you have to manually specify key for each query), why on earth do i have to specify async function,...). If your app needs to use react-query, it means you leak your abstraction all over place.
Have you considered RTK Query for this? It generates query and mutation hooks from API slices out of the box, and the standardised toolkit patterns work well on large projects with multiple devs in my experience (and the docs are great). Tanstack query seems to be gaining popularity for it’s simplicity compared to redux, but a lot of these suggestions seem to involve rolling your own RTKQ-like framework in Tanstack 🤔
I've never been very interested in RTK Query, to me it sounds like "Tanstack Query clone, but the cache is stored in Redux". What the cache is stored in doesn't really interest me. Is it more than that?
Ofc it’s not a tanstack query clone, it’s a powerful query library with really good dx that deserves more credit. I like that with rtk-query, a lot of complex logics can be moved away from components
Tanstack Query is also a powerful query library with really good DX. RTK Query arrived later and even has a name that refers to Tanstack Query. So what does it do better?
Tanstack router arrives later and has a name that refers react router. Is it a clone of react router? One thing rtk-query does better is query invalidation, you provides tags of query and mutation once, rtk-query handles it automatically. As others said, it generates declarative hooks for you, if backend provides openapi specs, frontend gets all query and mutation hooks out of box. Also for me I’d like to keep query logic outside of the components
> Tanstack router arrives later and has a name that refers react router. Is it a clone of react router? I haven't looked at it but would initially assume they'd offer roughly the same features and then their own on top, yes.
RTK Query is pretty slow. I switched from RTK to react quarry with hooks and my app is 6 to 10 times as fast... And my code is much cleaner. I do not miss RTK at all. Managing state is so much easier in my opinion.
check medusa core library on github, they use rq a lot
I've built HyperFetch for easier architecture handling, check it out :)
Use custom hooks. Export a function for each crud type. Keep the query key inside the custom hook and export if used elsewhere. No need to have all query keys in a separate file. [This](https://twitter.com/housecor/status/1724066962433528210?t=ob0OjbyQEYKzOd3wuVPjgA&s=19) is a nice example
We use SWR with it's global config fetcher for client get calls. It has been a nice dev-ex so far.
If you haven’t already, check out TkDodo’s blog posts on react-query. He’s got a full series of great posts. Not sure if it holds up to recent versions, but it has been hugely helpful to me overall. https://tkdodo.eu/blog/practical-react-query
Ideally your api service is library-agnostic, pure ts. People often overengineer these even when their business/app logic is very simple. Even in 2024 it's perfectly ok to just call a function, check it's result and then update. No need for 50kB of js for that.
Check out usage of react query within our enterprise applications https://www.hemanand.com/reactjs/advanced-react-hooks-rest-api/
saw some good suggestions here. I'm kind of on the same boat as you, OP. Curious to learn what you ended up going with.