Creating domain objects
What would be the correct pattern to create domain objects that need multiple API requests?
In ngrx you would create a selector like this
This can be much more complicated as you can merge foreign objects into a user that are much deeper nested than in this example.
A post can have a property
likedByUsers
and you can also merge that into the selector too.
What would be a tanstack query setup?41 Replies
extended-salmon•2w ago
Do you really need to merge it into a user object though?
I remember a React project years ago with a very granular (and annoying) backend api as the micro service architecture wasn’t abstracted away from the frontend. First I needed to get an id then for other fields and relations other dependent queries.
I just kept all of these queries separate, where needed as simple dependent queries and relied on request deduplication and prefetching. There were data grids where each page could have a lot of queries - a single column could have many instances of the same query to fetch a name field for an id for example. Because of request deduplication that doesn't matter. Even though the backend was quite sluggish the frontend felt very snappy. Only merged such data in forms sometimes.
If data from multiple requests needs to be merged in a single object I'd definitely use RxJS to compose it into a single observable and pass that to queryFn. Note that this approach will result in a single cache entry for the combined data.
Dependent Queries | TanStack Query Angular Docs
injectQuery dependent Query Dependent (or serial) queries depend on previous ones to finish before they can execute. To achieve this, it's as easy as using the enabled option to tell a query when it i...
extended-salmon•2w ago
BTW - in the Angular community there's still a strong sense of a need to separate "smart" and "dumb" components. It depends, but I don't necessarily agree with that being always the best pattern. It can make code substantially more complex.
flat-fuchsiaOP•2w ago
Thanks a lot for your answer.
I have a few remarks. I try to keep it short and concise.
1. Creating one domain object enables us to create the object once and then pass it down to other components that then can work with it. It sure has downsides, e.g. less granular change detection.
2. Let's assume we have a
User
and the user has Posts
(nested relations can go much deeper as you already stated).
Keeping two separate objects for User
and Posts_for_user
would make the setup simpler as it doesn't require merging, I agree.
Do you "cross invalidate" queries from UserService
to PostService
and vice versa? Or does every mutation inside a Domain service
invalidate their "own" queries?
3. Caching the combined query (using rxjs) under one cache key seems a bit impractical as I would always invalidate the entire object and refetch every entity in the merged object.extended-salmon•2w ago
1. Is classic container components and why it often complicates applications. Everything needs to pass and be mapped through a container component. In my opinion it's outdated but often "not done" to say that. Unless someone requires you to follow that pattern you could consider if just getting whatever data you need in a component would simplify the architecture (within reason - a simple component can get some data from a parent. You can still organize queries in services of course - I'd recommend queryOptions for that.
2. If you structure the query keys well it's very easy to invalidate the queries needed. For example an array with a user id. Just invalidate everything with that user id. It's usually better to invalidate too much than too little.
3. Exactly: it's often much better to have separated queries. But container components make that hard and you may quickly need a ton of data mapping and merging code.
If it helps: having a query in a component is just injecting a data dependency. Even more when queryOptions keeps the fetching logic outside of the component. The component just declares that it needs the data and the service with QueryOptions and TanStack Query automate the whole fetching process.
flat-fuchsiaOP•2w ago
Ok, I can see what you mean.
Still, though. I can't get my head wrapped around the "cross service" invalidation of queries. That seems very confusing.
I can have many queries that are affected by this mutation. E.g.
extended-salmon•2w ago
I don't know your application requirements but I can imagine it's all related to that user?
You could invalidate at user level, post level etc Structure the query keys according to those needs
So if a user has blogs posts and comments below that blog post, invalidating the user could invalidate all of those and cause a refresh of the data if all of these queries contain the user id in their key
flat-fuchsiaOP•2w ago
Yes,
user
can have many relations in this example. If I understood correctly every relation of that user gets its own query (with api call), so it needs to be invalidated by the user service somehow, when it updates
ok, that means, in that user service we need to know all the other queries that might be affected by the update of the user, right?extended-salmon•2w ago
Those queries probably already require a user id? So then it's natural to include it in the query key too
Even if the backend API doesn't require it you can still include it in the query key
flat-fuchsiaOP•2w ago
Yes, true.
extended-salmon•2w ago
Just add some information to be able to make a distinction between coworker and post
queryKey: [COWORKER_KEY, userId()],
queryKey: [POST_KEY, userId()],
It looks like these could be the same now so you cannot invalidate just posts or just coworkers and if a coworker and a post would have the same id they would overwrite each others stateflat-fuchsiaOP•2w ago
Those queries need separate api requests so I thought they should get their own query. They both rely on a user id though.
extended-salmon•2w ago
Yes they can have their own query but it looks like they share a query key
E.g. if you would have both a post and a coworker with id 1
flat-fuchsiaOP•2w ago
Yes, true. What would the invalidateQueries call look like?
extended-salmon•2w ago
Your code example looks correct but consider the queryKey structure
flat-fuchsiaOP•2w ago
I'm not sure what you mean. Should I use a different key at index 0?
extended-salmon•2w ago
I don't know what goes into COWORKER_KEY. If it's a number you have overlapping query keys between coworker and post
flat-fuchsiaOP•2w ago
Ah,
COWORKER_KEY = "coworker"; POST_KEY = "post"
Just an identifier for the entityextended-salmon•2w ago
Right, yeah that would work
But you'll also need the actual id for post or coworker right?
flat-fuchsiaOP•2w ago
In this case I need the id of the user.
So both
updateUser
and updatePost
should trigger a refetch of
extended-salmon•2w ago
I'd reverse it though: first userId then post
Then you can do:
this.queryClient.invalidateQueries({ queryKey: [userId() ] })
Both coworkers and posts would be invalidatedflat-fuchsiaOP•2w ago
Correct, using UUID that works. Then all queries that depend on
userId()
need to start their queryKey with userId()
extended-salmon•2w ago
Yes easy peasy and far less code than forcing everything into a single object and a container component
flat-fuchsiaOP•2w ago
What about updating that post?
The query key of that post query doesn't include the
ID
of the post as we don't have it upfront
If the post with id 123-abc
gets updated I need to invalidate the query.
extended-salmon•2w ago
It's multiple posts
flat-fuchsiaOP•2w ago
true, bad example
extended-salmon•2w ago
Something like this?
queryKey: ['users', userId, 'posts', postId],
queryKey: ['users', userId, 'manager', managerId],
flat-fuchsiaOP•2w ago
There's no way to invalidate the query by the ID of the fetched entity right?
extended-salmon•2w ago
Can you give an example? I think it's possible as you can include objects in a key
flat-fuchsiaOP•2w ago
extended-salmon•2w ago
queryKey: ['users', userId, 'posts', postId, 'comments', commentId]
A simple array already allows a lot of control. With the example above you can invalidate everything for a user including the user data itself, their posts and comments, just their posts and comments on posts or just comments on their posts
flat-fuchsiaOP•2w ago
Inside this mutation
I only know that I'm updating a company and the companyId. So I can only invalidate all companies or the one id
extended-salmon•2w ago
You need to reverse the key order in the array
You can also use objects but it might be a bit more complex. I've definitely seen code where it makes sense to use objects in keys though.
flat-fuchsiaOP•2w ago
But I can't update the query key after requests is finished right?
I'd need to do
extended-salmon•2w ago
https://tkdodo.eu/blog/effective-react-query-keys You can find almost anything on Dominik his blog. The principles are usually easy to translate to Angular as I kept the API close to the original React.
Effective React Query Keys
Learn how to structure React Query Keys effectively as your App grows
flat-fuchsiaOP•2w ago
thank you
extended-salmon•2w ago
I'm not sure what the purpose of that code is?
Generally rely on reactivity, both TanStack Query and signals are great for that
Exception is something like a router guard and even that may change with new signal based APIs in Angular
flat-fuchsiaOP•2w ago
I'm not sure how to invalidate a query by the id of the fetched entity.
This query can only be invalidated by the string "company" or
userId()
How do you invalidate the query in a mutation that updates the company by the companyId?
This only invalidates by companyId()
extended-salmon•2w ago
The ID of the fetched entity is usually on the right. If it's included in an object it's also possible to invalidate on that
fetchCompanyByUserIdAsync
in that case the entity is the company so its id needs to be on the right
Or even without company id: [userId(), 'company']
But if you have an ID it's good to include it
[userId(), 'company', companyId()]
Or [userId(), 'companies', companyId()]
flat-fuchsiaOP•2w ago
the problem is I only have the companyId after I get the response from the queryFn
extended-salmon•2w ago
If a user has a single company [userId(), 'company'] is fine
flat-fuchsiaOP•2w ago
When I make mutation that updates the company using the companyId as input param I can't invalidate the query by the userId in the
onSuccess
of the mutation as I don't have the userId
in the mutation