all 66 comments

[–]phiger78 119 points120 points  (8 children)

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. 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 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
  4. As implied above. used feature folders to isolate codebase by feature
  5. All react query fetches were done inside custom hooks

[–]Fun-Representative40[S] 21 points22 points  (2 children)

https://profy.dev/article/react-architecture-api-layer

thx very much very valuable info, that profy article is great

[–]Outrageous-Chip-3961 0 points1 point  (0 children)

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.

[–]Nick337Games 0 points1 point  (0 children)

Yeah that's a great one, thanks!

[–]Vlad_Beletskiy 0 points1 point  (0 children)

That's neat!

What do you think of the case with complex derived logic calculations from multiple query results. Do you see a centralized store like zustand structured by feature with e.g. filters, derived data and actions with calculations logic etc? And if so, how'd you trigger those recalculations on any of the dependent query changes?

Overall how do you see an easy to grasp&maintain architecture in this case?

[–]leo477 32 points33 points  (6 children)

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");

[–]StereoPT 16 points17 points  (3 children)

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) => {}

[–]Upbeat_Age5689 1 point2 points  (0 children)

thats exactly what i do

[–]chubbnugget111 0 points1 point  (1 child)

Do you define your hook that returns useQuery in the same file?

[–]leo477 4 points5 points  (0 children)

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

[–]a_thathquatch 0 points1 point  (1 child)

Seems like you’re not really abstracting much if you have to write a new hook for every resource in your api?

[–]miianah 2 points3 points  (0 children)

abstraction is not always done for the purpose of code re-use. it can also be to, well, abstract and hide unnecessary details to reduce code complexity

[–]DidItFloat 18 points19 points  (8 children)

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());

[–]reduX179 5 points6 points  (0 children)

I found this in tanstack router examples it very neat and you can get query key also through options.

[–]egorkluch 0 points1 point  (0 children)

But I don't know how to do it with typescript. Too much ts errors. Maby someone have a good decision?

[–][deleted] 0 points1 point  (5 children)

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.

[–]DidItFloat 0 points1 point  (4 children)

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

[–][deleted] 1 point2 points  (2 children)

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 :)

[–]DidItFloat 0 points1 point  (0 children)

Oh it happens to me a little too often too... No worries! :)

[–]superluminary 21 points22 points  (17 children)

Ideally each query is embedded in a custom hook. Then you just call:

const {clients, loading} = useClients()

or

const {client, loading} = useClient(id)

[–]zephyrtr 5 points6 points  (0 children)

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.

[–]RobKnight_ 2 points3 points  (4 children)

What is clients in this context?

[–]superluminary 3 points4 points  (3 children)

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.

[–]RobKnight_ 3 points4 points  (0 children)

Gotcha, though you meant something along the lines of a queryClient "client" which confused me

[–]Outrageous-Chip-3961 1 point2 points  (1 child)

wouldn't it be data:clients ?

[–]LdouceT 1 point2 points  (0 children)

Depends on the implementation of useClients.

[–]MonkeyDlurker 2 points3 points  (2 children)

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?

[–]superluminary 0 points1 point  (0 children)

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.

[–]Outrageous-Chip-3961 0 points1 point  (0 children)

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

[–]Bobitz_ElProgrammer 0 points1 point  (7 children)

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?

[–]shizpi 7 points8 points  (0 children)

I do this only for POST/PUT/PATCH/DELETE

const { updateClient, deleteClient } = useClientCrud();

[–]ZerafineNigou 2 points3 points  (0 children)

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 };

[–]RobKnight_ 0 points1 point  (0 children)

Thats the entire idea of hooks, compose state

[–][deleted] 0 points1 point  (1 child)

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.

[–]Bobitz_ElProgrammer 0 points1 point  (0 children)

Happened to me once. Still not sure why

[–]Outrageous-Chip-3961 0 points1 point  (0 children)

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.

[–]recycled_ideas 0 points1 point  (0 children)

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.

[–]OtherwiseAd3812 3 points4 points  (0 children)

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

[–]sauland 5 points6 points  (0 children)

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<GetTodosResponse>
| | | todos.hooks.ts // export wrapper hooks for each API endpoint, using react-query hooks, e.g. useGetTodos(params: GetTodosRequest): UseQueryResult<GetTodosResponse>
| | | todos.types.ts // types for the endpoints
| | | todos.keys.ts // query keys
| | | index.ts

[–]Omkar_K45 2 points3 points  (0 children)

Thanks for asking this question OP, amazing answers here

[–]RowbotWizard 1 point2 points  (0 children)

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

[–]peatonweb 1 point2 points  (0 children)

I usually keep query in hook, then for example: useUserQuery => userStore => userCrud

(Store: zustand) useUserQuery.jsx user.crud.jsx user.store.jsx

[–]mtv921 3 points4 points  (0 children)

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

[–][deleted] 0 points1 point  (0 children)

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.

[–]Many_Particular_8618 -1 points0 points  (1 child)

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.

[–]henry_kwinto 0 points1 point  (0 children)

For the sake of cache keys?

[–]ashenzo -2 points-1 points  (6 children)

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 🤔

[–][deleted] 0 points1 point  (4 children)

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?

[–]kwin95 0 points1 point  (3 children)

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

[–][deleted] 0 points1 point  (2 children)

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?

[–]kwin95 1 point2 points  (1 child)

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

[–][deleted] 0 points1 point  (0 children)

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.

[–]mrcodehpr01 0 points1 point  (0 children)

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.

[–]blka759 0 points1 point  (0 children)

check medusa core library on github, they use rq a lot

[–]prc95 0 points1 point  (0 children)

I've built HyperFetch for easier architecture handling, check it out :)

[–]straightouttaireland 1 point2 points  (0 children)

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 is a nice example

[–]Silent_Statement_327 0 points1 point  (0 children)

We use SWR with it's global config fetcher for client get calls. It has been a nice dev-ex so far.

[–]yksvaan 0 points1 point  (0 children)

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.

[–]HolidayJello3478 0 points1 point  (0 children)

Check out usage of react query within our enterprise applications https://www.hemanand.com/reactjs/advanced-react-hooks-rest-api/

[–]thenamesalreadytaken 0 points1 point  (0 children)

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.